#! /bin/bash
#
# cygcheck-dep
# Copyright (c) 2013-2018 Mikhail Usenko <mikeus@nm.ru>
# GNU General Public License
#
# https://github.com/mmaxs/cygcheck-dep
#


### DECLARATIONS

name="cygcheck-dep"
version="3.0"

## version(), help() /*

version()
{
  echo "$name, version $version"
}

help()
{
  version
  cat << $
Show information on dependencies for installed Cygwin packages.

Usage:
  $name [-c] [-s FILE | -S] [:setup diagnostics switches:]
  $name [-c] [-s FILE | -S] [:package information switches:]
  $name [-h | --help]
  $name [-V | --version]

Setup diagnostics switches:
  -l              Check for installed packages that are not required by any
                  other installed packages (that are "leaves" in the package
                  dependency tree). (Note: unless the -O option is specified,
                  this list also typically includes obsolete installed
                  packages for which the corresponding replacement packages
                  are installed, but <setup.exe> might not recognize such a
                  packages as "leaves".)

  -i              Check for package "islands". "An island" is a group of
                  interdependent packages that require each other (with
                  circular or all-to-all dependencies), but being taken
                  all together not needed for any other installed packages.

  -I              Show all groups of interdependent packages with circular
                  or all-to-all dependencies (including groups that are
                  not "islands"). Supersedes -i switch.

  -b              Show installed packages that have broken or unknown
                  dependencies along with lists of missing required packages
                  for them (which might include not only available but for
                  some reason not installed dependencies, but also merely
                  unknown package names).

  -m              Show missing required dependencies and installed packages
                  for which they are required.

  -o              Show installed packages that are obsoleted by available
                  replacement package.

  -x              Show extraneous installed packages that are not present in
                  Cygwin standard distribution whose dependencies can not
                  therefore be figured out from <setup.ini> file.

  -C              Check for available updates for installed packages.
                  Ignores -c.

Package information switches:
  -r PACKAGE...   Show the list of packages that are required by the installed
                  PACKAGE.

  -R PACKAGE...   Recursively resolve the list of packages that are required
                  by the installed PACKAGE.

  -n PACKAGE...   Show the list of packages that need the installed PACKAGE.

  -N PACKAGE...   Recursively resolve the list of packages that need the
                  installed PACKAGE.

  -d PACKAGE...   Show the list of packages that will become not required by
                  any other installed packages (i.e. will become "leaves"),
                  if the PACKAGE is uninstalled. Thus, these packages,
                  if being unnecessary, can be selected for uninstalling
                  in <setup.exe> together with the PACKAGE.

  -D PACKAGE...   The recursive version of -d switch.

Run mode options:
  -c              Normally every time $name runs, it downloads latest
                  Cygwin package database file <setup.ini> with information
                  on package dependencies from Cygwin's ftp site. This option
                  prevents from unnecessary downloads and forces to use the
                  file cached at the previous run.

  -s FILE         Normally $name shows information on the installed
                  packages for the Cygwin installation you are working in (the
                  list of installed packages is /etc/setup/installed.db).
                  With this option you can specify a different database FILE
                  of the installed packages (e.g. the file from another Cygwin
                  installation on your system).

  -O              Respect obsolete packages:
                    - when resolving package dependencies, do not replace
                      obsolete packages in dependency lists with the
                      corresponding replacement packages;
                    - for installed obsolete packages do not use replacement
                      packages as their required dependencies; instead, use
                      their own normal dependencies;
                    - for the -S switch, treat obsolete packages as having
                      been also installed.

<setup.ini> file diagnostics switches:
  -S              Instead of checking through only installed packages, check
                  through the list of all available packages from <setup.ini>
                  file. In other words, all the available packages from the
                  package database (excluding packages from the category
                  '_obsolete', unless the -O option is specified) will be
                  treated as if they where installed. Supersedes -s option.

Output modifiers:
  -p              While printing package names prefix them with status marks.
                  Status marks are:
                    ?? - unknown package name,
                    ?  - extraneous installed package,
                    &  - obsolete installed package,
                    !  - installed package with broken dependencies,
                    *  - not installed package (e.g. a broken dependency);
                  (! and & marks might be juxtaposed).

  -q              Be more quiet: suppress any advance warnings.

  -v              Be more verbose: turn on Wget's output to /dev/stderr while
                  downloading <setup.ini> file.

Other switches:
  -h, --help      Print this help and exit.
  -V, --version   Print the program version and exit.
$
}
## */

## global variables /*

# ${PkgID[$name]}
#     - associative array to be used as a map (package $name) -> (package $id)
# ${PkgName[$id]}
#     - indexed array for the reverse map (package $id) -> (package $name)
#
# the above arrays serve as a global catalogue of all package names encountered
# while processing entries read form <setup.ini> and <installed.db> files
#
# $MaxPkgID
#     - track the current maximum package ID,
#       it is always = ${#PkgID[*]} = ${#PkgName[*]}
declare -A PkgID
declare -a PkgName
declare -i MaxPkgID=0

# ${PkgCategories[$id]}
#     - indexed array, the list of categories which the package $id belongs to
declare -a PkgCategories

# ${PkgVersionAvailable[$id]}
#     - indexed array, the version of the package $id from <setup.ini> file
declare -a PkgVersionAvailable PkgVersionInstalled

# ${PkgRequisites[$id]}
#     - indexed array, array element is a $'\n'-dlimited list of package IDs
#       that are directly required by the package $id
# ${PkgRequisites_R[$id]}
#     - indexed array, array element is a recursively resolved $'\n'-dlimited list
#       of package IDs that are directly and indirectly required by the package $id
declare -a PkgRequisitesDeclared PkgRequisites PkgRequisites_R
declare -a  PkgBrokenRequisites MissingPkgs

# ${PkgObsoletedPkgs[$id]}
declare -a PkgObsoletedPkgs

# ${PkgDependants[$id]}
#     - indexed array, array element is a $'\n'-dlimited list of package IDs
#       that directly depend on the package $id
# ${PkgDependants_R[$id]}
#     - indexed array, array element is a recursively resolved $'\n'-dlimited list
#       of package IDs that directly and indirectly depend on the package $id
declare -a PkgDependants PkgDependants_R

# ${InstalledPkgs[$id]}
#     - indexed array to be used as a set of all installed package IDs, InstalledPkgs[$id] = $name
# ${ExtraneousPkgs[$id]}
#     - indexed array to store extraneous installed package IDs, ExtraneousPkgs[$id] = $name
declare -a InstalledPkgs ExtraneousPkgs

# ${ObsoletePkgs[$id]}
#     - indexed array to store IDs of obsolete packages, ObsoletePkgs[$id] = (replacement package IDs)
declare -a ObsoletePkgs

# ${UnknownPkgs[$id]}
#     - indexed array to store packages ID that have unknown names,
#       UnknownPkgs[$id] = (where the unknown name has appeared)
declare -a UnknownPkgs

## */

## __squeeze(), __make_list(), __find_in_list(), __remove_from_list() /*

# [in/out]  $1 - reference (variable name) to string being processing
# [in]      $2 - char for squeezing
__squeeze()
{
  local -n string_="$1"
#  local t="$string_"
#  string_="${t//$2$2/$2}"
#  while [ "$string_" != "$t" ]; do
#    t="$string_"
#    string_="${t//$2$2/$2}"
#  done
  eval "
    local t$1=\"\$string_\"
    string_=\"\${t$1//\$2\$2/\$2}\"
    while [ \"\$string_\" != \"\${t$1}\" ]; do
      t$1=\"\$string_\"
      string_=\"\${t$1//\$2\$2/\$2}\"
    done
  "
}

# [in/out]  $1 - reference (variable name) to the string with resulting list
# [in]      $2 - string with source list
# [opt]     $3 - source list delimiter, a space by default
__make_list()
{
  local -n list_="$1"
#  local t="${2//${3:- }/$'\n'}"
#  [ "$t" = "$list_" ] && return
#  t="${t// }"
#  __squeeze t $'\n'
#  t="${t#$'\n'}"
#  t="${t%$'\n'}"
#  list_="$t"
  eval "
    local t$1=\"\${2//\${3:- }/\$'\n'}\"
    [ \"\${t$1}\" = \"\$list_\" ] && return
    t$1=\"\${t$1// }\"
    __squeeze t$1 \$'\n'
    t$1=\"\${t$1#\$'\n'}\"
    t$1=\"\${t$1%\$'\n'}\"
    list_=\"\${t$1}\"
  "
}

# [in]  $1 - string with the list to be searched
# [in]  $2 - sought item
# [opt] $3 - list delimiter, default to $'\n'
__find_in_list()
{
  local d="${3:-$'\n'}"
  [ "$2" ] && [[ "$1" =~ (^|"$d")"$2"("$d"|$) ]]
}
# [in]  $1 - string with the list to be searched
# [in]  $2 - string with the list of sought items
# [opt] $3 - list delimiter, default to $'\n'
__find_any_in_list()
{
  local f t d="${3:-$'\n'}" IFS0="$IFS"
  IFS="$d"
  for t in $2; do
    [ "$t" ] && if [[ "$1" =~ (^|"$d")"$t"("$d"|$) ]]; then
      f="yes"
      break
    fi
  done
  IFS="$IFS0"
  test "$f"
}
# [in]  $1 - string with the list to be searched
# [in]  $2 - string with the list of sought items
# [opt] $3 - list delimiter, default to $'\n'
__find_all_in_list()
{
  local f t d="${3:-$'\n'}" IFS0="$IFS"
  IFS="$d"
  for t in $2; do
    [ "$t" ] && if [[ "$1" =~ (^|"$d")"$t"("$d"|$) ]]; then
      f="yes"
    else
      f=''
      break
    fi
  done
  IFS="$IFS0"
  test "$f"
}

# [in/out]  $1 - reference (variable name) to the string with the list to be modified
# [in]      $2 - string with list of items to be removed
# [opt]     $3 - list delimiter, $'\n' by default
__remove_from_list()
{
  local -n list_="$1"
#  local t d="${3:-$'\n'}" IFS0="$IFS"
#  list_="$d$list_$d"
#  IFS="$d"
#  for t in $2; do list_="${list_//$d$t$d/$d}"; done
#  IFS="$IFS0"
#  list_="${list_#$d}"
#  list_="${list_%$d}"
  eval "
    local t$1 d$1=\"\${3:-\$'\n'}\" IFS0$1=\"\$IFS\"
    list_=\"\${d$1}\$list_\${d$1}\"
    IFS=\"\${d$1}\"
    for t$1 in \$2; do list_=\"\${list_//\${d$1}\${t$1}\${d$1}/\${d$1}}\"; done
    IFS=\"\${IFS0$1}\"
    list_=\"\${list_#\${d$1}}\"
    list_=\"\${list_%\${d$1}}\"
  "
}

## */

## nout() /*
nout()
{
  local id m=''
  local -a p

  for id in $*; do p["$id"]="$id"; done

  for id in "${!p[@]}"; do
    if [ "$opt_use_prefix" ]; then
      m=''
      if [ "${UnknownPkgs[$id]}" ]; then
        m+='??'
      elif [ "${InstalledPkgs[$id]}" ]; then
        if [ "${ExtraneousPkgs[$id]}" ]; then
          m+='?'
        else
          [ "${PkgBrokenRequisites[$id]}" ] && m+='!'
          [ "${ObsoletePkgs[$id]}" ] && m+='&'
        fi
      else
        m+='*'
      fi
    fi
    echo -n " $m${PkgName[$id]}"
  done
}
## */

## add_new_package_to_catalogue() /*
add_new_package_to_catalogue()
{
  let ++MaxPkgID
  PkgID["$1"]="$MaxPkgID"
  PkgName[$MaxPkgID]="$1"
}
## */

## resolve_package_name_list() /*
# [in/out]  $1 - reference (variable name) to resulting list of package IDs
# [in]      $2 - string with ${IFS}-delimited source list of package names
# [opt]     $3 - source list description
# [side]    add unknown package names to the catalogue and to ${UnknownPkgs[$id]}
resolve_package_name_list()
{
  local -n list_="$1"
  list_=''
#  local name id
#  for name in $2; do
#    id="${PkgID[$name]}"
#    if ! [ "$id" ]; then
#      add_new_package_to_catalogue "$name"
#      UnknownPkgs[$MaxPkgID]="${3:-$name}"
#      id="$MaxPkgID"
#    fi
#    list_+=$'\n'"$id"
#  done
  eval "
    local name$1 id$1
    for name$1 in \$2; do
      id$1=\"\${PkgID[\${name$1}]}\"
      if ! [ \"\${id$1}\" ]; then
        add_new_package_to_catalogue \"\${name$1}\"
        UnknownPkgs[\$MaxPkgID]=\"\${3:-\${name$1}}\"
        id$1=\"\$MaxPkgID\"
      fi
      list_+=\$'\n'\"\${id$1}\"
    done
  "
  list_="${list_#$'\n'}"
}
## */

## are_all_installed(), filter_not_installed() /*
are_all_installed()
{
  local id
  for id in $*; do
    [ "${InstalledPkgs[$id]}" ] || return 1
  done
  return 0
}

filter_not_installed()
{
  local -n list_="$1"
  list_=''
#  local id
#  for id in ${*:2}; do
#    [ "${InstalledPkgs[$id]}" ] || list_=$'\n'"$id"
#  done
  eval "
    local id$1
    for id$1 in \${*:2}; do
      [ \"\${InstalledPkgs[\${id$1}]}\" ] || list_=\$'\n'\"\${id$1}\"
    done
  "
  list_="${list_#$'\n'}"
}
## */

## resolve_PkgRequisites_R(), resolve_PkgDependants_R() /*
resolve_PkgRequisites_R()
{
  local id IFS0="$IFS"
  local -a result

  resolve()
  {
    local p t
    for p in ${PkgRequisites["$1"]}; do
      [ "${result[$p]}" ] && continue
      result["$p"]="$p"
      if [ "${PkgRequisites_R[$p]+set}" ]; then
        for t in ${PkgRequisites_R["$p"]}; do result["$t"]="$t"; done
      else
        resolve "$p"
      fi
    done
  }

  for id in $*; do
    result=()
    resolve "$id"
    IFS=$'\n'
    PkgRequisites_R["$id"]="${!result[*]}"
    IFS="$IFS0"
  done
}

resolve_PkgDependants_R()
{
  local id IFS0="$IFS"
  local -a result

  resolve()
  {
    local p t
    for p in ${PkgDependants["$1"]}; do
      [ "${result[$p]}" ] && continue
      result["$p"]="$p"
      if [ "${PkgDependants_R[$p]+set}" ]; then
        for t in ${PkgDependants_R["$p"]}; do result["$t"]="$t"; done
      else
        resolve "$p"
      fi
    done
  }

  for id in $*; do
    result=()
    resolve "$id"
    IFS=$'\n'
    PkgDependants_R["$id"]="${!result[*]}"
    IFS="$IFS0"
  done
}
## */


### SCRIPT

## parse command-line options /*
cmd_show_leaves=''
cmd_show_islands=''
cmd_show_interdependent=''
cmd_show_broken=''
cmd_show_missing=''
cmd_show_unknown=''
cmd_show_obsoleted=''
cmd_show_extraneous=''
cmd_show_updates=''
cmd_show_required=''
cmd_show_required_recursive=''
cmd_show_dependent=''
cmd_show_dependent_recursive=''
cmd_show_new_leaves=''
cmd_show_new_leaves_recursive=''
cmds=''
opt_use_cached_setup_ini=''
opt_use_custom_installed_db=''
opt_treat_setup_ini_as_installed=''
opt_respect_obsoleted=''
opt_use_prefix=''
opt_be_more_quiet=''
opt_be_more_verbose=''

while getopts "liIbmMoxCrRnNdDcs:SOpqv-:hV" OPT; do
  case "$OPT" in
    l)  cmd_show_leaves="{cmd_show_leaves}"
        ;;
    i)  cmd_show_islands="{cmd_show_islands}"
        ;;
    I)  cmd_show_interdependent="{cmd_show_interdependent}"
        ;;
    b)  cmd_show_broken="{cmd_show_broken}"
        ;;
    m)  cmd_show_missing="{cmd_show_missing}"
        ;;
    M)  cmd_show_unknown="{cmd_show_unknown}"
        ;;
    o)  cmd_show_obsoleted="{cmd_show_obsoleted}"
        ;;
    x)  cmd_show_extraneous="{cmd_show_extraneous}"
        ;;
    C)  cmd_show_updates="{cmd_show_updates}"
        ;;
    r)  cmd_show_required="{cmd_show_required}"
        ;;
    R)  cmd_show_required_recursive="{cmd_show_required_recursive}"
        ;;
    n)  cmd_show_dependent="{cmd_show_dependent}"
        ;;
    N)  cmd_show_dependent_recursive="{cmd_show_dependent_recursive}"
        ;;
    d)  cmd_show_new_leaves="{cmd_show_new_leaves}"
        ;;
    D)  cmd_show_new_leaves_recursive="{cmd_show_new_leaves_recursive}"
        ;;
    c)  opt_use_cached_setup_ini="yes"
        ;;
    s)  opt_use_custom_installed_db="$OPTARG"
        ;;
    S)  opt_treat_setup_ini_as_installed="yes"
        ;;
    O)  opt_respect_obsoleted="yes"
        ;;
    p)  opt_use_prefix="yes"
        ;;
    q)  opt_be_more_quiet="yes"
        ;;
    v)  opt_be_more_verbose="yes"
        ;;
    -)  case "$OPTARG" in
             help)  help
                    exit 0
                    ;;
          version)  version
                    exit 0
                    ;;
                *)  echo >&2 "$0: illegal option -- -$OPTARG"
                    exit 1;
                    ;;
        esac
        ;;
    h)  help
        exit 0
        ;;
    V)  version
        exit 0;
        ;;
   \?)  exit 1
        ;;
  esac
done
## */

eval cmds=\""$(IFS='$'; echo "\$${!cmd_*}")"\"
[ "$cmds" ] || exit

## check for the cache directory /*
cache_dir="/var/cache/$name"
[ -d "$cache_dir" ] ||
if ! mkdir -p "$cache_dir"; then
  echo >&2 "$0: unable to create cache directory:"
  echo >&2 "$0:   $cache_dir"
  exit 2
fi
## */

## check for the <installed.db> file /*
installed_db="${opt_use_custom_installed_db:-/etc/setup/installed.db}"
[ "$opt_treat_setup_ini_as_installed" ] ||
if ! [ -r "$installed_db" ]; then
  echo >&2 "$0: <installed.db> file is not exist or is not readable:"
  echo >&2 "$0:   $installed_db"
  exit 3
fi
## */

## identify the host architecture /*
# mach="$(uname -m)"
mach="${MACHTYPE%%-*}"
[ "$mach" = "x86_64" ] || mach="x86"
## */

## download <setup.ini> file /*
cw_setup_ini="$cache_dir/cygwin/$mach/setup.ini"
if ! [ "$opt_use_cached_setup_ini" ] || [ "$cmd_show_updates" ]; then
  cw_setup_bz2_url="ftp://sourceware.org/pub/cygwin/$mach/setup.bz2"
  [ "$opt_be_more_verbose" ] && opt_wget_verbosity="-nv" || opt_wget_verbosity="-q"
  if ! wget >&2 "$opt_wget_verbosity" -r -nH --cut-dirs 1 -P "$cache_dir" "$cw_setup_bz2_url"; then
    echo >&2 "$0: failed to download <setup.ini> file:"
    echo >&2 "$0:   $cw_setup_bz2_url"
    echo >&2 "$0: you may try to run with -c option to use cached file from previous download"
    exit 4
  fi
  cw_setup_bz2="$cache_dir/cygwin/$mach/setup.bz2"
  if ! bzip2 -t "$cw_setup_bz2"; then
    echo >&2 "$0: failed to check integrity of downloaded <setup.ini> file:"
    echo >&2 "$0:   $cw_setup_bz2"
    echo >&2 "$0: you may try to run with -c option to use cached file from previous download"
    exit 5
  fi
  if ! bzcat "$cw_setup_bz2" > "$cw_setup_ini"; then
    echo >&2 "$0: failed to decompress downloaded <setup.ini> file:"
    echo >&2 "$0:   $cw_setup_bz2"
    exit 6
  fi
fi

if ! [ -r "$cw_setup_ini" ]; then
  echo >&2 "$0: <setup.ini> file is not exist or is not readable:"
  echo >&2 "$0:   $cw_setup_ini"
  [ "$opt_use_cached_setup_ini" ] &&
  echo >&2 "$0: you may try to run without -c option to download the file"
  exit 7
fi
## */

# $L $P $C $R $V $O $D $T - temporary variables
L=''; P=''; C=''; R=''; V=''; O=''; D=''; T=''

## read and parse <setup.ini> file; populate ${PkgID[]}, ${PkgName[]}, et al. /*
L="$(sed -n '
    :begin
    /^@\s\+\(\S\+\)\s*$/{s//add_new_package_to_catalogue "\1"/p; b}
    /^category:\s\+\(_obsolete\)\s*$/{s//PkgCategories[$MaxPkgID]="\1"; T="obsolete package"/p; b}
    /^category:\s\+\(\S\+\(\s\+\S\+\)*\)\s*$/{s//__make_list C "\1"; PkgCategories[$MaxPkgID]="$C"; T=''/p; b}
    /^version:\s\+\(\S\+\)\s*$/{s//PkgVersionAvailable[$MaxPkgID]="\1"/p; b}
    /^obsoletes:\s\+\(\S\+\(\s\+\S\+\)*\)\s*$/{s//__make_list O "\1" ","; PkgObsoletedPkgs[$MaxPkgID]="$O"/p; b}
    /^requires:\s\+\(\S\+\(\s\+\S\+\)*\)\s*$/{s//[ "$T" ] \&\& { __make_list R "\1"; PkgRequisitesDeclared[$MaxPkgID]="$R"; }/p; b}
    /^depends2:\s\+\(\S\+\(\s\+\S\+\)*\)\s*$/{s//__make_list R "\1" ","; [ "$T" ] \&\& ObsoletePkgs[$MaxPkgID]="$R" || PkgRequisitesDeclared[$MaxPkgID]="$R"/p; b}
    /^\[prev\]/{:skip-prev n; /^@/b begin; b skip-prev}
    /^\[test\]/{:skip-test n; /^@/b begin; b skip-test}
' "$cw_setup_ini")"
eval "$L"
L=''

## check the results of reading/parsing <setup.ini> file /*
if [ "$(sed -n '/^@ /p' "$cw_setup_ini" | wc -l)" != "$MaxPkgID" ]; then
  echo >&2 "$0: failed to parse data from <setup.ini> file:"
  exit 8
fi
if [ "${#PkgCategories[@]}" != "${#PkgName[@]}" ] && ! [ "$opt_be_more_quiet" ]; then
  echo >&2    "# $0:"
  echo >&2    "# warning: not available package category:"
  echo >&2    "#   failed to parse <setup.ini> file record of"
  echo >&2    "#   'category:' for the following packages:"
  echo >&2 -n "#("
  for id in "${!PkgName[@]}"; do [ "${PkgCategories[$id]}" ] || echo >&2 -n " ${PkgName[$id]}"; done
  echo >&2    " )"
  echo >&2
fi
if [ "${#PkgVersionAvailable[@]}" != "${#PkgName[@]}" ] && ! [ "$opt_be_more_quiet" ]; then
  echo >&2    "# $0:"
  echo >&2    "# warning: not available package version:"
  echo >&2    "#   failed to parse <setup.ini> file record of"
  echo >&2    "#   'version:' for the following packages:"
  echo >&2 -n "#("
  for id in "${!PkgName[@]}"; do [ "${PkgVersionAvailable[$id]}" ] || echo >&2 -n " ${PkgName[$id]}"; done
  echo >&2    " )"
  echo >&2
fi
## */
## */

if [ "$opt_treat_setup_ini_as_installed" ]; then
## populate ${InstalledPkgs[]} using data from ${PkgName[]} /*
  L="${PkgName[@]@A}"
  L="${L/ PkgName=/ InstalledPkgs=}"
  eval "$L"

  L="${PkgVersionAvailable[@]@A}"
  L="${L/ PkgVersionAvailable=/ PkgVersionInstalled=}"
  eval "$L"
  L=''

  if [ "${#InstalledPkgs[@]}" -ne "$MaxPkgID" ] || [ "${InstalledPkgs[0]+index is set}" ]; then
    echo >&2 "$0: internal error (9)"
    exit 9
  fi

  [ "$opt_respect_obsoleted" ] ||
  for id in "${!InstalledPkgs[@]}"; do
    [ "${PkgCategories[$id]}" = "_obsolete" ] && unset -v InstalledPkgs\["$id"\]
  done
## */
else
## read and parse <installed.db> file; populate ${InstalledPkgs[]}, ${PkgVersionInstalled[]} /*
  while read -r P F _ || [ "$P" ]; do
    V="${F#$P-}"; V="${V%.tar*}"

    id="${PkgID[$P]}"
    if ! [ "$id" ]; then
      add_new_package_to_catalogue "$P"
      ExtraneousPkgs[$MaxPkgID]="$P"
      id="$MaxPkgID"
    fi

    InstalledPkgs["$id"]="${PkgName[$id]}"
    PkgVersionInstalled["$id"]="$V"
  done < <(sed '1d; /^\s*$/d' "$installed_db")
## */
fi

## print advance warning about extraneous installed packages /*
[ "$opt_be_more_quiet" ] ||
if [ "${#ExtraneousPkgs[@]}" -ne 0 ] && ! [ "$cmd_show_extraneous" ]; then
  echo >&2    "# $0:"
  echo >&2    "# warning: extraneous installed packages with unknown dependencies:"
  echo >&2    "#   the following installed packages are not present"
  echo >&2    "#   in Cygwin standard distribution:"
  echo >&2 -n "#("
  nout >&2 "${!ExtraneousPkgs[@]}"
  echo >&2    " )"
  echo >&2
fi
## */

## $cmd_show_extraneous /*
if [ "$cmd_show_extraneous" ]; then
  if [ "${#ExtraneousPkgs[@]}" -ne 0 ]; then
    for id in "${!ExtraneousPkgs[@]}"; do
      nout "$id"; echo ": extraneous installed package with unknown dependencies"
    done
    echo
  fi

  cmds="${cmds//$cmd_show_extraneous}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_updates /*
if [ "$cmd_show_updates" ]; then
  T=''
  for id in "${!InstalledPkgs[@]}"; do
    [ "${ExtraneousPkgs[$id]}" ] && continue
    C="${PkgVersionInstalled[$id]}"
    V="${PkgVersionAvailable[$id]}"
    if [ "$V" ]; then
      if [ "$C" \< "$V" ]; then
        nout "$id"; echo "-$C: new version ( $V )"
        T="command output"
      fi
    else
      nout "$id"; echo "-$C: information about the new version is not available"
      T="command output"
    fi
  done
  [ "$T" ] && echo

  cmds="${cmds//$cmd_show_updates}"
  [ "$cmds" ] || exit
fi
## */

## convert name lists in ${ObsoletePkgs[]} to package ID lists /*
for id in "${!ObsoletePkgs[@]}"; do
  O="${ObsoletePkgs[$id]}"
  resolve_package_name_list O "$O" "in <setup.ini> file record of 'depends2:' for package ${PkgName[$id]}"
  ObsoletePkgs["$id"]="$O"
done
## */
## convert name lists in ${PkgObsoletedPkgs[]} to package ID lists, add obsoleted packages to ${ObsoletePkgs[]} /*
for id in "${!PkgObsoletedPkgs[@]}"; do
  O="${PkgObsoletedPkgs[$id]}"
  resolve_package_name_list O "$O" "in <setup.ini> file record of 'obsoletes:' for package ${PkgName[$id]}"
  PkgObsoletedPkgs["$id"]="$O"

  # add each obsoleted package and its replacement into ${ObsoletePkgs[]} if they are not there yet
  for P in $O; do
    R="${ObsoletePkgs[$P]}"
    if [ "$R" ]; then
      __find_in_list "$R" "$id" || ObsoletePkgs["$P"]+=$'\n'"$id"
    else
      ObsoletePkgs["$P"]="$id"
    fi
  done
done
## */

## determine requisites for installed packages /*
L="${!InstalledPkgs[*]}"
while [ "$L" ]; do
  C=''

  for id in $L; do
    # convert name lists in ${PkgRequisitesDeclared[$id]} to package ID lists
    R="${PkgRequisitesDeclared[$id]}"
    if [ "$R" ]; then
      resolve_package_name_list R "$R" "in <setup.ini> file record of 'requires:' for package ${PkgName[$id]}"
      PkgRequisitesDeclared["$id"]="$R"
    fi

    # choose the appropriate list of requisites
    O="${ObsoletePkgs[$id]}"
    if [ "$O" ]; then
      if [ "$opt_respect_obsoleted" ]; then
        R="${PkgRequisitesDeclared[$id]}"
        __remove_from_list R "$O"
      else
        R="$O"
      fi
    else
      R="${PkgRequisitesDeclared[$id]}"
    fi

    [ "$R" ] || continue

    # examine each package in the requisite list
    for P in $R; do
      O="${ObsoletePkgs[$P]}"
      if [ "$O" ] && ! [ "$opt_respect_obsoleted" ]; then
        PkgRequisites["$id"]+=$'\n'"$O"
      else
        PkgRequisites["$id"]+=$'\n'"$P"
      fi
    done

    # the resulting list
    PkgRequisites["$id"]="${PkgRequisites[$id]#$'\n'}"

    # detect broken dependencies
    filter_not_installed D "${PkgRequisites[$id]}"
    if [ "$D" ]; then
      PkgBrokenRequisites["$id"]="$D"
      for P in $D; do
        if [ "${MissingPkgs[$P]}" ]; then
          MissingPkgs["$P"]+=$'\n'"$id"
        else
          MissingPkgs["$P"]="$id"
          C+=" $P"
        fi
      done
    fi
  done

  L="$C"
done
## */

## print advance warning about unknown package names /*
[ "$opt_be_more_quiet" ] ||
if [ "${#UnknownPkgs[@]}" -ne 0 ] && ! [ "$cmd_show_unknown" ]; then
  echo >&2    "# $0:"
  echo >&2    "# warning: unknown package names:"
  echo >&2    "#   the following encountered package names does not match"
  echo >&2    "#   any available or installed package (use -M switch to get details):"
  echo >&2 -n "#("
  nout >&2 "${!UnknownPkgs[@]}"
  echo >&2    ")"
  echo >&2
fi
## */

## $cmd_show_unknown /*
if [ "$cmd_show_unknown" ]; then
  if [ "${#UnknownPkgs[@]}" -ne 0 ]; then
    for id in "${!UnknownPkgs[@]}"; do
      nout "$id"; echo -n ": unknown package name ${UnknownPkgs[$id]}"
    done
    echo
  fi

  cmds="${cmds//$cmd_show_unknown}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_obsoleted /*
if [ "$cmd_show_obsoleted" ]; then
  T=''
  for id in "${!InstalledPkgs[@]}"; do
    O="${ObsoletePkgs[$id]}"
    if [ "$O" ]; then
      nout "$id"; echo -n ": is obsoleted by ("; nout "$O"; echo " )"
      T="command output"
    fi
  done
  [ "$T" ] && echo

  cmds="${cmds//$cmd_show_obsoleted}"
  [ "$cmds" ] || exit
fi
## */

## print advance warning about missing required dependencies /*
[ "$opt_be_more_quiet" ] ||
if [ "${#MissingPkgs[@]}" -ne 0 ] && ! [ "$cmd_show_missing" ]; then
  echo >&2    "# $0:"
  echo >&2    "# warning: missing required dependencies:"
  echo >&2    "#   the following packages are not installed, but are required as dependencies"
  echo >&2    "#   for some installed packages (use -m switch to get details):"
  echo >&2 -n "#("
  nout >&2 "${!MissingPkgs[@]}"
  echo >&2    " )"
  echo >&2
fi
## */

## $cmd_show_broken /*
if [ "$cmd_show_broken" ]; then
  if [ "${#PkgBrokenRequisites[@]}" -ne 0 ]; then
    for id in "${!PkgBrokenRequisites[@]}"; do
      nout "$id"; echo -n ": package with broken dependencies ("; nout "${PkgBrokenRequisites[$id]}"; echo " )"
    done
    echo
  fi

  cmds="${cmds//$cmd_show_broken}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_missing /*
if [ "$cmd_show_missing" ]; then
  if [ "${#MissingPkgs[@]}" -ne 0 ]; then
    for id in "${!MissingPkgs[@]}"; do
      nout "$id"; echo -n ": missing required dependency for ("; nout "${MissingPkgs[$id]}"; echo " )"
    done
    echo
  fi

  cmds="${cmds//$cmd_show_missing}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_islands, $cmd_show_interdependent /*
if [ "$cmd_show_islands" ] || [ "$cmd_show_interdependent" ]; then
  resolve_PkgRequisites_R "$(
      for id in "${!InstalledPkgs[@]}" "${!MissingPkgs[@]}"; do
        echo "$id"$'\n'"${PkgRequisites[$id]}"
      done | sed '/^$/d' | sort -n | uniq -c | sort -nr | sed 's/^\s\+[0-9]\+\s\+//'
  )"
  declare -A RR
  for id in "${!PkgRequisites_R[@]}"; do
    R="${PkgRequisites_R[$id]}"
    if __find_in_list "$R" "$id"; then
      RR["$R"]+=$'\n'"$id"
    fi
  done

  if [ "${#RR[@]}" -ne 0 ]; then
    if [ "$cmd_show_interdependent" ]; then
      for V in "${RR[@]}"; do
        echo -n " ("; nout "$V"; echo " )"
      done
    else
      for V in "${RR[@]}"; do
      (
        for id in $V; do unset -v PkgRequisites\["$id"\]; done
        IFS=$'\n'
        __find_any_in_list "${PkgRequisites[*]}" "$V" && continue
        echo -n " ("; nout "$V"; echo " )"
      )
      done
    fi | sort
    echo
  fi

  cmds="${cmds//$cmd_show_islands}"
  cmds="${cmds//$cmd_show_interdependent}"
  [ "$cmds" ] || exit
fi
## */

if [ "$cmd_show_leaves" ] ||
   [ "$cmd_show_dependent" ] ||
   [ "$cmd_show_dependent_recursive" ] ||
   [ "$cmd_show_new_leaves" ] ||
   [ "$cmd_show_new_leaves_recursive" ]; then
## determine dependent packages for installed and missing packages /*
  for id in "${!InstalledPkgs[@]}" "${!MissingPkgs[@]}"; do
    for P in ${PkgRequisites["$id"]}; do
      if [ "${PkgDependants[$P]}" ]; then
        PkgDependants["$P"]+=$'\n'"$id"
      else
        PkgDependants["$P"]="$id"
      fi
    done
  done
## */
fi

## $cmd_show_leaves /*
if [ "$cmd_show_leaves" ]; then
  T=''
  for id in "${!InstalledPkgs[@]}"; do
    if ! [ "${PkgDependants[$id]}" ]; then
      nout "$id"; __find_in_list "${PkgCategories[$id]}" "Base" && echo " [Base]" || echo
      T="command output"
    fi
  done
  [ "$T" ] && echo

  cmds="${cmds//$cmd_show_leaves}"
  [ "$cmds" ] || exit
fi
## */

queried_ids=''
resolve_package_name_list L "${*:$OPTIND}" "in command line arguments"
for id in $L; do
  if [ "${InstalledPkgs[$id]}" ]; then
    queried_ids+=" $id"
  else
    nout "$id"
    if [ "${UnknownPkgs[$id]}" ]; then
      echo ": unknown package name ${UnknownPkgs[$id]}"
    else
      echo ": package is not installed"
    fi
  fi
done
[ "$queried_ids" ] || exit

## $cmd_show_required, $cmd_show_dependent /*
if [ "$cmd_show_required" ] || [ "$cmd_show_dependent" ]; then
  for id in $queried_ids; do
    if [ "$cmd_show_required" ]; then
      nout "$id"; echo -n ": requires ("; nout "${PkgRequisites[$id]}"; echo " )"
    fi
    if [ "$cmd_show_dependent" ]; then
      nout "$id"; echo -n ": is needed for ("; nout "${PkgDependants[$id]}"; echo " )"
    fi
  done
  echo

  cmds="${cmds//$cmd_show_required}"
  cmds="${cmds//$cmd_show_dependent}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_required_recursive, $cmd_show_dependent_recursive /*
if [ "$cmd_show_required_recursive" ] || [ "$cmd_show_dependent_recursive" ]; then
  for id in $queried_ids; do
    if [ "$cmd_show_required_recursive" ]; then
      R="${PkgRequisites_R[$id]-unset}"
      if [ "$R" = "unset" ]; then
        resolve_PkgRequisites_R "$id"
        R="${PkgRequisites_R[$id]}"
      fi
      nout "$id"; echo -n ": recursively requires ("; nout "$R"; echo " )"
    fi
    if [ "$cmd_show_dependent_recursive" ]; then
      D="${PkgDependants_R[$id]-unset}"
      if [ "$D" = "unset" ]; then
        resolve_PkgDependants_R "$id"
        D="${PkgDependants_R[$id]}"
      fi
      nout "$id"; echo -n ": is recursively needed for ("; nout "$D"; echo " )"
    fi
  done
  echo

  cmds="${cmds//$cmd_show_required_recursive}"
  cmds="${cmds//$cmd_show_dependent_recursive}"
  [ "$cmds" ] || exit
fi
## */

## $cmd_show_new_leaves, $cmd_show_new_leaves_recursive /*
if [ "$cmd_show_new_leaves" ] || [ "$cmd_show_new_leaves_recursive" ]; then
  L=''
  for id in $queried_ids; do
    if [ "${PkgDependants[$id]}" ]; then
      nout "$id"; echo ": is not a leaf package"
    else
      L+=$'\n'"$id"
    fi
  done
  L="${L#$'\n'}"

  if [ "$L" ]; then
    C=''
    for id in $L; do
      for P in ${PkgRequisites["$id"]}; do
        if __find_all_in_list "$L" "${PkgDependants[$P]}"; then
          C+=$'\n'"$P"
        fi
      done
    done
    if [ "$cmd_show_new_leaves" ]; then
      echo -n " ("; nout "$L"; echo -n " ): uninstalling results in new leaves ("; nout "$C"; echo " )"
    fi

    if [ "$cmd_show_new_leaves_recursive" ]; then
      V="$C"
      while :; do
        O="$C"
        C=''
        for id in $O; do
          for P in ${PkgRequisites["$id"]}; do
            if __find_all_in_list "$L"$'\n'"$V" "${PkgDependants[$P]}"; then
              C+=$'\n'"$P"
            fi
          done
        done
        if [ "$C" ]; then
          V+=$'\n'"$C"
        else
          break
        fi
      done
      echo -n " ("; nout "$L"; echo -n " ): recursive uninstalling with leaves releases ("; nout "$V"; echo " )"
    fi
  fi
  echo

  cmds="${cmds//$cmd_show_new_leaves}"
  cmds="${cmds//$cmd_show_new_leaves_recursive}"
  [ "$cmds" ] || exit
fi
## */


# vim: et ts=2 sw=2 sts=0
# vim: fdc=4 fdl=0 fdm=marker fmr=/*,*/
