#!/bin/sh
# /usr/bin/dphys-config - install/update config files and trigger commands
# author Neil Franklin, last modification 2013.03.01
# copyright ETH Zuerich Physics Departement,
#   use under either modified/non-advertising BSD or GPL license

# this script is intended to be run as an normal user


### ------ configuration for this site

# first CONF_*  various site or user dependant user config variables
# then  DEBUG_* various debugging settings
# last  SYS_*   various system internal values
# some of these are overridable by command line options


# --- CONF_* various site or subnet dependant user config variables

# base directory for temporary files
#   some prefer /var/tmp for size and safety, works everywhere
#   some prefer /tmp for speed or boot-time auto-delete
CONF_TMP_DIR=/var/tmp

# base directory for config files on server
#   set to something that will produce an error message without site config
CONF_BASEURL=http://not-configured-server.example.net/not/configured/directory

# configuration name to use, usually host name, unless chroot or vhost or test
CONF_CONFNAME=`hostname`

# only process lines matching this grep regexp, default do all
CONF_LINEFILTER=".*"

# log what we have done
CONF_LOG_DONE=yes


# --- DEBUG_*, various debugging settings

# these can be set to "yes" by -D option, followed by name without DEBUG_
#   such as like this:  dphys-config -D PRINT_STEP ...

# set this to sleep after displaying each steps header
#DEBUG_SLEEP=yes

# set this to output debug state info after each step
#DEBUG_PRINT_STEP=yes

# set this to wait after each debug state info
#DEBUG_WAIT_STEP=yes

# set this to have debug dry runs install not in /, avoid killing system
#DEBUG_INSTALL_BASE=${CONF_TMP_DIR}/dphys-config-debug-install

# set this to leave temporary directories undeleted after working
#DEBUG_LEAVE_TEMPDIRS=yes

# set this to leave wget output file that was parsed
#   this requires also DEBUG_LEAVE_TEMPDIRS, else file gone with directories
#DEBUG_LEAVE_WGET_OUTPUT=yes


# --- SYS_*, various system internal values

# name of config file list file on server
SYS_CONFLIST_NAME=dphys-config.list

# where we want to do all the download/merge/diff-test stuff
#   this must be a directory that root can access (not a root-squash NFS mount)
SYS_WORKDIR=${CONF_TMP_DIR}/dphys-config-$$-work


### ------ actual implementation from here on
# no user settings any more below this point


# --- get ready to work

# report as many called program errors as possible
set -e

# sanitise this place, else some commands may fail
#   must be before any commands are executed, incl config/input processing
# /usr/local added for FreeBSD, because their ports end up in there
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
export PATH

# also sanitise this, else wget output parsing fails on some systems
LANG=C
export LANG


# --- tidy up some commands, make systematic

# stuff that goes wrong, not expected by user, not in data output, use >&2
#   so also with $0 in case this script was called by an other script
# something within the system that user does not expect, give up
CMD_FATAL="echo $0: FATAL:"
# something from users input, user will correct this and continue
CMD_ERROR="echo $0: ERROR:"
# something we can continue with, but may be wrong, and user may not suspect it
CMD_WARNING="echo $0: WARNING:"
# something most likely not wrong, but tell user for the odd case it is wrong
CMD_NOTE="echo $0: NOTE:"

# normal stuff users expect, so to stdout as normal output, no $0, no marking
CMD_INFO="echo"
# stuff users asked for, so add to stdout as normal output, no $0, but mark it
CMD_DEBUG="echo DEBUG:"


# and also put out some permanent messages, in case running as cron/init.d job
BASENAME="`basename "$0"`"
# something within the system that user does not expect, give up
CMD_LOG_FATAL="logger -t ${BASENAME} -p user.error FATAL:"
# something from users input, user can possibly correct this and continue
CMD_LOG_ERROR="logger -t ${BASENAME} -p user.error ERROR:"
# something we can continue with, but may be wrong, and user may not suspect it
CMD_LOG_WARNING="logger -t ${BASENAME} -p user.warning WARNING:"
# something most likely not wrong, but tell user for the odd case it is wrong
CMD_LOG_NOTE="logger -t ${BASENAME} -p user.notice NOTE:"

# normal stuff users expect, so no marking
CMD_LOG_INFO="logger -t ${BASENAME} -p user.info"
# stuff users asked for, but mark it as special
CMD_LOG_DEBUG="logger -t ${BASENAME} -p user.debug DEBUG:"


# other stuff we may want to use
CMD_SLEEP="sleep 2"
CMD_WAIT="read -p ---DEBUG-wait-after-step--- dummy"


# set config option controllable stuff
CMD_INFO_PRINT="${CMD_INFO}"
CMD_VERBOSE_PRINT=true


# set debug option controllable stuff
if [ "${DEBUG_SLEEP}" = yes ] ; then
  CMD_DEBUG_SLEEP="${CMD_SLEEP}"
else
  CMD_DEBUG_SLEEP=true
fi
if [ "${DEBUG_PRINT_STEP}" = yes ] ; then
  CMD_DEBUG_PRINT="${CMD_DEBUG}"
else
  CMD_DEBUG_PRINT=true
fi
if [ "${DEBUG_WAIT_STEP}" = yes ] ; then
  CMD_DEBUG_WAIT="${CMD_WAIT}"
else
  CMD_DEBUG_WAIT=true
fi


# --- config file stuff

# what we are, program and package name
NAME=dphys-config
PNAME=dphys-config

# check user config file(s), let user override settings
if [ -e /etc/"${PNAME}" ] ; then
  . /etc/"${PNAME}"
fi
if [ -e ~/."${PNAME}" ] ; then
  . ~/."${PNAME}"
fi


# --- control variable output

# set config option controllable stuff
if [ "${CONF_LOG_DONE}" = yes ] ; then
  CMD_LOG_IF_DONE="${CMD_LOG_INFO}"
else
  CMD_LOG_IF_DONE=true
fi


# --- parse command line

# so long next parameter is a set of options (= begins with an - character)
while [ "`echo "$1" | cut -c 1`" = - ] ; do

  # extract options from parameter (cut off the "-")
  OPTS="`echo "$1" | cut -c 2-`"
  shift 1

  # so long still unprocessed option characters
  while [ "${OPTS}" != "" ] ; do

    # first option to process
    OPT="`echo "${OPTS}" | cut -c 1`"
    # and rest of options for later
    OPTS="`echo "${OPTS}" | cut -c 2-`"

    case "${OPT}" in

      c)
        # configname: use this set of files config instead of hostname set
        CONF_CONFNAME="$1"
        shift 1
        ;;

      f)
        # filter: only process lines matching this grep regexp
        CONF_LINEFILTER="$1"
        shift 1
        ;;

      q)
        # quiet: don't inform user what we are doing
        CMD_INFO_PRINT=true
        ;;

      v)
        # verbose: detailed inform user what we are doing
        CMD_VERBOSE_PRINT="${CMD_INFO}"
        ;;

      D)
        # Debug: set an debug option to yes
        eval "DEBUG_$1"=yes
        if [ "DEBUG_$1" = DEBUG_SLEEP ] ; then
          CMD_DEBUG_SLEEP="${CMD_SLEEP}"
        fi
        if [ "DEBUG_$1" = DEBUG_PRINT_STEP ] ; then
          CMD_DEBUG_PRINT="${CMD_DEBUG}"
        fi
        if [ "DEBUG_$1" = DEBUG_WAIT_STEP ] ; then
          CMD_DEBUG_WAIT="${CMD_WAIT}"
        fi
        shift 1
        ;;

      h)
        # help: give out help how this script can be used
        cat << END-HELP-TEXT
Usage is: $0 [options]

options:
  -c         configname: use this set of config files instead of hostname set
  -f filter  filter: only process lines matching this grep regexp
  -q         quiet: don't give user an running report of what we are doing
  -v         verbose: give user detailed running report
  -D         Debug: activate an debug option, see source for operation
  -h         help: output this text, and then abort operation

mode         none or "none" or "init" (run directly) or "cron" (delay 0..59min)
END-HELP-TEXT
        exit 0
        ;;

      *)
        # not one of our recognized options
        ${CMD_ERROR} "unknown option: ${OPT}" >&2
        ${CMD_INFO} >&2
        # call self with -h to display help
        "$0" -h >&2
        exit 1
        ;;

    esac

  done

done


# do not use  elif [ "" = "" -o "" = "" ] ;  as some shells test fails on this
if [ "$1" = "" ] ; then
  MODE=none
elif ( [ "$1" = cron ] || [ "$1" = init ] ) ; then
  MODE="$1"
else
  ${CMD_ERROR} "unknown mode (or even multiple) given: $1" >&2
  ${CMD_INFO} >&2
  # call self with -h to display help
  "$0" -h >&2
  exit 1
fi

${CMD_DEBUG_PRINT} "options parsed"
${CMD_DEBUG_WAIT}


# --- spread config file server load

# avoid load peak on the file server from over 100 machines at same time
#   delay cron jobs of various machines up to 1 hour, do not affect init jobs
#   therefore we random wait for 0..3599 seconds before working
if [ "${MODE}" = cron ] ; then
  DELAY=`perl -e 'print int(rand(3600)), "\n"'`
  ${CMD_INFO} "waiting ${DELAY}s (0-3599s) for our time slot ..."
  sleep "${DELAY}"
  ${CMD_INFO} "waiting done, continuing ..."
fi


# --- wget handle unreliable network, DNS faillures or DNS not reachable

broken_net_wget_or_die () {

  # our many thanks to the net admins for their unreliable net
  #   and the resulting unreachable DNS, for client->host and host client auth
  # with 150 hosts times 30 files, is produces up to 5 failed hosts per run

  # later this subroutine also got used to "fix" the missing file: URLs in wget

  local URL
  URL="$1"
  local DIR
  DIR="$2"
  ${CMD_DEBUG_PRINT} "URL=${URL} DIR=${DIR}"
  ${CMD_DEBUG_WAIT}


  # support for file:<something> URLs which wget does not know about
  if [ "`echo "${URL}" | cut -c 1-5`" = "file:" ] ; then

    local FILE
    FILE="`echo "${URL}" | cut -c 6-`"
    # URL ends in /, this is directory existance test, generate fake index.html
    if [ "`echo "${FILE}" | rev | cut -c 1`" = "/" ] ; then
      # we later only test for file existance of index.html, so non-HTML is OK
      (cd "${DIR}"; ls -al "${FILE}" > index.html 2> /dev/null)
    else
      local FILEBASE
      FILEBASE="`basename "${FILE}"`"
      (cd "${DIR}"; cp -p "${FILE}" "${FILEBASE}")
    fi
    return 0

  fi


  # check if we can work
  if ! which wget > /dev/null ; then
    ${CMD_FATAL} "failed to find wget, can not work, aborting ..." >&2
    ${CMD_LOG_FATAL} "failed to find wget, can not work, aborting ..."
    exit 1
  fi

  local TRY
  for TRY in 1 2 3 ; do

    # wget gives back 1 for both DNS failure (bad) and simple 404 (expected)
    #   also we need to expect 403 if we managed to reach the server
    #     but it then failed to resolve our IP address
    #   and also for any other access violation problems
    #     such as admin giving wrong file permissions
    #   better only continue if we got 200 (got file) or 404 (no file)
    #     this fails safe, can never break config files
    #   uses an temp file for wget output, split on that to see which happened

    local WGET_OUTPUT
    WGET_OUTPUT="${SYS_WORKDIR}/@wget.output"

    # -N so we do not re-fetch unchanged files, important when large files
    #   -N and -O do not like each other, so use an (cd ; wget)
    (cd "${DIR}"; wget -N -o "${WGET_OUTPUT}" "${URL}" || true)

    local DNS_STAT
    DNS_STAT="`grep '^Resolving .*... failed: Host not found.$' \
        "${WGET_OUTPUT}" | tr -d ' '`"
    # the 6th field is the numeric HTTP return code
    #   do not depend on 7th field, textual code, can vary depending on server
    local HTTP_STAT
    HTTP_STAT="`grep '^HTTP request sent, awaiting response... ' \
        "${WGET_OUTPUT}" | tail -n 1 | cut -f 6 -d ' '`"
    if [ "${DEBUG_LEAVE_WGET_OUTPUT}" != yes ] ; then
      # offer to leave this to investigate bugs
      rm "${WGET_OUTPUT}"
    fi

    ${CMD_DEBUG_PRINT} "DNS_STAT=${DNS_STAT} HTTP_STAT=${HTTP_STAT}"

    # 2 similar "elif" as some shells built-in [ command fail on -o operator
    if [ "${DNS_STAT}" != "" ] ; then
      # having these as WARNING triggers one users hobbit system monitor
      #   is unnecessary for what are just transient network errors, use NOTE
      #   especially as all 3 attemps failing will fall into below FATAL
      ${CMD_NOTE} "failed try ${TRY} to resolve server for URL ${URL}" >&2
      ${CMD_LOG_NOTE} "failed try ${TRY} to resolve server for URL ${URL}"
    elif ( [ "${HTTP_STAT}" = 200 ] || [ "${HTTP_STAT}" = 404 ] ) ; then
      # we managed to get a file, or there is no file, both are OK for us
      return 0
    else
      # same downgrade from WARNING to NOTE as above
      ${CMD_NOTE} "failed try ${TRY} to retrieve file from" \
          "URL ${URL}, with HTTP status ${HTTP_STAT}" >&2
      ${CMD_LOG_NOTE} "failed try ${TRY} to retrieve file from" \
          "URL ${URL}, with HTTP status ${HTTP_STAT}"
    fi
 
  done

  ${CMD_FATAL} "failed entirely to retrieve file from URL ${URL}," \
      "with HTTP status ${HTTP_STAT}, aborting ..." >&2
  ${CMD_LOG_FATAL} "failed entirely to retrieve file from URL ${URL}," \
      "with HTTP status ${HTTP_STAT}, aborting ..."
  exit 1

}


# --- check for neccessary configuration done by user

${CMD_INFO_PRINT} "checking configuration for user setting CONF_BASEURL ..."
${CMD_DEBUG_SLEEP}

if `echo "${CONF_BASEURL}" | \
    grep -q 'http://not-configured-server.example.net/'` ; then
  # CONF_BASEURL still has not configured setting, so we can not work with it
  ${CMD_ERROR} "user config variable CONF_BASEURL has not been set to an" \
      "valid base directory for config files on your server, aborting ..." >&2
  ${CMD_LOG_ERROR} "user config variable CONF_BASEURL has not been set to an" \
      "valid base directory for config files on your server, aborting ..."
  exit 1
fi


# --- check for config file server

${CMD_INFO_PRINT} "checking file server for configuration ${CONF_CONFNAME} ..."
${CMD_DEBUG_SLEEP}

# use mktemp if users system has it installed
#   some systems need $$ PIDs as no utility is present, works everywhere
#   others can use mktemp, preferable as this prevents race conditions
#     in this case just add mktemp on to end of name with $$ in it, use both
if [ "`which mktemp 2> /dev/null`" != "" ]; then
  SYS_WORKDIR="`mktemp -d "${SYS_WORKDIR}.XXXXXXXXXX"`"
fi

# make work space for this and other temporary files
mkdir -p "${SYS_WORKDIR}"
# prevent access to just-wget-ed temporary files with wrong permissions
chmod 700 "${SYS_WORKDIR}"

# check for valid server URL, read directory with all hosts in it
rm -f "${SYS_WORKDIR}/index.html"
broken_net_wget_or_die "${CONF_BASEURL}/" "${SYS_WORKDIR}"
# do not rely on the contents of index.html being actual HTML
#   can be  ls -al  output in case of  file:<something>/  URLs
#   so only use this file for existance test, no parsing its content
if [ ! -f "${SYS_WORKDIR}/index.html" ] ; then
  ${CMD_FATAL} "failed to find server dir ${CONF_BASEURL}, aborting ..." >&2
  ${CMD_LOG_FATAL} "failed to find server dir ${CONF_BASEURL}, aborting ..."
  exit 1
fi


# server subdirectory for this hosts configuration
CONFHOST_URL="${CONF_BASEURL}/${CONF_CONFNAME}"

# check for valid host name, read directory of this host
rm -f "${SYS_WORKDIR}/index.html"
broken_net_wget_or_die "${CONFHOST_URL}/" "${SYS_WORKDIR}"
if [ ! -f "${SYS_WORKDIR}/index.html" ] ; then
  ${CMD_FATAL} "failed to find host dir for ${CONFHOST_URL}, aborting ..." >&2
  ${CMD_LOG_FATAL} "failed to find host dir for ${CONFHOST_URL}, aborting ..."
  exit 1
fi

${CMD_DEBUG_PRINT} "`ls -al "${SYS_WORKDIR}/index.html"`"
${CMD_DEBUG_WAIT}


# --- fetch config file from server to spool and preprocess it

fetch_and_preprocess_config_file () {

  local URL
  URL="$1"
  local WORK
  WORK="$2"
  ${CMD_DEBUG_PRINT} "URL=${URL} WORK=${WORK}"
  ${CMD_DEBUG_WAIT}


  # ensure first that work dir is there, in case config file in an new subdir
  local DIR
  DIR="`dirname "${WORK}"`"
  mkdir -p "${DIR}"

  # get the file
  broken_net_wget_or_die "${URL}" "${DIR}"

  if [ ! -f "${WORK}" ] ; then
    # config file not fetched, so we can not work on it
    ${CMD_ERROR} "no config file ${URL} fetched to ${WORK}" >&2
    ${CMD_LOG_ERROR} "no config file ${URL} fetched to ${WORK}"
    exit 1
  fi

  ${CMD_DEBUG_PRINT} "`ls -al "${WORK}"`"
  ${CMD_DEBUG_WAIT}


  # preprocessor stuff

  # header calls for preprocessing, with syntax similar to the #! shell trigger
  local HEAD
  HEAD="`head -n 1 "${WORK}"`"
  if `echo "${HEAD}" | grep -q '^#@dphys-config-preprocess'` ; then

    # use rest of file, purged of header, which is never part of output
    sed -ne '2,$p' "${WORK}" > "${WORK}.temp"
    mv "${WORK}.temp" "${WORK}"

    # what preprocessing actions the user wants, and in which row to run them
    #   extract from HEAD format:  #@dphys-config-preprocess [preprocessor...]
    # add an space after "${HEAD}" to circumvent  cut -f 2- -d ' '  brokenness
    #   if no preprocessor was listed by user, and so possibly no space in line
    local PREPROCS
    PREPROCS="`echo "${HEAD} " | cut -f 2- -d ' ' | tr -c -d 'a-z '`"
    ${CMD_DEBUG_PRINT} "HEAD=${HEAD} PREPROCS=${PREPROCS}"

    # no "${PREPROCS}", else intervening and trailing spaces end up in PREPROC
    #   and that would then really confuse case
    #   split junk will simply feed nonsense to case, and get caught by *)
    local PREPROC
    for PREPROC in ${PREPROCS} ; do
      case "${PREPROC}" in

        backtick)
          # replace backtick expressions with stdout generated by running them
          ${CMD_VERBOSE_PRINT} "preprocessing for backtick ..."
          ${CMD_DEBUG_SLEEP}
          local LINE
          # IFS= to avoid read doing separator processing (mangles spaces)
          #   and -r to prevent \ interpretation (mangles line contents)
          cat "${WORK}" | while IFS= read -r LINE ; do

            # syntax taken from the shell backtick, which is also used here
            #   may have multiple substitutions on one line, so use while loop
            while echo "${LINE}" | grep -q '`' ; do
              # the '\` here need the \ because of the `` subshell
              local FRONT
              FRONT="`echo "${LINE}" | cut -f 1  -d '\`' `"
              local TICK
              TICK="`echo "${LINE}"  | cut -f 2  -d '\`' `"
              local REST
              REST="`echo "${LINE}"  | cut -f 3- -d '\`' `"

              # prevent non zero return from user given command from killing us
              set +e
              local SUBST
              SUBST="`${TICK}`"
              RETVAL="$?"
              set -e
              if [ "${RETVAL}" != 0 ] ; then
                ${CMD_WARNING} "backtick returned non zero: ${RETVAL}" >&2
                ${CMD_LOG_WARNING} "backtick returned non zero: ${RETVAL}"
              fi

              LINE="${FRONT}${SUBST}${REST}"
            done
            echo "${LINE}" >> "${WORK}.temp"

          done
          mv "${WORK}.temp" "${WORK}"
          ;;


        if)
          # if condition met, leave rest of line, else delete it
          ${CMD_VERBOSE_PRINT} "preprocessing for if ..."
          ${CMD_DEBUG_SLEEP}
          local LINE
          cat "${WORK}" | while IFS= read -r LINE ; do

            # extract from LINE format:  #@if condition ; line-to-leave
            if echo "${LINE}" | grep -q '^#@if ' ; then
              # end of command part with ; analog to shell if/while/for
              if ! echo "${LINE}" | grep -q ';' ; then
                ${CMD_ERROR} "#@if config line \"${LINE}\" has no \";\"" >&2
                ${CMD_LOG_ERROR} "#@if config line \"${LINE}\" has no \";\""
                exit 1
              fi
              local COND
              COND="`echo "${LINE}" | cut -f 1  -d ';'`"
              LINE="`echo "${LINE}" | cut -f 2- -d ';'`"

              # purge command, which is not part of condition
              COND="`echo "${COND}" | cut -f 2- -d ' '`"
              # strip leading spaces after ; from line
              while [ "`echo "${LINE}" | cut -c 1`" = " " ] ; do
                LINE="`echo "${LINE}" | cut -c 2-`"
              done

              set +e
              if sh -c "${COND}" ; then
                echo "${LINE}" >> "${WORK}.temp"
              fi
              set -e
            else
              echo "${LINE}" >> "${WORK}.temp"
            fi

          done
          mv "${WORK}.temp" "${WORK}"
          ;;


        include)
          # replace line with contents of section file named on rest of line
          ${CMD_VERBOSE_PRINT} "preprocessing for include ..."
          ${CMD_DEBUG_SLEEP}
          local LINE
          cat "${WORK}" | while IFS= read -r LINE ; do

            if echo "${LINE}" | grep -q '^#@include ' ; then
              local SECTION
              SECTION="`echo "${LINE}" | cut -f 2 -d ' '`"
              broken_net_wget_or_die "${URL}.${SECTION}" "${DIR}"
              if [ -f "${WORK}.${SECTION}" ] ; then
                ${CMD_DEBUG_PRINT} "`ls -al "${WORK}.${SECTION}"`"
              fi
              ${CMD_DEBUG_WAIT}
              if [ -f "${WORK}.${SECTION}" ] ; then
                cat "${WORK}.${SECTION}" >> "${WORK}.temp"
              #else
                # NOTE: this output is more annoying than useful, disable it
                #${CMD_NOTE} "no config file section ${URL}.${SECTION} ..." >&2
                #${CMD_LOG_NOTE} "no config file section ${URL}.${SECTION} ..."
              fi
            else
              echo "${LINE}" >> "${WORK}.temp"
            fi

          done
          mv "${WORK}.temp" "${WORK}"
          ;;


        *)
          ${CMD_WARNING} "unknown preprocessor \"${PREPROC}\", " \
              "ignoring it ..." >&2
          ${CMD_LOG_WARNING} "unknown preprocessor \"${PREPROC}\"," \
              "ignoring it ..."
          ;;

      esac

      ${CMD_DEBUG_PRINT} "`ls -al "${WORK}"`"
      ${CMD_DEBUG_WAIT}

    done
  fi

  return 0

}


# --- download config file list

${CMD_INFO_PRINT} "downloading configuration list ${SYS_CONFLIST_NAME} ..."
${CMD_DEBUG_SLEEP}

# file list url on server
CONFLIST_URL="${CONFHOST_URL}/${SYS_CONFLIST_NAME}"

# temp file here on host
CONFLIST="${SYS_WORKDIR}/${SYS_CONFLIST_NAME}"

# get the config file list, it lists what config files are to be processed
fetch_and_preprocess_config_file "${CONFLIST_URL}" "${CONFLIST}"


# file here on host
if [ "`whoami`" != root ] ; then
  # being run by an user for personal files, not as root for system
  #   FreeBSD sh fails to substitute if the ~ is inside the ""
  CONFLIST_PLACE=~/."${SYS_CONFLIST_NAME}"
else
  CONFLIST_PLACE="/etc/${SYS_CONFLIST_NAME}"
fi


# need to prevent diff below from complaining about missing previous file
#   if file nonexistant generate, empty, hopefully this will not break anything
if [ ! -e "${CONFLIST_PLACE}" ] ; then

  ${CMD_INFO_PRINT} "will be installing new list at ${CONFLIST_PLACE}"
  ${CMD_DEBUG_SLEEP}

  # empty file ensures it is different (new one from server is not empty)
  touch "${CONFLIST_PLACE}"

fi


# only if file has changed do we do anything
#   diff is missnamed, successful finding difference regarded as faillure
#     that makes ist an "equal" test, not an "difference" test :-)
#   so ! in here despite wanting to do the if stuff in case of difference
if ! diff "${CONFLIST_PLACE}" "${CONFLIST}" > /dev/null ; then

  ${CMD_INFO_PRINT} "updating old or installing new list" \
      "${SYS_CONFLIST_NAME} as ${CONFLIST_PLACE} ..."
  ${CMD_DEBUG_SLEEP}

  cp -pf "${CONFLIST}" "${CONFLIST_PLACE}"

  ${CMD_DEBUG_PRINT} "`ls -al "${CONFLIST_PLACE}"`"
  ${CMD_DEBUG_WAIT}

fi


# --- process each config file

${CMD_INFO_PRINT} "processing individual config files from the list ..."
${CMD_DEBUG_SLEEP}

# cut get rid of comments and grep empty lines (may be emptied comment lines)
#   and allow user to select/filter subset of lines to be processed
#   IFS= to avoid read doing separator processing (mangles spaces)
#     and -r to prevent \ interpretation (mangles line contents)
cut -f 1 -d '#' "${CONFLIST_PLACE}" | grep -v '^ *$' | \
    grep "${CONF_LINEFILTER}" | while IFS= read -r LINE ; do

  ${CMD_DEBUG_PRINT} "LINE=${LINE}"
  ${CMD_DEBUG_WAIT}

  # extract from LINE format:  file-name:place-on-target[:command-to-trigger]
  if ! echo "${LINE}" | grep -q ':' ; then
    ${CMD_ERROR} "config line \"${LINE}\" has no \":\" to split on" >&2
    ${CMD_LOG_ERROR} "config line \"${LINE}\" has no \":\" to split on"
    exit 1
  fi

  # file-name: work on this file
  #   if this is set to -, then remove an now unused config file
  CONFFILE_NAME="`echo "${LINE}" | cut -f 1 -d ':'`"
  # needs to be set, but may be unset if an : is right at begin of line
  if [ "`echo "${CONFFILE_NAME}" | tr -d ' '`" = "" ] ; then
    ${CMD_ERROR} "config line \"${LINE}\" has no file name before \":\"" >&2
    ${CMD_LOG_ERROR} "config line \"${LINE}\" has no file name before \":\""
    exit 1
  fi

  # place-on-target: put result here
  CONFFILE_PLACE="`echo "${LINE}" | cut -f 2 -d ':'`"
  # needs to be set, but may be unset if mothing between the first 2 :
  if [ "`echo "${CONFFILE_PLACE}" | tr -d ' '`" = "" ] ; then
    ${CMD_ERROR} "config line \"${LINE}\" has no config place" \
        "between first and second \":\"" >&2
    ${CMD_LOG_ERROR} "config line \"${LINE}\" has no config place" \
        "between first and second \":\""
    exit 1
  fi

  # command-to-trigger: run this facultatively
  #   run post installing (or pre removing) an config file
  #   if no second : on line or nothing after it this is automatically blank
  #   if more than 2 : on line include the rest of them into this with 3-
  CMDTRIG="`echo "${LINE}" | cut -f 3- -d ':'`"

  ${CMD_DEBUG_PRINT} "CONFFILE_NAME=${CONFFILE_NAME}" \
      "CONFFILE_PLACE=${CONFFILE_PLACE} CMDTRIG=${CMDTRIG}"
  ${CMD_DEBUG_WAIT}


  ${CMD_VERBOSE_PRINT} "processing config file ${CONFFILE_NAME} ..."
  ${CMD_DEBUG_SLEEP}


  # final file place here on host stuff
  #   ensure that it is an absolute filename
  if [ "`echo "${CONFFILE_PLACE}" | cut -c 1`" != / ] ; then
    ${CMD_WARNING} "correcting non-absolute \"${CONFFILE_PLACE}\", add /" >&2
    ${CMD_LOG_WARNING} "correcting non-absolute \"${CONFFILE_PLACE}\", add /"
  fi
  #   allow debug runs as normal non-root user, and without overwriting stuff
  CONFFILE_PLACE="${DEBUG_INSTALL_BASE}${CONFFILE_PLACE}"
  #   test for only directory name (ends in /), add base file name to it
  if [ "`echo "${CONFFILE_PLACE}" | rev | cut -c 1`" = / ] ; then
    # user gave directory, add filename to it
    # add no / after "${CONFFILE_PLACE}" as it is already contained in it
    #   explicit user request that no // is displayed in resulting names
    CONFFILE_PLACE="${CONFFILE_PLACE}${CONFFILE_NAME}"
  else
    # user gave no directory, but check for forgotten /
    if [ -d "${CONFFILE_PLACE}" ] ; then
      ${CMD_WARNING} "place without / at end, but is existing directory" >&2
      ${CMD_LOG_WARNING} "place without / at end, but is existing directory"
      CONFFILE_PLACE="${CONFFILE_PLACE}/${CONFFILE_NAME}"
    fi
  fi

  # expand an {} in commands to trigger to the full name of the file installed
  #   syntax in copied from the  find -exec ;  command
  if echo "${CMDTRIG}" | grep -q '\{\}' ; then
    # hide slashes in path, to not confuse sed s command
    SED_CONFFILE_PLACE="`echo "${CONFFILE_PLACE}" | sed -e '/\//s@@\\\/@g'`"
    CMDTRIG="`echo "${CMDTRIG}" | sed -e "/{}/s//${SED_CONFFILE_PLACE}/g"`"
  fi


  if [ "${CONFFILE_NAME}" != - ] ; then
    # install an config file

    # file url on server
    CONFFILE_URL="${CONFHOST_URL}/${CONFFILE_NAME}"

    # temp file here on host
    CONFFILE="${SYS_WORKDIR}/${CONFFILE_NAME}"

    # get the actual config file to be processed
    fetch_and_preprocess_config_file "${CONFFILE_URL}" "${CONFFILE}"


    # ensure first that target dir is there, case new config file in new dir
    mkdir -p "`dirname "${CONFFILE_PLACE}"`"
 

    # prevent diff complaining fix, as for config list
    if [ ! -e "${CONFFILE_PLACE}" ] ; then

      ${CMD_INFO_PRINT} "will be installing new file at" \
          "${CONFFILE_PLACE}"
      ${CMD_DEBUG_SLEEP}

      # empty file ensures it is different (new one from server is not empty)
      touch "${CONFFILE_PLACE}"

    fi


    # only if file has changed do we do anything, as for config list
    if ! diff "${CONFFILE_PLACE}" "${CONFFILE}" > /dev/null ; then

      ${CMD_INFO_PRINT} "updating old or installing new file" \
          "${CONFFILE_NAME} as ${CONFFILE_PLACE} ..."
      ${CMD_DEBUG_SLEEP}

      cp -pf "${CONFFILE}" "${CONFFILE_PLACE}"

      ${CMD_DEBUG_PRINT} "`ls -al "${CONFFILE_PLACE}"`"
      ${CMD_DEBUG_WAIT}


      # run the facultative postinstall command trigger stuff
      if [ "`echo "${CMDTRIG}" | tr -d ' '`" != "" ] ; then

        ${CMD_INFO_PRINT} "triggering postinstall command \"${CMDTRIG}\" ..."
        ${CMD_DEBUG_SLEEP}

        # prevent non zero return from user given command from killing us
        set +e
        sh -c "${CMDTRIG}"
        RETVAL="$?"
        set -e
        if [ "${RETVAL}" != 0 ] ; then
          ${CMD_WARNING} "command returned non zero: ${RETVAL}" >&2
          ${CMD_LOG_WARNING} "command returned non zero: ${RETVAL}"
        fi

        ${CMD_DEBUG_PRINT} "`ls -al "${CONFFILE_PLACE}"`"
        ${CMD_DEBUG_WAIT}

      fi

    fi


  else
    # remove an config file

    # only if file still exists do we do anything
    if [ -e "${CONFFILE_PLACE}" ] ; then

      ${CMD_INFO_PRINT} "removing old config file ${CONFFILE_PLACE} ..."
      ${CMD_DEBUG_SLEEP}

      # test for only directory name (ends in /), possibly dangerous leftover
      if [ "`echo "${CONFFILE_PLACE}" | rev | cut -c 1`" = "/" ] ; then

        ${CMD_WARNING} "would remove entire directory ${CONFFILE_PLACE}," \
            "may be partially edited list file entry, DANGEROUS, leaving it," \
            "add the files name after the / to delete an single file," \
            "remove / after dir name to really delete entire directory" >&2
        ${CMD_LOG_WARNING} "would remove entire directory ${CONFFILE_PLACE}," \
            "may be partially edited list file entry, DANGEROUS, leaving it," \
            "add the files name after the / to delete an single file," \
            "remove / after dir name to really delete entire directory"

      else

        # run the facultative preremove command trigger stuff
        if [ "`echo "${CMDTRIG}" | tr -d ' '`" != "" ] ; then

          ${CMD_INFO_PRINT} "triggering preremove command \"${CMDTRIG}\" ..."
          ${CMD_DEBUG_SLEEP}

          set +e
          sh -c "${CMDTRIG}"
          RETVAL="$?"
          set -e
          if [ "${RETVAL}" != 0 ] ; then
            ${CMD_WARNING} "command returned non zero: ${RETVAL}" >&2
            ${CMD_LOG_WARNING} "command returned non zero: ${RETVAL}"
          fi

        fi

        ${CMD_INFO_PRINT} "removing config file ${CONFFILE_PLACE} ..."
        ${CMD_DEBUG_SLEEP}

        ${CMD_DEBUG_PRINT} "`ls -ald "${CONFFILE_PLACE}"`"
        ${CMD_DEBUG_WAIT}

        rm -rf "${CONFFILE_PLACE}"

      fi

    fi

  fi

done


# --- finish off

${CMD_INFO_PRINT} "tidying up from working ..."
${CMD_DEBUG_SLEEP}

${CMD_DEBUG_PRINT} "+++ begin workdirectory listing +++"
if [ "${DEBUG_PRINT_STEP}" = yes ] ; then
  ls -al "${SYS_WORKDIR}" 2> /dev/null
fi
${CMD_DEBUG_PRINT} "+++ end workdirectory listing +++"
${CMD_DEBUG_WAIT}

# remove temporary directory and its contents, to save space
#   must be after debug printout
if [ "${DEBUG_LEAVE_TEMPDIRS}" != yes ] ; then
  # but offer to leave this to investigate bugs
  rm -rf "${SYS_WORKDIR}"
fi

${CMD_LOG_IF_DONE} "has updated/installed/removed config files"
exit 0
