#!/bin/bash
# Package Installation Observer, pio
#PGR was here for minor config changes
#PGR SUBVER=16a: added restore command, added a deliberate delay
#PGR SUBVER=16b: changed "files" function to include "touched" files in
#		 lists and backups.  Important!
#PGR SUBVER=16c: added --color to ls, shortened backup file extension
#PGR SUBVER=16d: find wants -maxdepth before -type (some versions)
#PGR SUBVER=16e: reorganized configuration section for easier bootstrapping
#PGR SUBVER=16f: functionalized restore code, added "check" command
#PGR SUBVER=17: re-identified, changed hyphenated commands
#PGR SUBVER=17a: tar: ignore zero-blocks, don't pass tar "/"
#PGR SUBVER=17b: in write_script, sort used an obsolete POSIX syntax
#PGR SUBVER=17c: find syntax for -perm changed
#PGR SUBVER=17d: added /srv to WATCHed DIRectorieS
#
# Once upon a time there was git, the Guarded Installation Tool, by Ingo
# Brueckl (ib@wupperonline.de) 14.11.1996.  I found it useful for LFS
# builds & added a few functions.  Then Linus made a kernel development
# management tool he chose to call git.  So I changed this script to
# avoid potential confusion.  pio is still almost entirely Ingo's git.
# Some functionality has been changed.  That's my responsibility.
#

LNAME="Package Installation Observer"
SNAME=$(basename "$0")
VERSION=2
SUBVER=17d

#PGR-16e: reorganized configuration section
#--------------------------------------------------------------------------
CFG=3
case $CFG in
 1)
  #PGR During LFS-Ch5 stage1 build when $LFS is defined externally
  VIEWPROG="less -c"
  SHOWRESULT=yes         # default answer to 'Show it?'
  WATCHDIRS=$LFS
  IGNOREDIRS=
  SCRIPTPATH=$LFS/$SNAME ;;
 2)
  #PGR During LFS-Ch6 stage2 build
  VIEWPROG=cat
  SHOWRESULT=yes         # default answer to 'Show it?'
  WATCHDIRS="/bin /boot /dev /etc /lib /opt /sbin /srv /usr /var"
  IGNOREDIRS="/usr/local/src /usr/local/$SNAME /var/log"
  SCRIPTPATH=/usr/local/$SNAME ;;
 3) 
  #PGR After LFS-Ch6 stage 2 complete, general ops
  VIEWPROG="less -c"
  SHOWRESULT=yes         # default answer to 'Show it?'
  WATCHDIRS="/bin /boot /dev /etc /lib /opt /sbin /srv /usr /var"
#PGR if cups is installed, ignore cups changes
  [ -d /etc/cups ] && cups="/etc/cups" || cups=""
  [ -d /var/spool/fcron ] && fcron="/var/spool/fcron" || fcron=""
  IGNOREDIRS="/usr/local/src /usr/local/$SNAME /var/log $cups $fcron"
  SCRIPTPATH=/usr/local/$SNAME ;;
esac

#PGR-16c: use color
LSPROG="ls --color=auto -dCv" # program used to list de-installation scripts
#PGR-16c: shorten extension
BACKUPEXT=tgz          # file extension used for backups
FINDCASE=-i            # will cause --find to ignore case
PROFILEVAR=*default*   # empty, *default*, or individual variable name
PRESERVE=yes           # preserve the de-installation script's timestamps
EXPERT=no              # non-experts get some additional help
EDITPROG=${EDITOR:-vi} # program used to edit de-installation scripts
BACKUPPIPE="tar --no-recursion --null -T - -czf" # program used to back up
#PGR-17b: ignore zero blocks
RESTOREPIPE="tar -xzif" #restore parms
#--------------------------------------------------------------------------
# end of configuration section

#PGR-17: eliminated hyphenated commands
USAGE="Usage: 
$SNAME PKG
$SNAME command
	[--cd] [--deps] [--help] [--profile] [--version]
$SNAME command [argument]
	[--cwd [...]] [--ls [PKG*]] [--lsbak [PKG*]] [--dirs [PKG*]] 
$SNAME command argument
	[--edit PKG] [--remove PKG] [--backup PKG] [--restore PKG] 
	[--check PKG] [--files PKG] [--files0 PKG] [--list PKG]
	[--xcheck PKG] [--find FILENAME] 
$SNAME PKG command argument
	[PKG --requires PKG2] [PKG --uses PKG2] [PKG --supports PKG2] 
$SNAME [PKG] --lib [LIBNAME] 
$SNAME [PKG] [--watch DIR1 DIR2 ...] [--ignore DIR1 DIR2 ...]

i.e.
  PKG			observe installation start/finish of PKG
  --backup PKG		create a backup of the files in PKG
  --cd			change the current directory to $SCRIPTPATH
  --check PKG		check current files against backup
  --cwd [...]		execute with arguments given in [...], but create all
			working files in ./ (only works as first option)
  --deps		show all dependencies and usages
  --dirs [PKG]		show the defaults for --watch and --ignore or the
 			values used for the (current) installation of PKG
  --edit PKG		edit PKG's script preserving the file's timestamp
  --files PKG		list the files in PKG
  --files0 PKG		list the files in PKG with null-terminated names
  --find FILENAME	tell which PKG installed files matching FILENAME
  --help		display this help and exit
  --ignore DIRn		paths excluded from being watched during installation
  --lib [LIBNAME]	tell which programs use libraries matching LIBNAME
  --list PKG		list information about the files in PKG
  --ls [PKG]		list all de-installation scripts matching PKG*
  --lsbak [PKG]		list all backups matching PKG*
  --profile		print definitions required for option --cd and for a
 			variable containing $SCRIPTPATH
  --remove PKG		call the de-installation script for PKG
  --requires PKG2	note the dependency on PKG2
  --restore PKG		restore files from backup PKG
  --supports PKG2	note the exclusive use by PKG2
  --uses PKG2 		note the advantage because of PKG2
  --version		output version information and exit
  --watch DIRn		paths to be watched during installation
  --xcheck PKG		cross-check contents of PKG against all other
 			(executable) de-installation scripts to find out
			whether a file has been installed more than once"

TRY="Try \`$SNAME --help' for more information."

ID="De-installation script"
#PGR-17: use unhyphenated synonyms
DEPEND="requires"
USE_OF="uses"
USED_BY="supports"

BACKUPPATH=$SCRIPTPATH/backups

SOFAR=so-far
SUFFIX=pre-inst
PIOOPT=pio-opts

OPTIONS=0     # a bitmap flag for options, bit 0: --watch, bit 1: --ignore

WSPACE=$(echo -e " \t")
eval "$(echo -e 'NL="\n"')"
# newlines in filenames will be expressed by ${NL} in de-installation scripts
# the following...
export NL
# ...allows subshells started by this script to handle those filenames properly

if [ ".$1" = ".--cwd" ]; then
  shift
  TEMPDIR=$(pwd)/$SNAME.$$
else
  TEMPDIR=${TMPDIR:-/tmp}/$SNAME.$$
fi
TEMPFILE=$TEMPDIR/$SNAME

function novice ()                  # $1: string to return
{
  if [ "$EXPERT" = no ]; then
    echo "$1 "
  else
    echo ""
  fi
}

function change_filenames ()
{
  # split lines separated by \0
  # change <newline> temporarily into \0,
  # question mark into //Q,
  # and \0 into question mark

  tr "\0\n" "\n\0" | sed "s:?://Q:g" | tr "\0" "?"
}

function restore_filename ()
{
  # reverse what change_filenames did

  tr "?" "\n" | sed "s://Q:?:g"
}

function find_filenames ()
{
  eval "find $WATCHDIRS $PRUNE \"\$@\" \( -type d -printf "d" -print0 -o \
                                                  -printf "f" -print0 \)" |
  change_filenames
}

function translate_filenames ()     # $1: inserted as first character in line
{
  # escape special characters,
  # change question mark into ${NL},
  # change //Q into question mark,
  # translate file type into 'rm -f' or 'rmdir'
  # and put double quotes around the filename if (possibly) necessary

  sed -e 's:["$\\`]:\\&:g' -e 's:?:${NL}:g' -e "s://Q:?:g" \
      -e "s:^f:$1 rm -f  :" -e "s:^d:$1 rmdir  :" \
      -e 's:^\(.\{8\}\) \(.*[^a-zA-Z0-9+,./:_-].*\)$:\1"\2":'
}

function create_tempdir ()
{
  mkdir "$TEMPDIR" && chmod go-w "$TEMPDIR" && rm -rf "$TEMPDIR"/{,.[^.],..?}*
  if [ $? -ne 0 ]; then
    echo "Fatal error while creating working directory."
    exit 1
  fi
  TMPDIR=$TEMPDIR
  export TMPDIR
}

function write_script ()            # $1: pre-inst, $2: script, $3: quiet?
{
  # detect new or modified files
  find_filenames -cnewer "$1" | sort > "$TEMPFILE.new"

  # detect files that have been deleted
  find_filenames | sort > "$TEMPFILE.all"
  comm -23 "$1" "$TEMPFILE.all" > "$TEMPFILE.del"

  # handle files that have been newly created
  comm -13 "$1" "$TEMPFILE.new" | translate_filenames " " > "$TEMPFILE.rm"

  # handle previously existing files that have been changed
  comm -12 "$1" "$TEMPFILE.new" | translate_filenames "#" >> "$TEMPFILE.rm"

  # now create the de-installation script
  echo "#!/bin/bash" > "$2"
  echo "#" >> "$2"
  echo $E "$ID for \`$(basename "$1" ".$SUFFIX")'," | sed "s:^:# :" >> "$2"
  echo "# created by $LNAME $VERSION.$SUBVER on $(date)" >> "$2"
  echo >> "$2"
  if grep -q "?" "$TEMPFILE.new" "$TEMPFILE.del"; then
    echo 'NL="' >> "$2"
    echo '"        # some filenames contain newlines expressed by ${NL}' >> "$2"
    echo >> "$2"
  fi
  if [ -s "$TEMPFILE.del" ]; then
    echo "## WARNING!" >> "$2"
    echo "##" >> "$2"
    echo "## This is a list of files that seem to have been deleted during installation:" >> "$2"
    echo "##" >> "$2"
    cat "$TEMPFILE.del" | translate_filenames "#" | sed "s:^# rm...:## :" | sort >> "$2"
    echo "##" >> "$2"
    echo "## End of WARNING" >> "$2"
    echo >> "$2"
  fi
  echo "$SNAME --xcheck" '"$(basename "$0")"' >> "$2"
  echo 'echo -n "Ok to start de-installation? "' >> "$2"
  echo "read" >> "$2"
  echo 'if [ ".$REPLY" != ".y" -a ".$REPLY" != ".yes" ]; then' >> "$2"
  echo '  echo "No, aborted."' >> "$2"
  echo "  exit" >> "$2"
  echo "else" >> "$2"
  echo -n '  echo -n "Removing \`$(basename "$0")' >> "$2"
  echo "'... \"" >> "$2"
  echo "fi" >> "$2"
  echo >> "$2"
  echo "# The following statements will completely remove the installation," >> "$2"
  echo "# files already existing prior to the installation are commented out." >> "$2"
  echo >> "$2"
  #PGR updated syntax for POSIX-200112 standard (klugey, I know)
  sort -t : -k1.9 -r "$TEMPFILE.rm" >> "$2"	# puts files before directory
  echo >> "$2"
  echo 'chmod -x "$0"' >> "$2"
  echo 'echo "done."' >> "$2"
  echo 'echo -n "Remove script, too? "' >> "$2"
  echo "read" >> "$2"
  echo 'if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then' >> "$2"
  echo '  rm "$0"' >> "$2"
  echo "else" >> "$2"
  echo '  echo "It has been made non-executable instead."' >> "$2"
  echo "fi" >> "$2"

  # we're done
  if [ -z "$3" ]; then
    trap "" SIGINT
    echo "done."
    if [ -s "$TEMPFILE.del" ]; then
      echo "See it for warnings!"
    fi
  fi
}

function show_result ()             # $1: script
{
  if [ ! -s "$1" ]; then
    echo "Nothing to show."
    return 1
  elif [ -z "$VIEWPROG" ]; then
    echo -n "Can't show it, press enter. "
    read
  else
    echo -n "Show it? [$SHOWRESULT] "
    read
    REPLY=${REPLY:-$SHOWRESULT}
    if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
      $VIEWPROG "$1"
    fi
  fi
  return 0
}

function extract_filenames ()       # $1: search pattern for beginning of line
{
  NFNAME="[^$WSPACE\";]\+"   # normal (unquoted) filenames
  QFNAME='".*"'              # double quoted filenames

  sed "s:$1\($NFNAME\|$QFNAME\)\([$WSPACE;].*\)\?\$: \1:"
}

function xcheck ()                  # $1: script, $2: ANYNAME
{
  echo $E -n "Checking \`$(basename "$1")'... "

  # separate files to be removed
  grep "^ *rm -f " "$1" | extract_filenames "^ *rm -f \+" |
  sort > "$TEMPFILE.check"

  # separate files commented out
#PGR-16d: -maxdepth order problem with some versions of find
#  find "$SCRIPTPATH" -maxdepth 1 -type f -perm +u+x -print0 |
#PGR-17c: syntax for -perm changed
  find "$SCRIPTPATH" -maxdepth 1 -type f -perm /u+x -print0 |
  xargs -0re grep -h '^#[^"]*rm -f ' | extract_filenames '^#[^"]*rm -f \+' |
  sort > "$TEMPFILE.against"

  # detect files being installed more than once
  FILES=$(comm -12 "$TEMPFILE.check" "$TEMPFILE.against")

  # check result
  if [ "$FILES" ]; then
    echo -e "\nSome files seem to have been installed more than once!\n"
    grep -F "$FILES" "$1"
    echo -e "conflict(s) with:\n"
    INUM=$(find "$1" -printf "%i")
#PGR-16d: -maxdepth order problem with some versions of find
#    find "$SCRIPTPATH" -maxdepth 1 -type f -perm +u+x -not -inum $INUM \
#PGR-17c: syntax for -perm changed
    find "$SCRIPTPATH" -maxdepth 1 -type f -perm /u+x -not -inum $INUM \
    -exec grep -F "$FILES" {} \; -printf "(in %P)\n\n"
  fi

  SCRIPT=$(echo $E -n "$2" | change_filenames)
  DEPENDENCIES=$(dependencies "$SCRIPT")

  if [ -z "$FILES" -a -z "$DEPENDENCIES" ]; then
    echo "done."
    return 0
  fi

  if [ -z "$FILES" ]; then
    echo -e "\n"
  fi

  if [ "$DEPENDENCIES" ]; then
    echo $E "$DEPENDENCIES"
    echo
  fi

  return 1
}

function permissions ()             # $1: script
{
  COL="\([^ ]\+\)\( \+\)"   # a column (with delimiter)

  IFSold=$IFS
  IFS=$(echo -e "\t")

  for c in " " "#"; do
    grep "^$c *rm... " "$1" | extract_filenames "^$c *rm... \+" | tr "\n" "\0" |
    xargs -0re sh -c 'eval ls -ldbF "$@"' -- |
    sed "s:^$COL$COL$COL$COL$COL$COL$COL$COL:\1\t\3\t\5\t\7\t$c\t:"
  done | sort -r -k 6 -t "$IFS" |
  while read -r p n o g c f; do
    printf "%s %3s %-8s %-8s %s %s\n" $p $n $o $g "$c" "$f"
  done

  IFS=$IFSold
}

function files ()                   # $1: script
{
#PGR-16b: don't ignore changed previously existing files
#  grep "^ *rm... " "$1" | extract_filenames "^ *rm... \+" | sed "s:^ ::" |
  grep "^[ #] *rm... " "$1" | extract_filenames "^[ #] *rm... \+" | sed "s:^ ::" |
  tr "\n" "\0" | xargs -0re sh -c 'eval find "$@" -maxdepth 0 -print0' --
}

function chkname ()                 # $1: ANYNAME
{
  case "$1" in
    */*)  echo $E "Will not handle \`$1' (containing a slash)!"
          exit 1
          ;;
  esac
}

function chkscript ()               # $1: ANYNAME
{
  chkname "$1"
  if [ ! -f "$SCRIPTPATH/$1" ]; then
    echo $E "\`$SCRIPTPATH/$1' does not exist!"
    exit 1
  elif sed -n "3p" "$SCRIPTPATH/$1" | grep -q "$ID"; then
    return 0
  else
    echo $E "\`$SCRIPTPATH/$1' does not seem to be a de-installation script!"
    exit 1
  fi
}

function chkbakpath ()
{
  if [ ! -d "$BACKUPPATH" ]; then
    echo -n "Ok to create backup directory $BACKUPPATH? $(novice '[no/yes]')"
    read
    if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
      mkdir -p "$BACKUPPATH" || exit 1
    else
      echo "No, aborted."
      exit 1
    fi
  fi
}

function check_paths ()             # $@: directories to check
{
  RC=0
  for p in "$@"; do
    case "$p" in
      /*)  if [ ! -d "$p" ]; then
             echo $E "Warning: \`$p' does not exist!"
             RC=1
           fi
           ;;
      *)   echo $E "Will not accept \`$p' (not an absolute path)!"
           exit 1
           ;;
    esac
  done
  return $RC
}

function find_script ()             # $1: string to search
{
  NOREGEX=$(echo $E -n "f$1" | change_filenames | translate_filenames " " |
            sed -e "s:^.\{9\}::" -e 's:"$::' -e 's:[][^$\\.*]:\\&:g')
  NFNAME="[^$WSPACE\";]*$NOREGEX"   # string inside normal (unquoted) filenames
  QFNAME="\".*$NOREGEX.*\""         # string inside double quoted filenames

  cd "$SCRIPTPATH" || return 1
#PGR-16d: -maxdepth order problem with some versions of find
#  find -maxdepth 1 -type f -perm +u+x -printf "%P\0" |
#PGR-17c: syntax for -perm changed
  find -maxdepth 1 -type f -perm /u+x -printf "%P\0" |
  xargs -0re grep $FINDCASE "^[ #] *rm... \+\($NFNAME\|$QFNAME\)" /dev/null
}

function profile ()
{
  echo "function $SNAME ()"
  echo "{"
  echo '  if [ ".$1" = ".--cd" ]; then'
  echo "    cd \"$SCRIPTPATH\""
  echo "  else"
  echo "    command $SNAME" '"$@"'
  echo "  fi"
  echo "}"
  echo "export -f $SNAME"

  if [ ".$PROFILEVAR" = ".*default*" ]; then
    PROFILEVAR=$SNAME
  fi

  case "$PROFILEVAR" in *[^a-zA-Z0-9_]* | [0-9]* | _)
    echo "$SNAME --profile: \`$PROFILEVAR' is not a valid variable name." >&2
    exit 1
    ;;
  esac

  if [ "$PROFILEVAR" ]; then
    echo "$PROFILEVAR=\"$SCRIPTPATH\""
    echo "export $PROFILEVAR"
  fi
}

function quote ()                   # $1: file or path name
{
  # put single quotes around the name
  # after replacing all ' by '\'' if necessary

  case "$1" in
    *\'*)  echo $E "$1" | sed -e "s:':'\\\'':g" -e "1s:^:':" -e '$s:$:'\'':'
           ;;
    *)     echo $E "'$1'"
           ;;
  esac
}

function option_list ()             # $@: command line
{
  CURROPT=""
  for p in "$@"; do
    case "$p" in
      --watch)   WATCHDIRS=""
                 CURROPT=--watch
                 OPTIONS=$((OPTIONS | 1))
                 ;;
      --ignore)  IGNOREDIRS=""
                 CURROPT=--ignore
                 OPTIONS=$((OPTIONS | 2))
                 ;;
      -*)        echo $TRY
                 exit 1
                 ;;
      "")        continue
                 ;;
      *)         if [ -z "$CURROPT" ]; then
                   CURROPT=none
                 elif [ $CURROPT = --watch ]; then
                   WATCHDIRS="$WATCHDIRS $(quote "$p")"
                 elif [ $CURROPT = --ignore ]; then
                   IGNOREDIRS="$IGNOREDIRS $(quote "$p")"
                 else
                   echo $TRY
                   exit 1
                 fi
                 ;;
    esac
  done
  WATCHDIRS=${WATCHDIRS# }
  IGNOREDIRS=${IGNOREDIRS# }
}

function prune_statement ()         # $@: directories to ignore
{
  PRUNE=""
  for p in "$@"; do
    PRUNE="$PRUNE -path $(quote "$p") -prune -o"
  done
  PRUNE=$(echo $E "$PRUNE" | sed "s:[][*?]:\\\&:g")
}

function default_dirs ()
{
  echo $E "--watch ${WATCHDIRS:-\"\"}"
  echo $E "--ignore ${IGNOREDIRS:-\"\"}"
}

function dirs ()                    # $1: ANYNAME
{
  if [ ! -f "$SCRIPTPATH/$1.$SUFFIX" ]; then
    echo $E "A guarded installation of \`$1' isn't in progress."
    return 1
  else
    if [ -f "$SCRIPTPATH/$1.$PIOOPT" ]; then
      WATCHDIRS=$(grep "^w" "$SCRIPTPATH/$1.$PIOOPT" | sed "s:^.::")
      IGNOREDIRS=$(grep "^i" "$SCRIPTPATH/$1.$PIOOPT" | sed "s:^.::")
    fi
    default_dirs
    return 0
  fi
}

function finish_installation ()     # $1: ANYNAME
{
  chmod u+x "$SCRIPTPATH/$1"
  rm "$SCRIPTPATH/$1.$SUFFIX"
  rm -f "$SCRIPTPATH/$1.$PIOOPT"
  trap SIGINT
  show_result "$SCRIPTPATH/$1"
}

function check_directories ()
{
  if [ -z "$WATCHDIRS" ]; then
    echo "Nothing to watch."
    exit 1
  fi

  eval check_paths "$WATCHDIRS $IGNOREDIRS"

  if [ $? -ne 0 -a $OPTIONS -ne 0 ]; then
    echo -n "Continue anyway? $(novice '[no/yes]')"
    read
    if [ ".$REPLY" != ".y" -a ".$REPLY" != ".yes" ]; then
      echo "No, aborted."
      exit 1
    fi
  fi

  eval prune_statement "$IGNOREDIRS"
}

function cleanup ()                 # $@: files to delete
{
  trap "" SIGINT
  echo "interrupted!"
  rm -f "$@"
  exit 1
}

function noregex ()                 # $1: pattern
{
  echo $E ".$1" | sed -e "s:^.::" -e 's:[][^$\\.*]:\\&:g'
}

function lddlib ()                  # $1: script, $2: search pattern, $3: ANYNAME
{
  if [ -z "$2" ]; then
    HEADER="Use of libraries found in \`$3'."
  else
    HEADER="Use of \`$2' found in \`$3'."
  fi

  NOREGEX=$(noregex "$2")
  FILE=""

  grep "^. *rm -f " "$1" | extract_filenames "^. *rm -f \+" |
  grep -v "^ /dev/\|^ /proc/" | tr "\n" "\0" |
  xargs -0re sh -c 'eval ldd "$@"' -- /dev/null 2> /dev/null |
  grep $FINDCASE "^[^$WSPACE]\|$NOREGEX" |

  while read -r; do
    case "$REPLY" in
      [^$WSPACE]*)  FILE=$REPLY
                    ;;
      *)            [ "$HEADER" ] && echo && echo $E "$HEADER" && HEADER=""
                    [ "$FILE" ] && echo $E "$FILE" && FILE=""
                    echo $E "$REPLY"
                    ;;
    esac
  done
}

function dependencies ()            # $1: search pattern
{
  NOREGEX=$(noregex "$1")
  if [ "$1" ]; then
    NOREGEX="$NOREGEX\$"
  fi

  MAGIC="#: \($DEPEND\|$USE_OF\|$USED_BY\): "

  cd "$SCRIPTPATH" || return 1
#PGR-16d: -maxdepth order problem with some versions of find
#  find -maxdepth 1 -type f -perm +u+x -printf "%P\0" |
#PGR-17c: syntax for -perm changed
  find -maxdepth 1 -type f -perm /u+x -printf "%P\0" |
  xargs -0re grep "^${MAGIC}${NOREGEX}" /dev/null |
  # change all question marks in front of :$MAGIC and
  # those in all lines not containing :$MAGIC to //Q
  # (which protects them against restore_filename)
  # then extract $DEPEND, $USE_OF or $USED_BY from :$MAGIC
  sed -e ":loop" -e "s;?\(.*:$MAGIC\);//Q\1;" -e "t loop" \
      -e "/:$MAGIC/ !s:?://Q:g" \
      -e "s;:$MAGIC; \1 ;" |
  restore_filename
}

function touch_script ()            # $1: script, $2: save/restore
{
  case $2 in
    save)     touch -r "$1" "$TEMPFILE.mtime"
              ;;
    restore)  if [ "$1" -nt "$TEMPFILE.mtime" -o \
                   "$1" -ot "$TEMPFILE.mtime" ]; then
                touch -mr "$TEMPFILE.mtime" "$1"
              fi
              ;;
  esac
}

function depend ()                  # $1: script, $2: DEPNAME, $3: string
{
  SCRIPT=$(echo $E -n "$2" | change_filenames)

  if grep -q "^#: $3: $(noregex "$SCRIPT")\$" "$1"; then
    return 0
  else
    touch_script "$1" save
    N="\\$NL"
    if grep -q "^#: $3: " "$1"; then
      MAGIC="^#: $3: "
      EXTRA=""
    elif grep -q "^#: " "$1"; then
      MAGIC="^#: "
      EXTRA=""
    else
      MAGIC="^[$WSPACE]*\$"
      EXTRA=$N
    fi

    # determine the line number behind the last $MAGIC line
    # and insert '#: $3: $SCRIPT' there
    LNR=$(sed -n "/$MAGIC/,\$ { /$MAGIC/ !{ =; q; }; }" "$1")
    sed "$((LNR)) i${N}#: $3: ${SCRIPT}${EXTRA}" "$1" > "$TEMPFILE.copy"

    if [ $? -eq 0 -a -s "$TEMPFILE.copy" ]; then
      cp "$TEMPFILE.copy" "$1"
    else
      return 1
    fi

    if [ "$PRESERVE" = yes ]; then
      touch_script "$1" restore
    fi
  fi
}

function edit_script ()             # $1: script
{
  touch_script "$1" save
  $EDITPROG "$1"
  touch_script "$1" restore
}

function no_other_pre_inst_file ()
{
#PGR-16d: -maxdepth order problem with some versions of find
  FILES=$(find "$SCRIPTPATH" -maxdepth 1 -type f -name "*.$SUFFIX")

  if [ "$FILES" ]; then
    echo "Other installation(s) still in progress:"
    cd "$SCRIPTPATH" && ls -CdbF *."$SUFFIX"
    echo -n "Continue anyway? $(novice '[no/yes]')"
    read
    if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
      return 0
    else
      return 1
    fi
  else
    return 0
  fi
}

function backup ()                  # $1: script, $2: ANYNAME
{
  if [ -e "$BACKUPPATH/$2.$BACKUPEXT" ]; then
    echo -n "Backup already exists. Ok to overwrite? $(novice '[no/yes]')"
    read
    if [ ".$REPLY" != ".y" -a ".$REPLY" != ".yes" ]; then
      echo "No, aborted."
      exit 1
    fi
  fi
# PGR passing it "/", i.e. when package contains a root-level directory,
# would cause it to backup a "null member".
#  files "$1" | $BACKUPPIPE "$BACKUPPATH/$2.$BACKUPEXT"
  files "$1" | sed -e '/^\/$/d' | $BACKUPPIPE "$BACKUPPATH/$2.$BACKUPEXT"
  echo "Done."
}

#PGR-16a: functionalized restore code
function restore ()                  # $1: script $2: ANYNAME
{
   if [ -e "$BACKUPPATH/$2.$BACKUPEXT" ]; then
     (cd / && $RESTOREPIPE "$BACKUPPATH/$2.$BACKUPEXT")
     if [ $? -eq 0 ]; then
	 echo "Files restored successfully."
       if [ -e "$1" ]; then
	 if [ ! -x "$1" ]; then
	   chmod u+x "$1"
	   echo "$1 made executable again."
	 fi
       else
	 echo "Oops! $ID $1 doesn't exist anymore."
# Yes, backup is restored even if deinstall script is missing.
       fi
     else
       echo "Restore returned error code $?"
     fi
   else
     echo "$BACKUPPATH/$2.$BACKUPEXT doesn't exist."
   fi
}

#PGR-16f: compare current files to contents of backup
function check ()
{
  create_tempdir
  mkdir $TEMPDIR/CHECK		#branch for temporary restore
  cd $TEMPDIR/CHECK
  $RESTOREPIPE "$BACKUPPATH/$1.$BACKUPEXT"
  for i in `find $TEMPDIR/CHECK -type f -exec echo {} \;`
    do
      file=${i#$TEMPDIR/CHECK}
      cmp -s $i $file || echo $file "changed."
    done
}

#
# start of main program
#

case "$SNAME" in *[^a-zA-Z0-9+,.:_-]*)
  echo "Please call me under a name only containing characters [a-zA-Z0-9+,.:_-]."
  exit 1
  ;;
esac

if echo "\t" | grep -q "t"; then
  E=""
elif echo -E "\t" | grep -q "t"; then
  E="-E"
else
  echo "Warning: Your \`echo' can't handle backslash-escaped characters literally."
  E=""
fi

trap 'trap "" SIGINT; rm -rf "$TEMPDIR"' EXIT
trap 'echo "interrupted!"; exit 1' SIGINT
create_tempdir

if [ ".$1" = ".--ls" ]; then
  chkname "$2"
  cd "$SCRIPTPATH" && $LSPROG -- "$2"*
  exit
elif [ ".$1" = ".--lsbak" ]; then
  chkname "$2"
  chkbakpath
  cd "$BACKUPPATH" && $LSPROG -- "$2"*
  exit
elif [ ".$1" = ".--lib" -a $# -le 2 ]; then
#PGR-16d: -maxdepth order problem with some versions of find
#  find "$SCRIPTPATH" -maxdepth 1 -type f -perm +u+x -print0 |
#PGR-17c: syntax for -perm changed
  find "$SCRIPTPATH" -maxdepth 1 -type f -perm /u+x -print0 |
  xargs -0re -i $SNAME {} "$@"
  exit
elif [ ".$2" = ".--lib" -a $# -le 3 ]; then
  ANYNAME=${1#"$SCRIPTPATH/"}
  chkscript "$ANYNAME"
  lddlib "$SCRIPTPATH/$ANYNAME" "$3" "$ANYNAME"
  exit
fi

case $# in
  3)  ANYNAME=${1#"$SCRIPTPATH/"}
      DEPNAME=${3#"$SCRIPTPATH/"}
      case "$2" in
        --requires)      chkscript "$ANYNAME"
                         chkscript "$DEPNAME"
                         depend "$SCRIPTPATH/$ANYNAME" "$DEPNAME" "$DEPEND"
                         exit
                         ;;
        --uses)          chkscript "$ANYNAME"
                         chkscript "$DEPNAME"
                         depend "$SCRIPTPATH/$ANYNAME" "$DEPNAME" "$USE_OF"
                         exit
                         ;;
        --supports)      chkscript "$ANYNAME"
                         chkscript "$DEPNAME"
                         depend "$SCRIPTPATH/$ANYNAME" "$DEPNAME" "$USED_BY"
                         exit
                         ;;
        *)               chkname "$1"
                         option_list "$@"
                         ;;
      esac
      ;;
  2)  ANYNAME=${2#"$SCRIPTPATH/"}
      case "$1" in
        --xcheck)  chkscript "$ANYNAME"
                   xcheck "$SCRIPTPATH/$ANYNAME" "$ANYNAME"
                   exit
                   ;;
        --remove)  chkscript "$ANYNAME"
                   rm -rf "$TEMPDIR"
                   export -n TMPDIR
                   exec "$SCRIPTPATH/$ANYNAME"
                   ;;
        --backup)  chkscript "$ANYNAME"
                   chkbakpath
                   backup "$SCRIPTPATH/$ANYNAME" "$ANYNAME"
                   exit
                   ;;
#PGR added "restore" command so we don't have to run tar from / and chmod
	--restore) chkname "$ANYNAME"
		   chkbakpath
		   restore "$SCRIPTPATH/$ANYNAME" "$ANYNAME"
		   exit
		   ;;
#PGR added "check" command to validate current files & backup
	--check)   chkname "$ANYNAME"
		   chkbakpath
                   rm -rf "$TEMPDIR"
                   export -n TMPDIR
		   check "$ANYNAME"
		   exit
		   ;;
        --list)    chkscript "$ANYNAME"
                   permissions "$SCRIPTPATH/$ANYNAME"
                   exit 0
                   ;;
        --files)   chkscript "$ANYNAME"
                   files "$SCRIPTPATH/$ANYNAME" | tr "\0" "\n"
                   exit 0
                   ;;
        --files0)  chkscript "$ANYNAME"
                   files "$SCRIPTPATH/$ANYNAME"
                   exit 0
                   ;;
        --edit)    chkscript "$ANYNAME"
                   edit_script "$SCRIPTPATH/$ANYNAME"
                   exit
                   ;;
        --find)    find_script "$2"
                   exit
                   ;;
        --dirs)    chkname "$ANYNAME"
                   dirs "$ANYNAME"
                   exit
                   ;;
        *)         echo $TRY
                   exit 1
                   ;;
      esac
      ;;
  1)  case "$1" in
        --cd)       echo "Option --cd isn't available, use \`cd $SCRIPTPATH' instead."
                    exit 1
                    ;;
        --deps)     dependencies
                    exit
                    ;;
        --dirs)     default_dirs
                    exit 0
                    ;;
        --profile)  profile
                    exit 0
                    ;;
        --help)     echo "$USAGE"
                    exit 0
                    ;;
        --version)  echo "$SNAME - $LNAME $VERSION.$SUBVER"
                    exit 0
                    ;;
        -*)         echo $TRY
                    exit 1
                    ;;
        *)          chkname "$1"
                    ;;
      esac
      ;;
  *)  case "$1" in
        -* | "")  echo $TRY
                  exit 1
                  ;;
        *)        chkname "$1"
                  option_list "$@"
                  ;;
      esac
      ;;
esac

# called first time ever
if [ ! -d "$SCRIPTPATH" ]; then
  echo -n "Ok to create script directory $SCRIPTPATH? $(novice '[no/yes]')"
  read
  if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
    mkdir -p "$SCRIPTPATH" || exit 1
  else
    echo "No, aborted."
    exit 1
  fi
fi

ANYNAME=$1

# installation in progress
if [ -f "$SCRIPTPATH/$ANYNAME.$SUFFIX" ]; then

  if [ -f "$SCRIPTPATH/$ANYNAME.$PIOOPT" ]; then
    if [ $((OPTIONS & 1)) -eq 0 ]; then
      WATCHDIRS=$(grep "^w" "$SCRIPTPATH/$ANYNAME.$PIOOPT" | sed "s:^.::")
    fi
    if [ $((OPTIONS & 2)) -eq 0 ]; then
      IGNOREDIRS=$(grep "^i" "$SCRIPTPATH/$ANYNAME.$PIOOPT" | sed "s:^.::")
    fi
  fi

  check_directories

  # ask what to do
  echo $E "Observing the installation of \`$ANYNAME'."
  REPLY=""
  until [ "$REPLY" ]; do
    echo -n "What shall $SNAME do now (s/r/f/q)? $(novice '[Press enter for help]')"
    read
    if [ ".$REPLY" != ".s" -a ".$REPLY" != ".r" -a \
         ".$REPLY" != ".f" -a ".$REPLY" != ".q" ]; then
      echo "  s ... show state of installation"
      echo "  r ... revoke installation"
      echo "  f ... finish installation"
      echo "  q ... quit"
      REPLY=""
    fi
  done

  # finish installation
  if [ "$REPLY" = "f" ]; then
    echo $E -n "Creating de-installation script \`$SCRIPTPATH/$ANYNAME'... "
    trap 'cleanup "$SCRIPTPATH/$ANYNAME"' SIGINT
    write_script "$SCRIPTPATH/$ANYNAME.$SUFFIX" "$SCRIPTPATH/$ANYNAME"
    finish_installation "$ANYNAME"

  # state of installation
  elif [ "$REPLY" = "s" ]; then
    echo $E -n "Analyzing the state of installation of \`$ANYNAME'... "
    write_script "$SCRIPTPATH/$ANYNAME.$SUFFIX" "$TEMPFILE.script" -q
    grep "^##" "$TEMPFILE.script" > "$TEMPDIR/$ANYNAME.$SOFAR"
    if [ -s "$TEMPDIR/$ANYNAME.$SOFAR" ]; then
      echo >> "$TEMPDIR/$ANYNAME.$SOFAR"
    fi
    permissions "$TEMPFILE.script" >> "$TEMPDIR/$ANYNAME.$SOFAR" 2>&1
    echo "done."
    show_result "$TEMPDIR/$ANYNAME.$SOFAR"

    # enable quick finish of installation
    if [ $? -eq 0 ]; then
      echo -n "Use state information and finish installation now? $(novice '[no/yes]')"
      read
      if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
        echo -n "State information may be out of date. Please confirm: $(novice '[no/yes]')"
        read
        if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
          echo $E -n "Creating de-installation script \`$SCRIPTPATH/$ANYNAME'... "
          trap 'cleanup "$SCRIPTPATH/$ANYNAME"' SIGINT
          mv "$TEMPFILE.script" "$SCRIPTPATH/$ANYNAME"
          trap "" SIGINT
          echo "done."
          finish_installation "$ANYNAME"
        else
          echo "No."
        fi
      else
        echo "No."
      fi
    fi

  # revoke installation
  elif [ "$REPLY" = "r" ]; then
    echo $E -n "Revoking the installation of \`$ANYNAME'... Sure? $(novice '[no/yes]')"
    read
    if [ ".$REPLY" = ".y" -o ".$REPLY" = ".yes" ]; then
      rm "$SCRIPTPATH/$ANYNAME.$SUFFIX"
      rm -f "$SCRIPTPATH/$ANYNAME.$PIOOPT"
      echo "Done."
    else
      echo "Nothing done."
      exit 1
    fi

  # quit
  else
    echo "Nothing done."
    exit 1
  fi

# new installation
else

  check_directories

  if [ -e "$SCRIPTPATH/$ANYNAME" ]; then
    echo $E "\`$ANYNAME' seems to be installed."
    echo "What about removing the script first?"
    exit 1
  elif no_other_pre_inst_file; then
    echo $E -n "Preparing to observe the installation of \`$ANYNAME'... "
    trap 'cleanup "$SCRIPTPATH/$ANYNAME".{"$SUFFIX","$PIOOPT"}' SIGINT
    find_filenames | sort > "$SCRIPTPATH/$ANYNAME.$SUFFIX"
    if [ $OPTIONS -ne 0 ]; then
      echo $E "$WATCHDIRS" | sed "s:^:w:" > "$SCRIPTPATH/$ANYNAME.$PIOOPT"
      echo $E "$IGNOREDIRS" | sed "s:^:i:" >> "$SCRIPTPATH/$ANYNAME.$PIOOPT"
    fi
    trap "" SIGINT
    if [ -f "$SCRIPTPATH/$ANYNAME.$SUFFIX" ]; then
#PGR-16a: make sure before and after timestamps aren't the same
      sleep 2
      echo "done."
    else
      echo "failed!"
      exit 1
    fi
  else
    echo "Nothing done."
    exit 1
  fi
fi

exit 0
