#!/usr/bin/env bash
#
################################################################################
#  LICENSE:                                                                    #
#  This program is licensed under GPLv2.                                       #
#  Please read the accompanying license information in gpl-2.0.txt.            #
################################################################################
#
#  CHANGELOG:
#  2.5.5 (2025-02-11)
#  - bugfix #143: parse duplicity 3.0.4 version fails (thx Mischa ter Smitten)
#  - clean out some compatibility code with ancient duplicity versions
#
#  2.5.4 (2025-01-01)
#  - bugfix #142: batch cmds with consecutive '_' fail, (hx Dominik Sommer
#  - allow undocumented space separated batch cmd list e.g. "pre bkp post"
#  - fix mawk incompatibility in gpg version detection
#  - fix and/or conditions skipped one too many commands
#  - simplify batch command parsing, remove superfluous 'read -ra'
#
#  2.5.3 (10.7.2024)
#  - bugfix #140,141: "GPG_OPTS broken"
#    see also https://duplicity.us/stable/duplicity.1.html#argparse-problem
#  - detect gpg version and add '--pinentry-mode loopback' as duplicity does
#    no need to add it manually in GPG_OPTS anymore
#
#  2.5.2 (30.11.2023)
#  - bugfix #139: "ampersand (&) in gpg passphrase breaks gpg tests"
#
#  2.5.1 (4.10.2023)
#  - quotewrap only strings with quotes ('") or spaces from now on
#  - add --verbosity only if set in profile conf
#  - bugfix #138: fix quoting when filtering params, thx Eric
#  - bugfix #137: relax version parsing regex
#
#  2.5.0 (25.09.2023)
#  - bugfix #136: "not compatible with duplicity 2.x", thx tengel, lds, Rhomeo
#    check for duplicity 2.1+ (2.0 broke implied commands), 
#    command line ui changed incompatibly
#  - filter in/excludes more strictly for more duplicity actions now
#  - replace '--file-to-restore' with '--path-to-restore'
#  - filter backup only params now
#
################################################################################

# utility functions overriding binaries

# wrap grep to override possible env set GREP_OPTIONS=--color=always
function grep {
  command env "GREP_OPTIONS=" grep "$@"
}

# implement basename in plain bash
function basename {
  local stripped="${1%/}"
  echo "${stripped##*/}"
}

# implement dirname in plain bash
function dirname {
  echo ${1%/*}
}

# implement basic which in plain bash
function which {
  type -p "$@"
}

# check availability of executables via file name or file paths
function lookup {
  local bin="$1"
  # look for file names in path via bash hash OR 
  # look for executables at given relative/absolute location
  ( [ "${bin##*/}" == "$bin" ] && hash "$bin" 2>/dev/null ) || [ -x "$bin" ]
}

# important definitions #######################################################

ME_LONG="$0"
ME="$(basename "$0")"
ME_NAME="${ME%%.*}"
ME_VERSION="2.5.5"
ME_WEBSITE="https://duply.net"

# default config values
DEFAULT_SOURCE='/path/of/source'
DEFAULT_TARGET='scheme://user[:password]@host[:port]/[/]path'
DEFAULT_TARGET_USER='_backend_username_'
DEFAULT_TARGET_PASS='_backend_password_'
DEFAULT_GPG='gpg'
DEFAULT_GPG_KEY='_KEY_ID_'
DEFAULT_GPG_PW='_GPG_PASSWORD_'

# function definitions ##########################

function set_config { # sets global config vars
  local CONFHOME_COMPAT="$HOME/.ftplicity"
  local CONFHOME="$HOME/.duply"
  local CONFHOME_ETC_COMPAT="/etc/ftplicity"
  local CONFHOME_ETC="/etc/duply"

  # confdir can be delivered as path (must contain /)
  if [ `echo $FTPLCFG | grep /` ] ; then 
    CONFDIR=$(readlink -f $FTPLCFG 2>/dev/null || \
              ( echo $FTPLCFG|grep -v '^/' 1>/dev/null 2>&1 \
               && echo $(pwd)/${FTPLCFG} ) || \
              echo ${FTPLCFG})          
  # or DEFAULT in home/.duply folder (NEW)
  elif [ -d "${CONFHOME}" ]; then
    CONFDIR="${CONFHOME}/${FTPLCFG}"
  # or in home/.ftplicity folder (OLD)
  elif [ -d "${CONFHOME_COMPAT}" ]; then
    CONFDIR="${CONFHOME_COMPAT}/${FTPLCFG}"
    warning_oldhome "${CONFHOME_COMPAT}" "${CONFHOME}"
  # root can put profiles under /etc/duply (NEW) if path exists
  elif [ -d "${CONFHOME_ETC}" ] && [ "$EUID" -eq 0 ]; then
    CONFDIR="${CONFHOME_ETC}/${FTPLCFG}"
  # root can keep profiles under /etc/ftplicity (OLD) if path exists
  elif [ -d "${CONFHOME_ETC_COMPAT}" ] && [ "$EUID" -eq 0 ]; then
    CONFDIR="${CONFHOME_ETC_COMPAT}/${FTPLCFG}"
    warning_oldhome "${CONFHOME_ETC_COMPAT}" "${CONFHOME_ETC}"
  # hmm no profile folder there, then use default for error later
  else
    CONFDIR="${CONFHOME}/${FTPLCFG}" # continue, will fail later in main
  fi

  # remove trailing slash, get profile name etc.
  CONFDIR="${CONFDIR%/}"
  PROFILE="${CONFDIR##*/}"
  CONF="$CONFDIR/conf"
  PRE="$CONFDIR/pre"
  POST="$CONFDIR/post"
  EXCLUDE="$CONFDIR/exclude"
  KEYFILE="$CONFDIR/gpgkey.asc"
}

function version_info { # print version information
  cat <<END
  $ME_NAME version $ME_VERSION
  ($ME_WEBSITE)
END
}

function version_info_using { 
  cat <<END
$(version_info)

  $(using_info)
END
}

function using_info {
  # init needed vars into global name space
  lookup duplicity && { duplicity_version_get; }
  local NOTFOUND="INVALID"
  local AWK_VERSION GREP_VERSION PYTHON_RUNNER \
    PYTHON_RUNNER_RESOLVED PYTHON_VERSION PYTHON_PATH
  # freebsd awk / GNU awk (--version), 
  # debian mawk (-W version), 
  # openbsd awk (-V, exitcode 0 when any program string is given regardless .e.g. "-W version", so place it last) 
  # some awks wait for input if they misinterpret/don't know the options, pipe '' as a precaution
  AWK_VERSION=$( lookup awk && ( 
  echo | awk --version ||\
  echo | awk -V ||\
  echo | awk -W version ) 2>/dev/null | awk 'NR<=2&&tolower($0)~/(busybox|awk)/{success=1;print;exit} END{if(success<1) print "unknown"}' || echo "$NOTFOUND" )
  GREP_VERSION=$( lookup grep && grep --version 2>&1 | awk 'NR<=2&&tolower($0)~/(busybox|grep.*[0-9]+\.[0-9]+)/{success=1;print;exit} END{if(success<1) print "unknown"}' || echo "$NOTFOUND" )

  if [ -n "$PYTHON" ]; then
    PYTHON_RUNNER=$PYTHON
  else
    PYTHON_RUNNER="$(duplicity_python_binary_parse)"
  fi
  # fetch version and resolve python
  [ -n "$PYTHON_RUNNER" ] && {
    PYTHON_VERSION=$($PYTHON_RUNNER -V 2>&1| awk '{print tolower($0);exit}' || echo "'$PYTHON_RUNNER' $NOTFOUND" )
    local PYTHON_RUNNER_ARRAY=( $PYTHON_RUNNER )
    PYTHON_RUNNER_RESOLVED="$(which ${PYTHON_RUNNER_ARRAY[0]})"
    # readd params if there were
    [ ${#PYTHON_RUNNER_ARRAY[@]} -gt 1 ] && \
      PYTHON_RUNNER_RESOLVED="${PYTHON_RUNNER_RESOLVED} ${PYTHON_RUNNER_ARRAY[@]:1}"
      PYTHON_PATH="$($PYTHON_RUNNER -c "import sys;print(':'.join(sys.path));")"
  }

  local GPG_INFO=$(gpg_avail && gpg --version 2>&1| awk '/^gpg.*[0-9\.]+$/&&length(v)<1{v=$1" "$3}/^Home:/{h=" ("$0")"}END{print v""h}' || echo "gpg $NOTFOUND")
  local BASH_VERSION=$(bash --version | awk 'NR==1{IGNORECASE=1;sub(/GNU bash, version[ ]+/,"",$0);print $0}')
  # print out
  echo -e "Using installed duplicity version ${DUPL_VERSION:-$NOTFOUND}\
${PYTHON_VERSION+, $PYTHON_VERSION ${PYTHON_RUNNER:+($PYTHON_RUNNER_RESOLVED)}${PYTHON_PATH:+ 'PYTHONPATH=$PYTHON_PATH'}}\
${GPG_INFO:+, $GPG_INFO}${AWK_VERSION:+, awk '${AWK_VERSION}'}${GREP_VERSION:+, grep '${GREP_VERSION}'}\
${BASH_VERSION:+, bash '${BASH_VERSION}'}."
}

function usage_info { # print usage information

  cat <<USAGE_EOF
VERSION:
$(version_info)
  
DESCRIPTION: 
  Duply deals as a wrapper for the mighty duplicity magic.
  It simplifies running duplicity with cron or on command line.

  First time usage (profile creation):
    $ME <profile> create

  General usage in single or batch mode (see EXAMPLES):
    $ME <profile> <command>[[_|+|-]<command>[_|+|-]...] [<options> ...]

  For batches the conditional separators can also be written as pseudo commands
  and(+), or(-). See SEPARATORS for details.

  Non $ME options are passed on to duplicity (see OPTIONS).
  All conf parameters can also be defined in the environment instead.

SEPARATORS:
  _ (underscore)  
             neutral separator
  + (plus sign), _and_  
             conditional AND
             the next command will only be executed if the previous succeeded
  - (minus sign), _or_  
             conditional OR
             the next command will only be executed if the previous failed
  [] (square brackets), _groupIn_/_groupOut_  
             enables grouping of commands

   example:  
    'pre+[bkp-verify]_post' translates to
    'pre_and_groupIn_bkp_or_verify_groupOut_post'

COMMANDS:
  usage      get usage help text

  and/or/groupIn/groupOut  
             pseudo commands used in batches (see SEPARATORS above)

  create     creates a configuration profile
  backup     backup with pre/post script execution (batch: [pre_bkp_post]),
              full (if full_if_older matches or no earlier backup is found)
              incremental (in all other cases)
  pre/post   execute '<profile>/$(basename "$PRE")', '<profile>/$(basename "$POST")' scripts
  bkp        as above but without executing pre/post scripts
  full       force full backup
  incr       force incremental backup
  list [<age>]  
             list all files in backup (as it was at <age>, default: now)
  status     prints backup sets and chains currently in repository
  verify [<age>] [--compare-data]  
             list files changed, since age if given
  verifyPath <rel_path_in_bkp> <local_path> [<age>] [--compare-data]  
             list changes of a file or folder path in backup compared to a
             local path, since age if given
  restore <target_path> [<age>]  
             restore the complete backup to <target_path> [as it was at <age>]
  fetch <src_path> <target_path> [<age>]  
             fetch single file/folder from backup [as it was at <age>]
  purge [<max_age>] [--force]  
             list outdated backup files (older than \$MAX_AGE)
              [use --force to actually delete these files]
  purgeFull [<max_full_backups>] [--force]  
             list outdated backup files (\$MAX_FULL_BACKUPS being the number of
             full backups and associated incrementals to keep, counting in 
             reverse chronological order)
              [use --force to actually delete these files]
  purgeIncr [<max_fulls_with_incrs>] [--force]  
             list outdated incremental backups (\$MAX_FULLS_WITH_INCRS being 
             the number of full backups which associated incrementals will be
             kept, counting in reverse chronological order) 
              [use --force to actually delete these files]
  purgeAuto [--force]  
             convenience batch wrapper for all purge commands above.
             purge, purgeFull, purgeIncr are added if their conf vars were set. e.g.
              MAX_AGE=1Y
              MAX_FULL_BACKUPS=6
              MAX_FULLS_WITH_INCR=3
             in profile conf file would result in
              [purge_purgeFull_purgeIncr]
  cleanup [--force]  
             list broken backup chain files archives (e.g. after unfinished run)
              [use --force to actually delete these files]

  changelog  print changelog / todo list
  version    show version information of $ME_NAME and needed programs

OPTIONS:
  --force    passed to duplicity (see commands: 
             purge, purgeFull, purgeIncr, cleanup)
  --preview  do nothing but print out generated duplicity command lines
  --disable-encryption  
             disable encryption, overrides profile settings

TIME FORMATS:
  For all time related parameters like age, max_age etc.
  Refer to the duplicity manpage for all available formats. Here some examples:
    2002-01-25T07:00:00+02:00 (full date time format string)
    2002/3/5 (date string YYYY/MM/DD)
    12D (interval, 12 days ago)
    1h78m (interval, 1 hour 78 minutes ago)

$(hint_profile)

SEE ALSO:
    man pages: duply(1), duplicity(1)
USAGE_EOF
}

# to check call 'duply txt2man | man -l -'
function usage_txt2man {
  usage_info | \
  awk '/^^[^[:lower:][:space:]][^[:lower:]]+$/{gsub(/[^[:upper:]]/," ",$0)}{print}' |\
  txt2man -t"$(toupper "${ME_NAME}")" -s1 -r"${ME_NAME}-${ME_VERSION}" -v'User Manuals'
}

function changelog {
  cat $ME_LONG | awk '/^#####/{on=on+1}(on==3){sub(/^#(  )?/,"",$0);print}'
}

function create_config {
  if [ ! -d "$CONFDIR" ] ; then
    mkdir -p "$CONFDIR" || error "Couldn't create config '$CONFDIR'."
  # create initial config file
    cat <<EOF >"$CONF"
# gpg encryption settings, simple settings:
#  GPG_KEY='disabled' - disables encryption alltogether
#  GPG_KEY='<key1>[,<key2>]'; GPG_PW='pass' - encrypt with keys,
#   sign if secret key of key1 is available use GPG_PW for sign & decrypt
#  Note: you can specify keys via all methods described in gpg manpage,
#        section "How to specify a user ID", escape commas (,) via backslash (\)
#        e.g. 'Mueller, Horst', 'Bernd' -> 'Mueller\, Horst, Bernd'
#        as they are used to separate the entries
#  GPG_PW='passphrase' - symmetric encryption using passphrase only
GPG_KEY='${DEFAULT_GPG_KEY}'
GPG_PW='${DEFAULT_GPG_PW}'
# gpg encryption settings in detail (extended settings)
#  the above settings translate to the following more specific settings
#  GPG_KEYS_ENC='<keyid1>[,<keyid2>,...]' - list of pubkeys to encrypt to
#  GPG_KEY_SIGN='<keyid1>|disabled' - a secret key for signing
#  GPG_PW='<passphrase>' - needed for signing, decryption and symmetric
#   encryption. If you want to deliver different passphrases for e.g. 
#   several keys or symmetric encryption plus key signing you can use
#   gpg-agent. Simply make sure that GPG_AGENT_INFO is set in environment.
#   also see "A NOTE ON SYMMETRIC ENCRYPTION AND SIGNING" in duplicity manpage 
# notes on en/decryption
#  private key and passphrase will only be needed for decryption or signing.
#  decryption happens on restore and incrementals (compare archdir contents).
#  for security reasons it makes sense to separate the signing key from the
#  encryption keys. https://answers.launchpad.net/duplicity/+question/107216
#GPG_KEYS_ENC='<pubkey1>,<pubkey2>,...'
#GPG_KEY_SIGN='<prvkey>'
# set if signing key passphrase differs from encryption (key) passphrase
# NOTE: available since duplicity 0.6.14, translates to SIGN_PASSPHRASE
#GPG_PW_SIGN='<signpass>'

# uncomment and set a file path or name force duply to use this gpg executable
# available in duplicity 0.7.04 and above (currently unreleased 06/2015)
#GPG='/usr/local/gpg-2.1/bin/gpg'

# gpg options passed from duplicity to gpg process (default='')
# e.g. "--trust-model pgp|classic|direct|always" 
#   or "--compress-algo=bzip2 --bzip2-compress-level=9"
#   or "--personal-cipher-preferences AES256,AES192,AES..."
#   or "--homedir ~/.duply" - keep keyring and gpg settings duply specific
#GPG_OPTS=''

# disable preliminary tests with the following setting
#GPG_TEST='disabled'
# disable automatic gpg key importing altogether
#GPG_IMPORT='disabled'
# disable automatic gpg key exporting to profile folder
#GPG_EXPORT='disabled'

# backend, credentials & location of the backup target (URL-Format)
# generic syntax is
#   scheme://[user[:password]@]host[:port]/[/]path
# e.g.
#   sftp://bob:secret@backupserver.com//home/bob/dupbkp
# for details and available backends see duplicity manpage, section URL Format
#   http://duplicity.us/vers8/duplicity.1.html#url-format
# BE AWARE:
#   some backends (cloudfiles, S3 etc.) need additional env vars to be set to
#   work properly, read after the TARGET definition for more details.
# ATTENTION:
#   characters other than A-Za-z0-9.-_.~ in the URL have to be
#   replaced by their url encoded pendants, see
#     http://en.wikipedia.org/wiki/Url_encoding
#   if you define the credentials as TARGET_USER, TARGET_PASS below $ME
#   will try to url_encode them for you if the need arises.
TARGET='${DEFAULT_TARGET}'

# optionally the username/password can be defined as extra variables
# setting them here _and_ in TARGET results in an error
# ATTENTION:
#   there are backends that do not support the user/pass auth scheme.
#   prominent examples are S3, Azure, Cloudfiles. when in doubt consult the
#   duplicity manpage. usually there is a NOTE section explaining if and which
#   env vars should be set.
#TARGET_USER='${DEFAULT_TARGET_USER}'
#TARGET_PASS='${DEFAULT_TARGET_PASS}'
# e.g. for cloud files backend it might look like this (uncomment for use!)
#export CLOUDFILES_USERNAME='someuser'
#export CLOUDFILES_APIKEY='somekey'
#export CLOUDFILES_AUTHURL ='someurl'
# the following is an incomplete list (<backend>: comma separated env vars list)
# Azure: AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY
# Cloudfiles: CLOUDFILES_USERNAME, CLOUDFILES_APIKEY, CLOUDFILES_AUTHURL
# Google Cloud Storage: GS_ACCESS_KEY_ID, GS_SECRET_ACCESS_KEY
# Pydrive: GOOGLE_DRIVE_ACCOUNT_KEY, GOOGLE_DRIVE_SETTINGS
# S3: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
# Swift: SWIFT_USERNAME, SWIFT_PASSWORD, SWIFT_AUTHURL,
#        SWIFT_TENANTNAME OR SWIFT_PREAUTHURL, SWIFT_PREAUTHTOKEN

# base directory to backup
SOURCE='${DEFAULT_SOURCE}'

# a command that runs duplicity e.g. 
#  shape bandwidth use via trickle
#  "trickle -s -u 640 -d 5120" # 5Mb up, 40Mb down"
#DUPL_PRECMD=""

# override the python interpreter to execute duplicity, unset by default
#  e.g. "python3" or "/usr/bin/python3.8"
#PYTHON="python"

# exclude folders containing exclusion file (since duplicity 0.5.14)
# Uncomment the following two lines to enable this setting.
#FILENAME='.duplicity-ignore'
#DUPL_PARAMS="\$DUPL_PARAMS --exclude-if-present '\$FILENAME'"

# Time frame for old backups to keep, Used for the "purge" command.  
# see duplicity man page, chapter TIME_FORMATS)
#MAX_AGE=1M

# Number of full backups to keep. Used for the "purgeFull" command. 
# See duplicity man page, action "remove-all-but-n-full".
#MAX_FULL_BACKUPS=1

# Number of full backups for which incrementals will be kept for.
# Used for the "purgeIncr" command.
# See duplicity man page, action "remove-all-inc-of-but-n-full".
#MAX_FULLS_WITH_INCRS=1

# activates duplicity --full-if-older-than option (since duplicity v0.4.4.RC3) 
# forces a full backup if last full backup reaches a specified age, for the 
# format of MAX_FULLBKP_AGE see duplicity man page, chapter TIME_FORMATS
# Uncomment the following two lines to enable this setting.
#MAX_FULLBKP_AGE=1M
#DUPL_PARAMS="\$DUPL_PARAMS --full-if-older-than \$MAX_FULLBKP_AGE " 

# sets duplicity --volsize option (available since v0.4.3.RC7)
# set the size of backup chunks to VOLSIZE MB instead of the default 25MB.
# VOLSIZE must be number of MB's to set the volume size to.
# Uncomment the following two lines to enable this setting. 
#VOLSIZE=50
#DUPL_PARAMS="\$DUPL_PARAMS --volsize \$VOLSIZE "

# verbosity of output (error 0, warning 1-2, notice 3-4, info 5-8, debug 9)
# default is 4, if not set
#VERBOSITY=5

# temporary file space. at least the size of the biggest file in backup
# for a successful restoration process. (default is '/tmp', if not set)
#TEMP_DIR=/tmp

# Modifies archive-dir option (since 0.6.0) Defines a folder that holds 
# unencrypted meta data of the backup, enabling new incrementals without the 
# need to decrypt backend metadata first. If empty or deleted somehow, the 
# private key and it's password are needed.
# NOTE: This is confidential data. Put it somewhere safe. It can grow quite 
#       big over time so you might want to put it not in the home dir.
# default '~/.cache/duplicity/duply_<profile>/'
# if set  '\${ARCH_DIR}/<profile>'
#ARCH_DIR=/some/space/safe/.duply-cache

# DEPRECATED setting
# sets duplicity --time-separator option (since v0.4.4.RC2) to allow users 
# to change the time separator from ':' to another character that will work 
# on their system.  HINT: For Windows SMB shares, use --time-separator='_'.
# NOTE: '-' is not valid as it conflicts with date separator.
# ATTENTION: only use this with duplicity < 0.5.10, since then default file 
#            naming is compatible and this option is pending depreciation 
#DUPL_PARAMS="\$DUPL_PARAMS --time-separator _ "

# DEPRECATED setting
# activates duplicity --short-filenames option, when uploading to a file
# system that can't have filenames longer than 30 characters (e.g. Mac OS 8)
# or have problems with ':' as part of the filename (e.g. Microsoft Windows)
# ATTENTION: only use this with duplicity < 0.5.10, later versions default file 
#            naming is compatible and this option is pending depreciation
#DUPL_PARAMS="\$DUPL_PARAMS --short-filenames "

# more duplicity command line options can be added in the following way
# don't forget to leave a separating space char at the end
#DUPL_PARAMS="\$DUPL_PARAMS --put_your_options_here " 

EOF

# create initial exclude file
    cat <<EOF >"$EXCLUDE"
# although called exclude, this file is actually a globbing file list
# duplicity accepts some globbing patterns, even including ones here
# here is an example, this incl. only 'dir/bar' except it's subfolder 'foo'
# - dir/bar/foo
# + dir/bar
# - **
# for more details see duplicity manpage, section File Selection
# http://duplicity.us/vers8/duplicity.1.html#file-selection

EOF

  # Hints on first usage
  cat <<EOF

Congratulations. You just created the profile '$FTPLCFG'.
The initial config file has been created as 
'$CONF'.
You should now adjust this config file to your needs.

$(hint_profile)

EOF
fi

}

# used in usage AND create_config
function hint_profile {
  cat <<EOF
IMPORTANT:
  Copy the _whole_ profile folder after the first backup to a safe place.
  It contains everything (duply related) needed to restore your backups. 

  Pay attention to (possibly later added) external files such as credentials
  or auth files (e.g. netrc, .megarc, ssh keys) or environment variables
  (e.g. DPBX_ACCESS_TOKEN). 
  It is good policy to place those in the profile folder if possible at all.
    e.g. in case of 'multi://' target the config .json file
  Env vars should be added to duply profiles' conf file.

  Keep access to these files restricted as they contain information (gpg key,
  passphrases etc.) to access and modify your backups.
  
  Finally:
  You should attempt a restore from an unrelated host to be sure you really
  have everything needed for restoration.

  Repeat these steps after _all_ configuration changes. Some configuration 
  options are crucial for restoration.

EOF
}

function separator {
  echo "--- $@ ---"
}

function inform {
  echo -e "\nINFO:\n\n$@\n"
}

function warning {
  echo -e "\nWARNING:\n\n$@\n"
}

function warning_oldhome {
  local old=$1 new=$2
  warning " ftplicity changed name to duply since you created your profiles.
  Please rename the old folder
  '$old'
  to
  '$new'
  and this warning will disappear.
  If you decide not to do so profiles will _only_ work from the old location."
}

function error_print {
  echo -e "$@" >&2
}

function error {
  error_print "\nSorry. A fatal ERROR occured:\n\n$@\n"
  exit -1
}

function error_gpg {
  [ -n "$2" ] && local hint="\n  $2\n\n  "
  
  error "$1

Hint${hint:+s}:
  ${hint}Maybe you have not created a gpg key yet (e.g. gpg --gen-key)?
  Don't forget the used _password_ as you will need it.
  When done enter the 8 digit id & the password in the profile conf file.

  The key id can be found doing a 'gpg --list-keys'. In the example output 
  below the key id for the public key would be FFFFFFFF.

  pub   1024D/FFFFFFFF 2007-12-17
  uid                  duplicity
  sub   2048g/899FE27F 2007-12-17
"
}

function error_gpg_test {
  [ -n "$2" ] && local hint="\n  $2\n\n  "

  error "$1

Hint${hint:+s}:
  ${hint}This error means that gpg is probably misconfigured or not working 
  correctly. The error message above should help to solve the problem.
  However, if for some reason $ME_NAME should misinterpret the situation you 
  can define GPG_TEST='disabled' in the conf file to bypass the test.
  Please do not forget to report the bug in order to resolve the problem
  in future versions of $ME_NAME.
"
}

function error_path {
  error "$@
PATH='$PATH'
"
}

function error_to_string {
  [ -n "$1" ] && [ "$1" -eq 0 ] && echo "OK" || echo "FAILED 'code $1'"
}

function duplicity_version_get {
  # use cached value, just print
  var_isset DUPL_VERSION && return

  local DUPL_VERSION_OUT DUPL_VERSION_AWK PYTHON_BIN CMD='duplicity'
  # only run with a user specific python if configured (running by default
  # breaks homebrew as they place a shell wrapper for duplicity in path)
  [ -n "$PYTHON" ] &&\
    CMD="$PYTHON $(qw "$(which duplicity)")"

  DUPL_VERSION_OUT=$($CMD --version)
  # keep "$DUPL_VERSION_OUT" in quotes to maintain line breaks of output
  DUPL_VERSION=$(echo "$DUPL_VERSION_OUT" | awk '/^duplicity +/{print $2; exit;}')
  #DUPL_VERSION='1.2.3' #'0.7.03' #'0.6.08b' #,0.4.4.RC4,0.6.08b
  DUPL_VERSION_VALUE=0
  DUPL_VERSION_AWK=$(awk -v v="$DUPL_VERSION" 'BEGIN{
  if (match(v,/[^\.0-9]+[0-9]*$/)){
    rest=substr(v,RSTART,RLENGTH);v=substr(v,0,RSTART-1);}
  if (pos=match(rest,/RC([0-9]+)$/)) rc=substr(rest,pos+2)
  split(v,f,"[. ]"); if(f[1]f[2]f[3]~/^[0-9]+$/) vvalue=f[1]*10000+f[2]*100+f[3]; else vvalue=0
  print "#"v"_"rest"("rc"):"f[1]"-"f[2]"-"f[3]
  print "DUPL_VERSION_VALUE=\047"vvalue"\047"
  print "DUPL_VERSION_RC=\047"rc"\047"
  print "DUPL_VERSION_SUFFIX=\047"rest"\047"
  }')
  eval "$DUPL_VERSION_AWK"
  #echo -e ",$DUPL_VERSION,$DUPL_VERSION_VALUE,$DUPL_VERSION_RC,$DUPL_VERSION_SUFFIX,"

  # doublecheck findings and report error
  if [ $DUPL_VERSION_VALUE -eq 0 ]; then
    inform "duplicity version check failed (please report, this is a bug)
the command
  $CMD --version
resulted in
  $DUPL_VERSION_OUT
"
  elif [ $DUPL_VERSION_VALUE -le 404 ] && [ ${DUPL_VERSION_RC:-4} -lt 4 ]; then
    error "The installed version $DUPL_VERSION is incompatible with $ME_NAME v$ME_VERSION.
You should upgrade your version of $ME_NAME to at least v0.4.4RC4 or
use the older ftplicity version 1.1.1 from $ME_WEBSITE."
  elif [ $DUPL_VERSION_VALUE -le 20100 ] ; then
    error "The installed version $DUPL_VERSION is incompatible with $ME_NAME v$ME_VERSION.
You should upgrade your version of duplicity to at least v2.1.0 or
use the older $ME_NAME version 2.4.3 from $ME_WEBSITE."
  fi
}

function duplicity_version_isvalid {
  awk '!/^[0-9]+$/{exit 1}' <<< "$DUPL_VERSION_VALUE" && [ "$DUPL_VERSION_VALUE" -gt 0 ]
}

function duplicity_version_ge {
  duplicity_version_isvalid && [ "$DUPL_VERSION_VALUE" -ge "$1" ]
}

function duplicity_version_lt {
  duplicity_version_isvalid && ! duplicity_version_ge "$1"
}

# parse interpreter from duplicity shebang
function duplicity_python_binary_parse {
  # reuse cached result
  var_isset 'DUPL_PYTHON_BIN' && {
    [ -n "$DUPL_PYTHON_BIN" ] && {
      echo $DUPL_PYTHON_BIN
      return
    } || return 1
  }

  # no duplicity, nothing to parse
  lookup duplicity || return 1

  local DUPL_BIN=$(which duplicity)
  # test for shebang
  IFS= LC_ALL=C read -rn2 shebang < "$DUPL_BIN" && [ "$shebang" != '#!' ] && {
    DUPL_PYTHON_BIN=""
    return 1
  }

  # parse it or warn
  DUPL_PYTHON_BIN=$(awk 'NR==1&&/^#!/{sub(/^#!( *\/usr\/bin\/env *)?/,""); print}' < "$DUPL_BIN")
  if ! echo "$DUPL_PYTHON_BIN" | grep -q -i 'python'; then
    warning "Could not parse the python interpreter used from duplicity ($DUPL_BIN). Result was 
'$DUPL_PYTHON_BIN'.
"
    DUPL_PYTHON_BIN=""
    return 1
  fi
  
  # success
  echo $DUPL_PYTHON_BIN
  return
}

function run_script { # run pre/post scripts
  local ERR=0
  local SCRIPT="$1"
  if [ ! -z "$PREVIEW" ] ; then
    echo "$([ ! -x "$SCRIPT" ] && echo ". ")$SCRIPT"
  elif [ -r "$SCRIPT" ] ; then 
    echo -n "Running '$SCRIPT' "
    if [ -x "$SCRIPT" ]; then
      OUT=$("$SCRIPT" 2>&1)
      ERR=$?
    else
      OUT=$(. "$SCRIPT" 2>&1)
      ERR=$?
    fi
    [ $ERR -eq "0" ] && echo "- OK" || echo "- FAILED (code $ERR)"
    echo -en ${OUT:+"Output: $OUT\n"} ;
  else
    echo "Skipping n/a script '$SCRIPT'."
  fi
  return $ERR
}

function run_cmd {
  # run or print escaped cmd string
  local CMD_ERR=0
  if [ -n "$PREVIEW" ]; then
    CMD_OUT=$( echo "$@ 2>&1" )
    CMD_MSG="-- Run cmd -- $CMD_MSG --\n$CMD_OUT"
  elif [ -n "$CMD_DISABLED" ]; then
    CMD_MSG="$CMD_MSG (DISABLED) - $CMD_DISABLED"
  else
    echo -n -e "$CMD_MSG"
    CMD_OUT=` eval "$@" 2>&1 `
    CMD_ERR=$?
    if [ "$CMD_ERR" = "0" ]; then
      CMD_MSG=" (OK)"
    else
      CMD_MSG=" (FAILED)"
    fi
  fi
  echo -e "$CMD_MSG"
  # reset
  unset CMD_DISABLED CMD_MSG
  return $CMD_ERR
}

function qw { quotewrap $@; }

function quotewrap {
  local param="$@"
  # quote strings having non word chars (e.g. spaces)
  if echo "$param" | awk '/[^a-zA-Z0-9,\._\+\-@%\/]/{exit 0}{exit 1}'; then
    echo "$param" | awk '{\
      gsub(/[\047]/,"\047\\\047\047",$0);\
      gsub(/[\042]/,"\047\\\042\047",$0);\
      print "\047"$0"\047"}'
    return
  fi
  echo $param
}

function duplicity_params_global {
  # already done? return
  var_isset 'DUPL_PARAMS_GLOBAL' && return
  local DUPL_ARG_ENC

  # use key only if set in config, else leave it to symmetric encryption
  if gpg_disabled; then
    local DUPL_PARAM_ENC='--no-encryption'
  else
    local DUPL_PARAM_ENC=$(gpg_prefix_keyset '--encrypt-key' "${GPG_KEYS_ENC_ARRAY[@]}")
    gpg_signing && local DUPL_PARAM_SIGN=$(gpg_prefix_keyset '--sign-key' "$GPG_KEY_SIGN")
    # interpret password settings
    var_isset 'GPG_PW' && DUPL_ARG_ENC="PASSPHRASE=$(qw "${GPG_PW}")"
    var_isset 'GPG_PW_SIGN' && DUPL_ARG_ENC="${DUPL_ARG_ENC} SIGN_PASSPHRASE=$(qw "${GPG_PW_SIGN}")"
  fi

  # workaround python argparse issue https://duplicity.us/stable/duplicity.1.html#argparse-problem
  local GPG_OPTS=${GPG_OPTS:+"--gpg-options=$(qw "${GPG_OPTS}")"}

  # set name for dupl archive folder, since 0.6.0
  local DUPL_ARCHDIR=''
  if var_isset 'ARCH_DIR'; then
    DUPL_ARCHDIR="--archive-dir $(qw "${ARCH_DIR}")"
    # reuse erronously duply_ prefixed folders from bug #117
    if [ -d "$ARCH_DIR/duply_${PROFILE}" ]; then
      DUPL_ARCHDIR="${DUPL_ARCHDIR} --name $(qw "duply_${PROFILE}")"
    else
      DUPL_ARCHDIR="${DUPL_ARCHDIR} --name $(qw "${PROFILE}")"
    fi
  else
    DUPL_ARCHDIR="--name $(qw "duply_${PROFILE}")"
  fi

  DUPL_PARAMS_GLOBAL="${DUPL_ARCHDIR} ${DUPL_PARAM_ENC}\
 ${DUPL_PARAM_SIGN} ${VERBOSITY:+--verbosity $VERBOSITY}\
 ${GPG_OPTS}"

  DUPL_VARS_GLOBAL="TMPDIR='$TEMP_DIR'\
 ${DUPL_ARG_ENC}"
}

# function to filter the DUPL_PARAMS var from user conf
function duplicity_params_conf {
  local OUT="$DUPL_PARAMS"
  # reuse cmd var from main loop
  ## in/exclude parameters are currently not supported on 
  ## cleanup, status (collection_status), list (list_current_files), purge* (remove_*), fetch/restore
  case $cmd in
    cleanup | status | list | purge* | restore | fetch )
      # filter exclude params from fetch/restore/status
      OUT=$(eval stripXcludes $OUT)
    ;;
  esac

  case $cmd in
    bkp | incr | full )
      # nothing to strip, we're backing up'
    ;;
    *)
      OUT=$(eval stripBkpOnlyParams $OUT)
    ;;
  esac

  # print result
  echo "$OUT"
}

# strip in/exclude parameters from param string
function stripXcludes {
  local STRIPNEXT OUT;
  for p in "$@"; do
    #echo "calc =$p="
    if [ -n "$STRIPNEXT" ]; then
      unset STRIPNEXT
      # strip the value of previous parameter
      continue
    elif echo "$p" | awk '/^\-\-(in|ex)clude(\-[a-zA-Z\-]+)?$/{exit 0;}{exit 1;}'; then
      # strips e.g. --include /foo/bar
      STRIPNEXT="yes"
      continue
    elif echo "$p" | awk '/^\-\-(in|ex)clude(\-[a-zA-Z\-]+)?=/{exit 0;}{exit 1;}'; then
      # strips e.g. --include=/foo/bar
      continue
    fi

    OUT="$OUT $(qw "$p")"
  done
  echo "$OUT"
}

# strip backup only parameters from param string
function stripBkpOnlyParams {
  local STRIPNEXT OUT;

  for p in "$@"; do
    if [ -n "$STRIPNEXT" ]; then
      unset STRIPNEXT
      # strip the value of previous parameter
      continue
    elif echo "$p" | awk '/^\-\-(allow-source-mismatch|asynchronous-upload|dry-run)$/{exit 0;}{exit 1;}'; then
      continue
    elif echo "$p" | awk '/^\-\-(volsize)$/{exit 0;}{exit 1;}'; then
      # strips e.g. --volsize 100
      STRIPNEXT="yes"
      continue
    elif echo "$p" | awk '/^\-\-volsize=/{exit 0;}{exit 1;}'; then
      # strips e.g. --volsize=100
      continue
    fi

    OUT="$OUT $(qw "$p")"
  done
  echo "$OUT"
}

function duplify { # the actual wrapper function
  local PARAMSNOW DUPL_CMD DUPL_CMD_PARAMS

  # put command (with params) first in duplicity parameters
  for param in "$@" ; do
  # split cmd from params (everything before splitchar --)
    if [ "$param" == "--" ] ; then
      PARAMSNOW=1
    else
      # wrap in quotes to protect from spaces
      [ ! $PARAMSNOW ] && \
        DUPL_CMD="$DUPL_CMD $(qw "$param")" \
      || \
        DUPL_CMD_PARAMS="$DUPL_CMD_PARAMS $(qw "$param")"
    fi
  done

  # init global duplicity parameters same for all tasks
  duplicity_params_global

  local RUN=eval CMD=duplicity
  # run in cmd line preview mode if requested
  var_isset 'PREVIEW' && RUN=echo
  # only run with a user specific python if configured (running by default
  # breaks homebrew as they place a shell wrapper for duplicity in path)
  # resolve duplicity path for usage with python interpreter
  [ -n "$PYTHON" ] &&\
    CMD="$PYTHON $(qw "$(which duplicity)")"

$RUN "${DUPL_VARS_GLOBAL} ${BACKEND_PARAMS}\
 ${DUPL_PRECMD} $CMD $DUPL_CMD $DUPL_PARAMS_GLOBAL $(duplicity_params_conf)\
 $GPG_USEAGENT $(gpg_custom_binary) $DUPL_CMD_PARAMS"

  local ERR=$?
  return $ERR
}

function secureconf { # secure the configuration dir
  #PERMS=$(ls -la $(dirname $CONFDIR) | grep -e " $(basename $CONFDIR)\$" | awk '{print $1}')
  local PERMS="$(ls -la "$CONFDIR/." | awk 'NR==2{print $1}')"
  if [ "${PERMS/#drwx------*/OK}" != 'OK' ] ; then
    chmod u+rwX,go= "$CONFDIR"; local ERR=$?
    warning "The profile's folder 
'$CONFDIR'
permissions are not safe ($PERMS). Secure them now. - ($(error_to_string $ERR))"
  fi
}

# params are $1=timeformatstring (default like date output), $2=epoch seconds since 1.1.1970 (default now)
function date_fix {
  local DEFAULTFORMAT='%a %b %d %H:%M:%S %Z %Y'
  local date
  #[ "$1" == "%N" ] && return #test the no nsec test below
  # gnu date with -d @epoch
  date=$(date ${2:+-d @$2} ${1:++"$1"} 2> /dev/null) && \
    echo $date && return
  # date bsd,osx with -r epoch
  date=$(date ${2:+-r $2} ${1:++"$1"} 2> /dev/null) && \
    echo $date && return
  # date busybox with -d epoch -D %s
  date=$(date ${2:+-d $2 -D %s} ${1:++"$1"} 2> /dev/null) && \
    echo $date && return
  ## some date commands do not support giving a time w/o setting it systemwide (irix,solaris,others?)
  # python fallback
  #date=$("$(python_binary)" -c "import time;print time.strftime('${1:-$DEFAULTFORMAT}',time.localtime(${2}))" 2> /dev/null) && \
  #  echo $date && return
  # awk fallback
  date=$(awk "BEGIN{print strftime(\"${1:-$DEFAULTFORMAT}\"${2:+,$2})}" 2> /dev/null) && \
    echo $date && return
  # perl fallback
  date=$(perl  -e "use POSIX qw(strftime);\$date = strftime(\"${1:-$DEFAULTFORMAT}\",localtime(${2}));print \"\$date\n\";" 2> /dev/null) && \
    echo $date && return
  # error
  echo "ERROR"
  return 1
}

function nsecs {
  local NSECS
  # test if date supports nanosecond output
  if ! var_isset NSECS_DISABLED; then
    NSECS=$(date_fix %N 2> /dev/null | head -1 |grep -e "^[[:digit:]]\{9\}$")
    [ -n "$NSECS" ] && NSECS_DISABLED=0 || NSECS_DISABLED=1
  fi

  # add 9 digits, not all date(s) deliver nsecs e.g. busybox date
  if [ "$NSECS_DISABLED" == "1" ]; then
    date_fix %s000000000
  else
    date_fix %s%N
  fi
}

function nsecs_to_sec {
  echo $(($1/1000000000)).$(printf "%03d" $(($1/1000000%1000)) )
}

function datefull_from_nsecs {
  date_from_nsecs $1 '%F %T'
}

function date_from_nsecs {
  local FORMAT=${2:-%T}
  local TIME=$(nsecs_to_sec $1)
  local SECS=${TIME%.*}
  local DATE=$(date_fix "${FORMAT}" ${SECS:-0})
  echo $DATE.${TIME#*.}
}

function var_isset {
  if [ -z "$1" ]; then
    echo "ERROR: function var_isset needs a string as parameter"
  elif eval "[ \"\${$1}\" == 'not_set' ]" || eval "[ \"\${$1-not_set}\" != 'not_set' ]"; then
    return 0
  fi
  return 1
}

function is_condition {
  local CMD=$(tolower "$1")
  [ "$CMD" == 'and' ] || [ "$CMD" == 'or' ]
}

function is_groupMarker {
  local CMD=$(tolower "$1")
  [ "$CMD" == 'groupin' ] || [ "$CMD" == 'groupout' ]
}

function is_command {
  local CMD=$(tolower "$1")
  ! is_condition "$CMD" && ! is_groupMarker "$CMD"
}

function url_encode {
  # utilize python, silently do nothing on error - because no python no duplicity
  local PYTHON_RUNNER
  PYTHON_RUNNER="$(duplicity_python_binary_parse)" ||\
  PYTHON_RUNNER="python" &&\
    OUT=$($PYTHON_RUNNER -c "
try: import urllib.request as urllib
except ImportError: import urllib
print(urllib.${2}quote('$1'));
" 2>/dev/null ); ERR=$?
  [ "$ERR" -eq 0 ] && echo $OUT || echo $1
}

function url_decode {
  # reuse function above with a simple string param hack
  url_encode "$1" "un"
}

function toupper {
  echo "$@"|awk '$0=toupper($0)'
}

function tolower {
  echo "$@"|awk '$0=tolower($0)'
}

function isnumber {
  case "$*" in
    ''|*[!0-9]*) return 1;;
    *) return 0;;
  esac
}

#function tmp_space {
#  
#  if ! isnumber $VOLSIZE; then
#    inform "failed to determine free space (please report, this is a bug)"
#    return
#  fi
#  
# get free temp space
#  TEMP_FREE="$(df -P -k "$TEMP_DIR" 2>/dev/null | awk 'END{pos=(NF-2);if(pos>0) print $pos;}')"
#  # check for free space or FAIL
#  if [ $((${TEMP_FREE:-0}-${VOLSIZE:-0}*1024)) -lt 0-lt 0 ]; then
#    error "Temporary file space '$TEMP_DIR' free space is smaller ($((TEMP_FREE/1024))MB)
#than one duplicity volume (${VOLSIZE}MB).
#    
#  Hint: Free space or change TEMP_DIR setting."
#fi
#
#}

function gpg_disabled {
  echo "${GPG_KEY}" | grep -iq -e '^disabled$'
}

# usage: join SEPARATOR "entry1" "entry2"
function join {
  local SEP="$1" ENTRY OUT; shift;
  for ENTRY in "$@"; do
    ENTRY=${ENTRY//$SEP/\\$SEP}
    [ -z "$OUT" ] && OUT=$ENTRY || OUT="$OUT$SEP$ENTRY"
  done
  echo $OUT
}

function gpg_testing {
  [ "$GPG_TEST" != "disabled" ]
}

function gpg_signing {
  echo ${GPG_KEY_SIGN} | grep -v -q -e '^disabled$'
}

function gpg_keytype {
  echo "$1" | awk '/^PUB$/{print "public"}/^SEC$/{print "secret"}'
}

# parameter key id, key_type
function gpg_keyfile {
  local GPG_KEY=$(gpg_key_legalize $1) TYPE="$2"
  local KEYFILE="${KEYFILE//.asc/${GPG_KEY:+.$GPG_KEY}.asc}"
  echo "${KEYFILE//.asc/${TYPE:+.$(tolower $TYPE)}.asc}"
}

# parameter key id
function gpg_import {
  local i FILE FOUND=0 KEY_ID="$1" KEY_TYPE="$2" KEY_FP="" ERR=0
  [ "$GPG_IMPORT" = "disabled" ] && {
    echo "Skipping import of needed $(gpg_keytype "$KEY_TYPE") key '$KEY_ID'. (GPG_IMPORT='disabled')"
    return
  }

  # create a list of legacy key file names and current naming scheme
  # we always import pub and sec if they are avail in conf folder
  local KEYFILES=( "$CONFDIR/gpgkey" $(gpg_keyfile "$KEY_ID") \
                   $(gpg_keyfile "$KEY_ID" "$KEY_TYPE") )

  # Try autoimport from existing old gpgkey files 
  # and new gpgkey.XXX.asc files (since v1.4.2)
  # and even newer gpgkey.XXX.[pub|sec].asc
  for (( i = 0 ; i < ${#KEYFILES[@]} ; i++ )); do
    FILE=${KEYFILES[$i]}
    if [ -f "$FILE" ]; then
      FOUND=1
      
      CMD_MSG="Import keyfile '$FILE' to keyring"
      run_cmd gpg $GPG_OPTS --batch --import $(qw "$FILE")
      if [ "$?" != "0" ]; then 
        warning "Import failed.${CMD_OUT:+\n$CMD_OUT}"
        ERR=1
        # continue with next
        continue
      fi
    fi
  done

  if [ "$FOUND" -eq 0 ]; then
    echo "Notice: No keyfile for '$KEY_ID' found in profile folder."
    return 1
  fi

  # try to set trust automagically
  CMD_MSG="Autoset trust of key '$KEY_ID' to ultimate"
  run_cmd echo $(gpg_fingerprint "$KEY_ID"):6: \| gpg $GPG_OPTS --import-ownertrust --batch --logger-fd 1
  if [ "$?" = "0" ] && [ -z "$PREVIEW" ]; then 
   # success on all levels, we're done
   return $ERR
  fi

  # failover: user has to set trust manually
  echo -e "For $ME_NAME to work you have to set the trust level 
with the command \"trust\" to \"ultimate\" (5) now.
Exit the edit mode of gpg with \"quit\"."
  CMD_MSG="Running gpg to manually edit key '$KEY_ID'"
  run_cmd sleep 5\; gpg $GPG_OPTS --edit-key $(qw "$KEY_ID")

  return $ERR
}

# see 'How to specify a user ID' on gpg manpage
function gpg_fingerprint {
  gpg $GPG_OPTS --fingerprint "$1" 2>&1 | \
  awk 'NR==2{sub(/^.*=/,"");gsub(/[ \t]/,""); if ( $0 !~ /^[A-F0-9]+$/ || length($0) != 40 ) exit 1; print}'
}

function gpg_export_if_needed {
  [ "$GPG_EXPORT" = 'disabled' ] && { \
    echo "Skipping export of gpg keys. (GPG_EXPORT='disabled')"
    return
  }

  local SUCCESS FILE KEY_TYPE
  local TMPFILE="$TEMP_DIR/${ME_NAME}.$$.$(date_fix %s).gpgexp"
  for KEY_ID in "$@"; do
    # check if already exported, do it if not
    for KEY_TYPE in PUB SEC; do
      FILE="$(gpg_keyfile "$KEY_ID" $KEY_TYPE)"
      if [ ! -f "$FILE" ] && eval gpg_$(tolower $KEY_TYPE)_avail \"$KEY_ID\"; then

        # exporting
        CMD_MSG="Backup $(gpg_keytype "$KEY_TYPE") key '$KEY_ID' to profile."
        # gpg2.1 insists on passphrase here, gpg2.0- happily exports w/o it
        # we pipe an empty string when GPG_PW is not set to avoid gpg silently waiting for input
        run_cmd $(gpg_pass_pipein GPG_PW_SIGN GPG_PW) gpg $GPG_OPTS $GPG_USEAGENT $(gpg_param_passwd GPG_PW_SIGN GPG_PW) --armor --export"$(test "SEC" = "$KEY_TYPE" && echo -secret-keys)" $(qw "$KEY_ID") '>>' $(qw "$TMPFILE")
        CMD_ERR=$?

        if [ "$CMD_ERR" = "0" ]; then
          CMD_MSG="Write file '"$(basename "$FILE")"'"
          run_cmd mv $(qw "$TMPFILE") $(qw "$FILE")
        fi

        if [ "$CMD_ERR" != "0" ]; then
          warning "Backup failed.${CMD_OUT:+\n$CMD_OUT}"
        else
          SUCCESS=1
        fi

        # cleanup
        rm $(qw "$TMPFILE") 1>/dev/null 2>&1
      fi
    done
  done
  
  [ -n "$SUCCESS" ] && inform "$ME_NAME exported new keys to your profile.
You should backup your changed profile folder now and store it in a safe place."
}

# replace all non-alnum chars with underscore (for file operations)
function gpg_key_legalize {
  echo $* | awk '{gsub(/[^a-zA-Z0-9]/,"_",$0); print}'
}

function gpg_key_cache {
  local RES
  local MODE=$1
  shift
  local PREFIX="GPG_KEY"
  local SUFFIX=$(gpg_key_legalize "$@")
  local KEYID="$*"
  local CACHE="${PREFIX}_${MODE}_${SUFFIX}"
  if [ "$MODE" = "RESET" ]; then
    eval unset ${PREFIX}_PUB_$SUFFIX ${PREFIX}_SEC_$SUFFIX
    return 255
  elif ! var_isset "$CACHE"; then
    if [ "$MODE" = "PUB" ]; then
      RES=$(gpg $GPG_OPTS --list-key "$KEYID" > /dev/null 2>&1; echo -n $?)
    elif [ "$MODE" = "SEC" ]; then
      RES=$(gpg $GPG_OPTS --list-secret-key "$KEYID" > /dev/null 2>&1; echo -n $?)
    else
      return 255
    fi
    eval $CACHE=$RES
  fi
  eval return \$$CACHE
}

function gpg_pub_avail {
  gpg_key_cache PUB "$@"
}

function gpg_sec_avail {
  gpg_key_cache SEC "$@"
}

function gpg_key_format {
  echo $1 | grep -q '^[0-9a-fA-F]\{8\}$'
}

# splits a comma separated line into lines, respects escaped commas
function gpg_split_keyset {
  local LIST
  LIST=$(echo "$@" | awk '{ gsub(/,/,"\n",$0); gsub(/\\\n/,",",$0); print $0 }')
  echo -e "$LIST"
}

function gpg_prefix_keyset {
  local PREFIX="$1" OUT=""
  shift
  for KEY_ID in "$@"; do
    OUT="${OUT} $PREFIX $(qw ${KEY_ID})"
  done
  echo $OUT
}

# grep a variable from conf text file (currently not used)
function gpg_passwd {
  [ -r "$CONF" ] && \
  awk '/^[ \t]*GPG_PW[ \t=]/{\
        sub(/^[ \t]*GPG_PW[ \t]*=*/,"",$0);\
        gsub(/^[ \t]*[\047"]|[\047"][ \t]*$/,"",$0);\
        print $0; exit}' "$CONF"
}

# return success if at least one secret key is available
function gpg_key_decryptable {
  # no keys, no problem
  gpg_symmetric && return 0

  local KEY_ID
  for KEY_ID in "${GPG_KEYS_ENC_ARRAY[@]}"; do
    gpg_sec_avail "$KEY_ID" && return 0
  done
  return 1
}

function gpg_symmetric {
  [ -z "${GPG_KEY}${GPG_KEYS_ENC_ARRAY}" ]
}

# checks for max two params if they are set, typically GPG_PW & GPG_PW_SIGN
function gpg_param_passwd {
  var_isset GPG_USEAGENT && exit 1
  
  if ( [ -n "$1" ] && var_isset "$1" ) || ( [ -n "$2" ] && var_isset "$2" ); then
    echo "--passphrase-fd 0 --batch"
  fi
}

# select the earliest defined and create an "echo <value> |" string
function gpg_pass_pipein {
  var_isset GPG_USEAGENT && exit 1
  
  for var in "$@"
  do
    if var_isset "$var"; then
      echo "echo $(qw $(eval echo \$$var)) |"
      return 0
    fi
  done
  
  return 1
}

# checks if gpg-agent is available, returns error code
# 0 on success
# 1 if GPG_AGENT_INFO is not set (unused, should probably be merged w/ 3)
# 2 if GPG_AGENT_INFO is stale
# 3 cannot connect to gpg-agent
function gpg_agent_avail {
  # GPG_AGENT_INFO is deprecated in gpg2.1, 
  # first try to connect to a possibly running agent here
  local ERR=3
  gpg-agent > /dev/null 2>&1 && return 0

  # detect stale pid in legacy GPG_AGENT_INFO env var
  if var_isset GPG_AGENT_INFO; then
    # check if a pid matching process is running at all
    local GPG_AGENT_PID=$(echo $GPG_AGENT_INFO|awk -F: '{print $2}')
    if isnumber "$GPG_AGENT_PID"; then
      ps -p "$GPG_AGENT_PID" > /dev/null 2>&1 || ERR=2
    fi
  fi

  return $ERR
}

# compare arg1 with "gpg --version" output, possible arg1 formats e.g. 
#   2.5+ (greater equals), 1.3- (less or equals) or plain 2.2 (equals)
#   returns 0 on success, 1 on failure or 255 on error
function gpg_version_compare {
  [ -n "$GPGVERSION_ERROR" ] && return 255
  
  GPGVERSION=$(gpg --version|awk '/^gpg \(GnuPG\) [0-9\.]+$/{print $NF;exit}')  || echo warning
  
  ! awk '!/^[0-9\.]+$/{exit 1}' <<< "$GPGVERSION" && {
    # silently ignore, currently only used for gpg tests anyway
    GPGVERSION_ERROR=1
    return 255
  }
  GPGVERSION=( ${GPGVERSION//./ } )

  CMPIN=$( awk '{sub(/[\+\-]*$/,"");print}' <<< "$1" )
  CMPMODE=$( awk '{mode="-eq"}/-$/{mode="-le"}/\+$/{mode="-ge"}{print mode;exit}' <<< "$1")
  CMPVERSION=( ${CMPIN//./ } )

  #echo ${GPGVERSION[@]} "/" ${CMPVERSION[@]} "/" $CMPMODE

  local max=$( [ ${#GPGVERSION[@]} -gt ${#CMPVERSION[@]} ] && echo ${#GPGVERSION[@]} || echo ${#CMPVERSION[@]} )

  #limit=$( [ $max -gt 0 ] && echo (($max-1)) || echo 0 )
  local i gpg cmp
  for ((i=0; i<$max; i++)); do
    gpg=${GPGVERSION[$i]}
    [ -z "$gpg" ] && gpg=0
    cmp=${CMPVERSION[$i]}
    [ -z "$cmp" ] && cmp=0
    #echo $gpg $CMPMODE $cmp
    # compare n fail early
    ! [ $gpg $CMPMODE $cmp ] && { return 1; }
  done
  
  return 0
}

function gpg_custom_binary {
  var_isset GPG && [ "$GPG" != "$DEFAULT_GPG" ] &&\
    echo "--gpg-binary $(qw "$GPG")"
}

function gpg_binary {
  local BIN
  var_isset GPG && BIN="$GPG" || BIN="$DEFAULT_GPG"
  echo "$BIN"
}

function gpg_avail {
  lookup "$(gpg_binary)"
}

# enforce the use of our selected gpg binary
function gpg {
  command "$(gpg_binary)" "$@"
}
export -f gpg

# start of script #######################################################################

# confidentiality first, all we create is only readable by us
umask 077

# check if ftplicity is there & executable
[ -n "$ME_LONG" ] && [ -x "$ME_LONG" ] || error "$ME missing. Executable & available in path? ($ME_LONG)"

if [ ${#@} -eq 1 ]; then
  cmd="${1}"
else
  FTPLCFG="${1}" ; cmd="${2}"
fi

# deal with command before profile validation calls
# show requested version
# OR requested usage info
# OR create a profile
# OR fall through
##if [ ${#@} -le 2 ]; then
case "$cmd" in
  changelog)
    changelog
    exit 0
    ;;
  create)
    set_config
    if [ -d "$CONFDIR" ]; then
      error "The profile '$FTPLCFG' already exists in
'$CONFDIR'.

Hint:
 If you _really_ want to create a new profile by this name you will 
 have to manually delete the existing profile folder first."
      exit 1
    else
      create_config
      exit 0
    fi
    ;;
  txt2man)
    set_config
    usage_txt2man
    exit 0
    ;;
  usage|-help|--help|-h|-H)
    set_config
    usage_info
    exit 0
    ;;
  version|-version|--version|-v|-V)
    # profile can override GPG/PYTHON, so import it if it was given
    var_isset FTPLCFG && {
      set_config
      [ -r "$CONF" ] && . "$CONF" || warning "Cannot import config '$CONF'."
    }
    version_info_using
    exit 0
    ;;
  # fallthrough.. we got a command that needs an existing profile
  *)
    # if we reach here, user either forgot profile or chose wrong profileless command
    if [ ${#@} -le 1 ]; then
      error "\
 Missing or wrong parameters. 
 Only the commands 
   changelog, create, usage, txt2man, version
 can be called without selecting an existing profile first.
 Your command was '$cmd'.

 Hint: Run '$ME usage' to get help."
    fi
esac


# Hello world
echo "Start $ME v$ME_VERSION, time is $(date_fix '%F %T')."

# check system environment

# is duplicity avail
lookup duplicity || error_path "duplicity missing. installed und available in path?"
# init, exec duplicity version check info
duplicity_version_get

# check for certain important helper programs
# TODO: we should probably check for duplicity and $PYTHON (if set) here too
for f in awk grep ; do
  lookup "$f" || \
    error_path "$f missing. installed und available in path?"
done

### read configuration
set_config
# check validity
if [ ! -d "$CONFDIR" ]; then 
    error "Selected profile '$FTPLCFG' does not resolve to a profile folder in
'$CONFDIR'.

Hints:
Select one of the available profiles: $(for d in "$(dirname "$CONFDIR")"/*/; do [ -e "$d" ] || [ -L "$d" ] || continue; printf "$sep'$(basename "$d")'"; sep=",";done)
 Use '$ME <name> create' to create a new profile.
 Use '$ME usage' to get usage help."
elif [ ! -x "$CONFDIR" ]; then
    error "\
Profile folder in '$CONFDIR' cannot be accessed.

Hint: 
 Check the filesystem permissions and set directory accessible e.g. 'chmod 700'."
elif [ ! -f "$CONF" ] ; then
  error "'$CONF' not found."
elif [ ! -r "$CONF" ] ; then
  error "'$CONF' not readable."
else
  . "$CONF"
  #KEYFILE="${KEYFILE//.asc/${GPG_KEY:+.$GPG_KEY}.asc}"
  TEMP_DIR=${TEMP_DIR:-'/tmp'}
  # backward compatibility: old TARGET_PW overrides silently new TARGET_PASS if set
  if var_isset 'TARGET_PW'; then
    TARGET_PASS="${TARGET_PW}"
  fi
fi
echo "Using profile '$CONFDIR'."

# secure config dir, if needed w/ warning
secureconf

# split TARGET in handy variables
TARGET_SPLIT_URL=$(echo "$TARGET" | awk '{ \
  target=$0; match(target,/^([^\/:]+):\/\//); \
  prot=substr(target,RSTART,RLENGTH);\
  rest=substr(target,RSTART+RLENGTH); \
  if (credsavail=match(rest,/^[^@]*@/)){\
    creds=substr(rest,RSTART,RLENGTH-1);\
    credcount=split(creds,cred,":");\
    rest=substr(rest,RLENGTH+1);\
    # split creds with regexp\
    match(creds,/^([^:]+)/);\
    user=substr(creds,RSTART,RLENGTH);\
    pass=substr(creds,RSTART+1+RLENGTH);\
  };\
  # filter quotes or escape them\
  gsub(/[\047\042]/,"",prot);\
  gsub(/[\047\042]/,"",rest);\
  gsub(/[\047]/,"\047\\\047\047",creds);\
  print "TARGET_URL_PROT=\047"prot"\047\n"\
         "TARGET_URL_HOSTPATH=\047"rest"\047\n"\
         "TARGET_URL_CREDS=\047"creds"\047\n";\
   if(user){\
     gsub(/[\047]/,"\047\\\047\047",user);\
     print "TARGET_URL_USER=\047"user"\047\n"}\
   if(pass){\
     gsub(/[\047]/,"\047\\\047\047",pass);\
     print "TARGET_URL_PASS=$(url_decode \047"pass"\047)\n"}\
  }')
eval "${TARGET_SPLIT_URL}"

# fetch commmand from parameters ########################################################
# Hint: cmds is also used to check if authentification info sufficient in the next step 
cmds="$2"; shift 2

# complain if command(s) missing
[ -z "$cmds" ] && error "  No command given.

  Hint: 
    Use '$ME usage' to get usage help."

# process params
for param in "$@"; do
  #echo !$param!
  case "$param" in
    # enable ftplicity preview mode
    '--preview')
      PREVIEW=1
      ;;
    # interpret duplicity disable encr switch
    '--disable-encryption')
      GPG_KEY='disabled'
      ;;
    *)
      if [ `echo "$param" | grep -e "^-"` ] || \
         [ `echo "$last_param" | grep -e "^-"` ] ; then
        # forward parameter[/option pairs] to duplicity
        dupl_opts["${#dupl_opts[@]}"]=${param}
      else
        # anything else must be a parameter (e.g. for fetch, ...)
        ftpl_pars["${#ftpl_pars[@]}"]=${param}
      fi
      last_param=${param}
      ;;
  esac
done

# plausibility check config - VARS & KEY ################################################
# check if src, trg, trg pw
# auth info sufficient 
# gpg key, gpg pwd (might be empty) set in config
# OR key in local gpg db
# OR key can be imported from keyfile 
# OR fail
if [ -z "$SOURCE" ] || [ "$SOURCE" == "${DEFAULT_SOURCE}" ]; then
 error " Source Path (setting SOURCE) not set or still default value in conf file 
 '$CONF'."

elif [ -z "$TARGET" ] || [ "$TARGET" == "${DEFAULT_TARGET}" ]; then
 error " Backup Target (setting TARGET) not set or still default value in conf file 
 '$CONF'."

elif var_isset 'TARGET_USER' && var_isset 'TARGET_URL_USER' && \
     [ "${TARGET_USER}" != "${TARGET_URL_USER}" ]; then
 error " TARGET_USER ('${TARGET_USER}') _and_ user in TARGET url ('${TARGET_URL_USER}') 
 are configured with different values. There can be only one.
 
 Hint: Remove conflicting setting."

elif var_isset 'TARGET_PASS' && var_isset 'TARGET_URL_PASS' && \
     [ "${TARGET_PASS}" != "${TARGET_URL_PASS}" ]; then
 error " TARGET_PASS ('${TARGET_PASS}') _and_ password in TARGET url ('${TARGET_URL_PASS}') 
 are configured with different values. There can be only one.
 
 Hint: Remove conflicting setting."
fi

# GPG config plausibility check1 (disabled check) #############################
if gpg_disabled; then
  : # encryption disabled, all is well
elif [ -z "${GPG_KEY}${GPG_KEYS_ENC}${GPG_KEY_SIGN}" ] && ! var_isset 'GPG_PW'; then
  warning "GPG_KEY, GPG_KEYS_ENC, GPG_KEY_SIGN and GPG_PW are empty/not set in conf file 
'$CONF'.
Will disable encryption for duplicity now.

Hint: 
 If you really want to use _no_ encryption you can disable this warning by 
 setting GPG_KEY='disabled' in conf file."
  GPG_KEY='disabled'
fi

# GPG availability check (now we know if gpg is really needed)#################
if ! gpg_disabled; then 
  gpg_avail || error_path "gpg '$(gpg_binary)' missing. installed und available in path?"
fi

# Output versions info ########################################################
using_info

# GPG create key settings, config check2 (needs gpg) ##########################
if gpg_disabled; then
  : # the following tests are not necessary
else

# we test this early as any invocation gpg2.1+ starts gpg-agent automatically
GPG_AGENT_ERR=$(gpg_agent_avail ; echo $?)

# enc key still default?
if [ "$GPG_KEY" == "${DEFAULT_GPG_KEY}" ]; then 
  error_gpg "Encryption Key GPG_KEY still default in conf file 
'$CONF'."
fi

# create array of gpg encr keys, for further processing
OIFS="$IFS" IFS=$'\n'
GPG_KEYS_ENC_ARRAY=( $( gpg_split_keyset ${GPG_KEY},${GPG_KEYS_ENC} ) )
IFS="$OIFS"

# pw set? 
# symmetric needs one, always
if gpg_symmetric && ( [ -z "$GPG_PW" ] || [ "$GPG_PW" == "${DEFAULT_GPG_PW}" ] ) \
  ; then
  error_gpg "Encryption passphrase GPG_PW (needed for symmetric encryption) 
is empty/not set or still default value in conf file 
'$CONF'."
fi
# this is a technicality, we can only pump one pass via pipe into gpg
# but symmetric already always needs one for encryption
if gpg_symmetric && var_isset GPG_PW && var_isset GPG_PW_SIGN &&\
  [ -n "$GPG_PW_SIGN" ] && [ "$GPG_PW" != "$GPG_PW_SIGN" ]; then
  error_gpg "GPG_PW _and_ GPG_PW_SIGN are defined but not identical in config
'$CONF'.
This is unfortunately impossible. For details see duplicity manpage, 
section 'A Note On Symmetric Encryption And Signing'.

Tip: Separate signing keys may have empty passwords e.g. GPG_PW_SIGN=''.
Tip2: Use gpg-agent."
fi

# test - GPG KEY AVAILABILITY ##################################################

# check gpg public keys availability, try import if needed
for (( i = 0 ; i < ${#GPG_KEYS_ENC_ARRAY[@]} ; i++ )); do
  KEY_ID="${GPG_KEYS_ENC_ARRAY[$i]}"
  # test availability, try to import, retest
  if ! gpg_pub_avail "${KEY_ID}"; then
    echo "Encryption public key '${KEY_ID}' not in keychain. Try to import from profile."
    gpg_import "${KEY_ID}" PUB
    gpg_key_cache RESET "${KEY_ID}"
    gpg_pub_avail "${KEY_ID}" || { \
      gpg_testing && error_gpg \
      "Needed public gpg key '${KEY_ID}' is not available in keychain." \
      "Doublecheck if the above key is listed by 'gpg --list-keys' or available
  as gpg key file '$(basename "$(gpg_keyfile "${KEY_ID}")")' in the profile folder.
  If not you can put it there and $ME_NAME will autoimport it on the next run.
  Alternatively import it manually as the user you plan to run $ME_NAME with."
    }
  else
    echo "Public key '${KEY_ID}' found in keychain."
  fi
done

# check gpg encr secret encryption keys availability and fail
# if none is available after a round of importing trials
gpg_key_decryptable || \
{
  echo "Missing secret keys for decryption in keychain."
  for (( i = 0 ; i < ${#GPG_KEYS_ENC_ARRAY[@]} ; i++ )); do
    KEY_ID="${GPG_KEYS_ENC_ARRAY[$i]}"
    # test availability, try to import, retest
    if ! gpg_sec_avail "${KEY_ID}"; then
    echo "Try to import secret key '${KEY_ID}' from profile."
      gpg_import "${KEY_ID}" SEC
      gpg_key_cache RESET "${KEY_ID}"
    fi
  done
  gpg_key_decryptable || \
  {
    gpg_testing && error_gpg_test "None of the configured keys '$(join "','" "${GPG_KEYS_ENC_ARRAY[@]}")' \
has a secret key in the keychain. Decryption will be impossible!"
  }
}

# gpg secret sign key availability
# if none set, autoset first encryption key as sign key
if ! gpg_signing; then
  echo "Signing disabled per configuration."
# try first key, if one set
elif ! var_isset 'GPG_KEY_SIGN'; then
  KEY_ID="${GPG_KEYS_ENC_ARRAY[0]}"
  if [ -z "${KEY_ID}" ]; then
    echo "Signing disabled. No GPG_KEY entries in config."
    GPG_KEY_SIGN='disabled'
  else  
    # use avail OR try import OR fail
    if gpg_sec_avail "${KEY_ID}"; then
      GPG_KEY_SIGN="${KEY_ID}"
    else
      echo "Signing secret key '${KEY_ID}' not found."
      gpg_import "${KEY_ID}" SEC
      gpg_key_cache RESET "${KEY_ID}"
      if gpg_sec_avail "${KEY_ID}"; then
        GPG_KEY_SIGN="${KEY_ID}"
      fi
    fi

    # interpret sign key setting
    if var_isset 'GPG_KEY_SIGN'; then
      echo "Autoset found secret key of first GPG_KEY entry '${KEY_ID}' for signing."
    else
      echo "Signing disabled. First GPG_KEY entry's '${KEY_ID}' private key is missing."
      GPG_KEY_SIGN='disabled'
    fi
  fi
else
  KEY_ID="${GPG_KEY_SIGN}"
  if ! gpg_sec_avail "${KEY_ID}"; then
    inform "Secret signing key defined in setting GPG_KEY_SIGN='${KEY_ID}' not found.\nTry to import."
    gpg_import "${KEY_ID}" SEC
    gpg_key_cache RESET "${KEY_ID}"
    gpg_sec_avail "${KEY_ID}" || error_gpg_key "${KEY_ID}" "Private"
  fi
fi

# using GPG_AGENT_ERR set early above, try to autoenable gpg-agent or issue some warnings
# key enc can deal without, but might profit from gpg-agent
# if GPG_PW is not set alltogether
# if signing key is different from first (main) enc key (we can only pipe one pass into gpg)
if ! gpg_symmetric && \
   ( ! var_isset GPG_PW || \
     ( gpg_signing && ! var_isset GPG_PW_SIGN && [ "$GPG_KEY_SIGN" != "${GPG_KEYS_ENC_ARRAY[0]}" ] ) ); then

  if [ "$GPG_AGENT_ERR" -eq 1 ]; then
    warning "Cannot use gpg-agent. GPG_AGENT_INFO not set."
  elif [ "$GPG_AGENT_ERR" -eq 2 ]; then
    warning "Cannot use gpg-agent! GPG_AGENT_INFO contains stale pid."
  elif [ "$GPG_AGENT_ERR" -eq 3 ]; then
    warning "No running gpg-agent found although GPG_PW or GPG_PW_SIGN (enc != sign key) not set."
  else
    echo "Enable gpg-agent usage. Running gpg-agent instance found and GPG_PW or GPG_PW_SIGN (enc != sign key) not set."
    GPG_USEAGENT="--use-agent"
  fi
fi

# end GPG config plausibility check2 
fi

# config plausibility check - SPACE ###########################################

# is tmp is a folder and writable
CMD_MSG="Checking TEMP_DIR '${TEMP_DIR}' is a folder and writable"
run_cmd test -d $(qw "$TEMP_DIR") '&&' test -w $(qw "$TEMP_DIR")
if [ "$?" != "0" ]; then
    error "Temporary file space '$TEMP_DIR' is not a directory or writable."
fi


# get volsize, default duplicity volume size is 25MB since v0.5.07
#VOLSIZE=${VOLSIZE:-25}
# double if asynch is on
#echo $@ $DUPL_PARAMS | grep -q -e '--asynchronous-upload' && FACTOR=2 || FACTOR=1

# TODO: check for enough (async= upload space and WARN only
#       use function tmp_space 
#echo TODO: reimplent tmp space check


# test - GPG SANITY #####################################################################
# if encryption is disabled, skip this whole section
if gpg_disabled; then
  echo -e "Test - En/Decryption skipped. (GPG='disabled')"
elif ! gpg_testing; then 
  echo -e "Test - En/Decryption skipped. (GPG_TEST='disabled')"
else

GPG_TEST_PREFIX="$TEMP_DIR/${ME_NAME}.$$.$(date_fix %s)"
function cleanup_gpgtest { 
  echo -en "Cleanup - Delete '${GPG_TEST_PREFIX}_*'"
  rm "${GPG_TEST_PREFIX}"_* 2>/dev/null && echo "(OK)" || echo "(FAILED)"
}

# add pinentry mode for gpg2.1+ and --use-agent not enabled, like duplicity does in gpg.py
GPG_PINENTRY='--pinentry-mode=loopback'
for p in $GPG_OPTS; do
  # we respect already set --pinentry-mode though
  $( awk '/^--pinentry-mode/{exit 0}{exit 1}' <<< "$p" ) && {
    unset GPG_PINENTRY
    break
    }
done
for p in $GPG_USEAGENT "${dupl_opts[@]}" $DUPL_PARAMS; do
  $( awk '/^--use-agent/{exit 0}{exit 1}' <<< "$p" ) && {
    unset GPG_PINENTRY
    break
    }
done
gpg_version_compare "2.1+" && var_isset GPG_PINENTRY && {
  GPG_OPTS="$GPG_OPTS $GPG_PINENTRY"
}

# signing enabled?
if gpg_signing; then
  CMD_PARAM_SIGN="--sign --default-key $(qw ${GPG_KEY_SIGN})"
  CMD_MSG_SIGN="Sign with '${GPG_KEY_SIGN}'"
fi

# using keys
if [ ${#GPG_KEYS_ENC_ARRAY[@]} -gt 0 ]; then

  for KEY_ID in "${GPG_KEYS_ENC_ARRAY[@]}"; do
    CMD_PARAMS="$CMD_PARAMS -r $(qw ${KEY_ID})"
  done
  # check encrypting
  CMD_MSG="Test - Encrypt to '$(join "','" "${GPG_KEYS_ENC_ARRAY[@]}")'${CMD_MSG_SIGN:+ & $CMD_MSG_SIGN}"
  run_cmd $(gpg_pass_pipein GPG_PW_SIGN GPG_PW) gpg $CMD_PARAM_SIGN $(gpg_param_passwd GPG_PW_SIGN GPG_PW) $CMD_PARAMS $GPG_USEAGENT --status-fd 1 $GPG_OPTS -o $(qw "${GPG_TEST_PREFIX}_ENC") -e $(qw "$ME_LONG")
  CMD_ERR=$?

  if [ "$CMD_ERR" != "0" ]; then 
    KEY_NOTRUST=$(echo "$CMD_OUT"|awk '/^\[GNUPG:\] INV_RECP 10/ { print $4 }')
    [ -n "$KEY_NOTRUST" ] && HINT="Key '${KEY_NOTRUST}' seems to be untrusted. If you really trust this key try to
  'gpg --edit-key "$KEY_NOTRUST"' and raise the trust level to ultimate. If you
  can trust all of your keys set GPG_OPTS='--trust-model always' in conf file."
    error_gpg_test "Encryption failed (Code $CMD_ERR).${CMD_OUT:+\n$CMD_OUT}" "$HINT"
  fi

  # check decrypting
  CMD_MSG="Test - Decrypt"
  gpg_key_decryptable || CMD_DISABLED="No matching secret key available."
  run_cmd $(gpg_pass_pipein GPG_PW) gpg $(gpg_param_passwd GPG_PW) $GPG_OPTS -o $(qw "${GPG_TEST_PREFIX}_DEC") $GPG_USEAGENT -d $(qw "${GPG_TEST_PREFIX}_ENC")
  CMD_ERR=$?

  if [ "$CMD_ERR" != "0" ]; then 
    error_gpg_test "Decryption failed.${CMD_OUT:+\n$CMD_OUT}"
  fi

# symmetric only
else
  # check encrypting
  CMD_MSG="Test - Encryption with passphrase${CMD_MSG_SIGN:+ & $CMD_MSG_SIGN}"
  run_cmd $(gpg_pass_pipein GPG_PW) gpg $GPG_OPTS $CMD_PARAM_SIGN --passphrase-fd 0 -o $(qw "${GPG_TEST_PREFIX}_ENC") --batch -c $(qw "$ME_LONG")
  CMD_ERR=$?
  if [ "$CMD_ERR" != "0" ]; then 
    error_gpg_test "Encryption failed.${CMD_OUT:+\n$CMD_OUT}"
  fi

  # check decrypting
  CMD_MSG="Test - Decryption with passphrase"
  run_cmd $(gpg_pass_pipein GPG_PW) gpg $GPG_OPTS --passphrase-fd 0 -o $(qw "${GPG_TEST_PREFIX}_DEC") --batch -d $(qw "${GPG_TEST_PREFIX}_ENC")
  CMD_ERR=$?
  if [ "$CMD_ERR" != "0" ]; then 
    error_gpg_test "Decryption failed.${CMD_OUT:+\n$CMD_OUT}"
  fi
fi

# compare original w/ decryptginal
CMD_MSG="Test - Compare"
[ -r "${GPG_TEST_PREFIX}_DEC" ] || CMD_DISABLED="File not found. Nothing to compare."
run_cmd "test \"\$(cat '$ME_LONG')\" = \"\$(cat '${GPG_TEST_PREFIX}_DEC')\""
CMD_ERR=$?
if [ "$CMD_ERR" = "0" ]; then 
  cleanup_gpgtest
else
  error_gpg_test "Comparision failed.${CMD_OUT:+\n$CMD_OUT}"
fi

fi # end disabled

## an empty line
#echo

# Exclude file is needed, create it if necessary
[ -f "$EXCLUDE" ] || touch "$EXCLUDE"

# export only used keys, if bkp not already exists ######################################
gpg_export_if_needed "${GPG_KEYS_ENC_ARRAY[@]}" "$(gpg_signing && echo $GPG_KEY_SIGN)"


# command execution #####################################################################

# urldecode url vars into plain text
var_isset 'TARGET_URL_USER' && TARGET_URL_USER="$(url_decode "$TARGET_URL_USER")"
var_isset 'TARGET_URL_PASS' && TARGET_URL_PASS="$(url_decode "$TARGET_URL_PASS")"

# defined TARGET_USER&PASS vars replace their URL pendants 
# (double defs already dealt with)
var_isset 'TARGET_USER' && TARGET_URL_USER="$TARGET_USER"
var_isset 'TARGET_PASS' && TARGET_URL_PASS="$TARGET_PASS"

TARGET_URL_PROT_lowercase="$(tolower "${TARGET_URL_PROT%%:*}")"

# issue some warnings
case "$TARGET_URL_PROT_lowercase" in
  'cf+http')
    # info on missing AUTH_URL
    if ! var_isset 'CLOUDFILES_AUTHURL'; then
      inform "No CLOUDFILES_AUTHURL exported (in conf).
Will use default which is probably rackspace."
    fi
    ;;
   'swift')
    # info on possibly missing AUTH_URL
       var_isset 'SWIFT_AUTHURL' ||\
       warning "\
Swift will probably fail because the conf var SWIFT_AUTHURL was not exported!"
    ;;
esac


# for all protocols we put username in url and pass into env var 
# for security reasons, we url_encode username to protect special chars
# first sortout backends with special ways to handle password
case "$TARGET_URL_PROT_lowercase" in
  'imap'|'imaps')
    var_isset 'TARGET_URL_PASS' && BACKEND_PARAMS="IMAP_PASSWORD=$(qw "${TARGET_URL_PASS}")"
    ;;
  *)
    # rest uses FTP_PASS var
    var_isset 'TARGET_URL_PASS' && \
      BACKEND_PARAMS="FTP_PASSWORD=$(qw "${TARGET_URL_PASS}")"
    ;;
esac
# insert url encoded username into target url if needed
if var_isset 'TARGET_URL_USER' && [ "$TARGET_URL_PROT_lowercase" != "file" ]; then
  BACKEND_URL="${TARGET_URL_PROT}$(url_encode "${TARGET_URL_USER}")@${TARGET_URL_HOSTPATH}"
else
  BACKEND_URL="$TARGET"
fi


# protect eval from special chars in url (e.g. open ')' in password, 
# spaces in path, quotes) happens above in duplify() via quotewrap()
SOURCE="$SOURCE"
BACKEND_URL="$BACKEND_URL"
EXCLUDE="$EXCLUDE"
EXCLUDE_PARAM="--exclude-filelist"

# replace 
# - magic separators to command equivalents (+=and,-=or,[=groupIn,]=groupOut)
# - multiple separator chars '_ ' with a single ' '
cmds=$(awk -v cmds="$cmds" "BEGIN{ \
  gsub(/\+/,\" and \",cmds);\
  gsub(/\-/,\" or \",cmds);\
  gsub(/\[/,\" groupIn \",cmds);\
  gsub(/\]/,\" groupOut \",cmds);\
  gsub(/[_ ]+/,\" \",cmds);\
  print cmds}")

# convert cmds to array, 
# post process, translate batch commands
# ATTENTION: commands are lowercase from here on out
declare -a CMDS
for cmd in $(tolower "$cmds"); do
  case "$cmd" in
    # backup -> [pre_bkp_post]
    'backup')
      CMDS=("${CMDS[@]}" groupin pre bkp post groupout)
    ;;
    # purgeAuto -> [purge purgeFull purgeIncr] depending on set conf vars 
    'purgeauto')
      purgeAuto=${MAX_AGE:+ purge}${MAX_FULL_BACKUPS:+ purgefull}${MAX_FULLS_WITH_INCRS:+ purgeincr}
      [[ -z "$purgeAuto" ]] && error "Command 'fullAuto' was given but neither of the purge conf vars configured."
      CMDS=("${CMDS[@]}" groupin $purgeAuto groupout)
    ;;
    *)
      CMDS=("${CMDS[@]}" "$cmd")
    ;;
  esac
done
#echo $(IFS=',';echo "${CMDS[*]}")

unset FTPL_ERR

# run CMDS
for cmd in "${CMDS[@]}";
do

## init
# raise index in cmd array for pre/post param
var_isset 'CMD_NO' && CMD_NO=$((++CMD_NO)) || CMD_NO=0

unset CMD_VALUE CMD_NEXT CMD_PREV CND_NEXT CND_PREV

# get next cmd,cnd vars
nextno=$(( $CMD_NO ))
while ! var_isset 'CMD_NEXT'
do
  nextno=$(($nextno+1))
  if [ "$nextno" -lt "${#CMDS[@]}" ]; then
    CMD_VALUE=${CMDS[$nextno]}
    is_condition "$CMD_VALUE" && CND_NEXT="$CMD_VALUE" && continue
    is_groupMarker "$CMD_VALUE" && continue
    CMD_NEXT="$CMD_VALUE"
  else
    CMD_NEXT='END'
  fi
done

# get prev cnd, cnds are skipped pseudocmds
prevno=$(( $CMD_NO ));
while ! var_isset 'CND_PREV'
do
  prevno=$(($prevno-1))
  if [ "$prevno" -ge 0 ]; then
    CMD_VALUE=${CMDS[$prevno]}
    is_condition "$CMD_VALUE" && CND_PREV="$CMD_VALUE" && break
    is_command "$CMD_VALUE" && break
  else
    break
  fi
done

# get prev cmd command minus skipped commands, only executed
prevno=$(( $CMD_NO - ${CMD_SKIPPED-0} ));
while ! var_isset 'CMD_PREV'
do
  prevno=$(($prevno-1))
  if [ "$prevno" -ge 0 ]; then
    CMD_VALUE=${CMDS[$prevno]}
    is_condition "$CMD_VALUE" && CND_PREV="$CMD_VALUE" && continue
    is_groupMarker "$CMD_VALUE" && continue
    CMD_PREV="$CMD_VALUE"
  else
    CMD_PREV='START'
  fi
done

function get_cmd_skip_count {
  # find closing bracket, get group skip count
  local nextno=$CMD_NO
  local GRP_OPEN=0
  local GRP_SKIP=0
  local CMD_VALUE
  while [ "$nextno" -lt "${#CMDS[@]}" ]
  do
    nextno=$(($nextno+1))
    CMD_VALUE=${CMDS[$nextno]}
    GRP_SKIP=$(( ${GRP_SKIP} + 1 ));
    if is_command "$CMD_VALUE" && [ "$GRP_OPEN" -lt 1 ]; then
        break;
    elif [ "$CMD_VALUE" == 'groupin' ]; then
      GRP_OPEN=$(( ${GRP_OPEN} + 1 ))
    elif [ "$CMD_VALUE" == 'groupout' ]; then
      GRP_OPEN=$(( ${GRP_OPEN} - 1 ))
      if [ "$GRP_OPEN" -lt 1 ]; then
        break;
      fi
    fi
  done

  echo $GRP_SKIP;
}

# decision time: are we skipping already or dealing with condition "commands" or other non-cmds?
unset SKIP_NOW
if var_isset 'CMD_SKIP' && [ $CMD_SKIP -gt 0 ]; then
  # skip cnd/grp cmds silently
  is_command "$cmd" && echo -e "\n--- Skipping command $(toupper $cmd) ! ---"
  CMD_SKIP=$(($CMD_SKIP - 1))
  SKIP_NOW="yes"
elif ! var_isset 'PREVIEW' && [ "$cmd" == 'and' ] && [ "$CMD_ERR" -ne "0" ]; then
  CMD_SKIP=$(get_cmd_skip_count)
  unset CMD_SKIPPED
  SKIP_NOW="yes"
elif ! var_isset 'PREVIEW' && [ "$cmd" == 'or' ] && [ "$CMD_ERR" -eq "0" ]; then
  CMD_SKIP=$(get_cmd_skip_count)
  unset CMD_SKIPPED
  SKIP_NOW="yes"
elif is_condition "$cmd" || is_groupMarker "$cmd"; then
  unset 'CMD_SKIP';
  SKIP_NOW="yes"
fi

# let's do the skip now
if [ -n "$SKIP_NOW" ]; then
  # sum up how many commands we actually skipped for the prev var routines
  CMD_SKIPPED=$((${CMD_SKIPPED-0} + 1))
  continue
fi

# save start time
RUN_START=$(nsecs)

# export some useful env vars for external scripts/programs to use
export PROFILE CONFDIR SOURCE TARGET_URL_PROT TARGET_URL_HOSTPATH \
       TARGET_URL_USER TARGET_URL_PASS \
       GPG_KEYS_ENC=$(join "\n" "${GPG_KEYS_ENC_ARRAY[@]}") GPG_KEY_SIGN \
       GPG_PW CMD_PREV CMD_NEXT CMD_ERR CND_PREV CND_NEXT\
       RUN_START

# user info
echo; separator "Start running command $(toupper $cmd) at $(datefull_from_nsecs $RUN_START)"

case "$(tolower $cmd)" in
  'pre'|'post')
    if [ "$cmd" == 'pre' ]; then
      script=$PRE
    else
      script=$POST
    fi
    # script execution in a subshell, protect us from failures/var overwrites
    ( run_script "$script" )
    ;;
  'bkp')
    duplify backup -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \
          "$SOURCE" "$BACKEND_URL"
    ;;
  'incr')
    duplify incremental -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \
          "$SOURCE" "$BACKEND_URL"
    ;;
  'full')
    duplify full -- "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \
          "$SOURCE" "$BACKEND_URL"
    ;;
  'verify')
    TIME="${ftpl_pars[0]:+"-t ${ftpl_pars[0]}"}"
    duplify verify -- $TIME "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \
          "$BACKEND_URL" "$SOURCE"
    ;;
  'verifypath')
    TIME="${ftpl_pars[2]:+"-t ${ftpl_pars[2]}"}"
    IN_PATH="${ftpl_pars[0]}"; OUT_PATH="${ftpl_pars[1]}"; 
    ( [ -z "$IN_PATH" ] || [ -z "$OUT_PATH" ] ) && error "  Missing parameter <rel_bkp_path> or <local_path> for verifyPath.
  
  Hint: 
    Syntax is -> $ME <profile> verifyPath <rel_bkp_path> <local_path> [<age>]"

    duplify verify -- $TIME "${dupl_opts[@]}" $EXCLUDE_PARAM "$EXCLUDE" \
          --path-to-restore "$IN_PATH" "$BACKEND_URL" "$OUT_PATH"
    ;;
  'list')
    # time param exists since 0.5.10+
    TIME="${ftpl_pars[0]:+"-t ${ftpl_pars[0]}"}"
    duplify list-current-files -- $TIME "${dupl_opts[@]}" "$BACKEND_URL"
    ;;
  'cleanup')
    duplify cleanup -- "${dupl_opts[@]}" "$BACKEND_URL"
    ;;
  'purge')
    MAX_AGE=${ftpl_pars[0]:-$MAX_AGE}
    [ -z "$MAX_AGE" ] && error "  Missing parameter <max_age>. Can be set in profile or as command line parameter."
    
    duplify remove-older-than "${MAX_AGE}" \
          -- "${dupl_opts[@]}" "$BACKEND_URL"
    ;;
  'purgefull')
    MAX_FULL_BACKUPS=${ftpl_pars[0]:-$MAX_FULL_BACKUPS}
    [ -z "$MAX_FULL_BACKUPS" ] && error "  Missing parameter <max_full_backups>. Can be set in profile or as command line parameter."
  
    duplify remove-all-but-n-full "${MAX_FULL_BACKUPS}" \
          -- "${dupl_opts[@]}" "$BACKEND_URL"
    ;;
  'purgeincr')
    MAX_FULLS_WITH_INCRS=${ftpl_pars[0]:-$MAX_FULLS_WITH_INCRS}
    [ -z "$MAX_FULLS_WITH_INCRS" ] && error "  Missing parameter <max_fulls_with_incrs>. Can be set in profile or as command line parameter."
  
    duplify remove-all-inc-of-but-n-full "${MAX_FULLS_WITH_INCRS}" \
          -- "${dupl_opts[@]}" "$BACKEND_URL"
    ;;
  'restore')
    OUT_PATH="${ftpl_pars[0]}"; TIME="${ftpl_pars[1]:-now}";
    [ -z "$OUT_PATH" ] && error "  Missing parameter target_path for restore.
  
  Hint: 
    Syntax is -> $ME <profile> restore <target_path> [<age>]"

    duplify restore -- -t "$TIME" "${dupl_opts[@]}" "$BACKEND_URL" "$OUT_PATH"
    ;;
  'fetch')
    IN_PATH="${ftpl_pars[0]}"; OUT_PATH="${ftpl_pars[1]}"; 
    TIME="${ftpl_pars[2]:-now}";
    ( [ -z "$IN_PATH" ] || [ -z "$OUT_PATH" ] ) && error "  Missing parameter <src_path> or <target_path> for fetch.

  Hint: 
    Syntax is -> $ME <profile> fetch <src_path> <target_path> [<age>]"

    duplify restore -- --restore-time "$TIME" "${dupl_opts[@]}" \
              --path-to-restore "$IN_PATH" "$BACKEND_URL" "$OUT_PATH"
    ;;
  'status')
    duplify collection-status -- "${dupl_opts[@]}" "$BACKEND_URL"
    ;;    
  *)
    error "  Unknown command '$cmd'.

  Hint:
    Use '$ME usage' to get usage help."
    ;;
esac

CMD_ERR=$?
RUN_END=$(nsecs)
RUNTIME=$(( $RUN_END - $RUN_START ))

# print message on error; set error code
if [ "$CMD_ERR" -ne 0 ]; then
  error_print "$(datefull_from_nsecs $RUN_END) Task '$(echo $cmd|awk '$0=toupper($0)')' failed with exit code '$CMD_ERR'."
  FTPL_ERR=1
fi

separator "Finished state $(error_to_string $CMD_ERR) at $(datefull_from_nsecs $RUN_END) - \
Runtime $(printf "%02d:%02d:%02d.%03d" $((RUNTIME/1000000000/60/60)) $((RUNTIME/1000000000/60%60)) $((RUNTIME/1000000000%60)) $((RUNTIME/1000000%1000)) )"

done

exit ${FTPL_ERR}
