#!/usr/bin/env bash

# apt-cyg: install tool for cygwin similar to debian apt-get

# The MIT License (MIT)
# 
# Copyright (c) 2013 Trans-code Design
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# 

shopt -s extglob

# EMBED_BEGIN: hhs_embed

(( 5 <= DEBUG )) && set -x

SCRIPT_PATH="$0"
SCRIPT_FILE="${SCRIPT_PATH##*/}"
SCRIPT_NAME="${SCRIPT_FILE%.*}"
SCRIPT_DIR="${SCRIPT_PATH%/*}"
SCRIPT_REALPATH="$(readlink -f "$SCRIPT_PATH")"
SCRIPT_REALFILE="${SCRIPT_REALPATH##*/}"
SCRIPT_REALNAME="${SCRIPT_REALFILE%.*}"
SCRIPT_REALDIR="${SCRIPT_REALPATH%/*}"

# EMBED_BEGIN: hhs_sgr.bash

function init_SGR ()
{
  if [ -n "$COLORIZE" ]; then
    SGR_reset="\e[0m"
    SGR_bold="\e[1m"
    SGR_fg_red="\e[31m"
    SGR_fg_green="\e[32m"
    SGR_fg_yellow="\e[33m"
    SGR_fg_blue="\e[34m"
    SGR_fg_magenta="\e[35m"
  else
    unset SGR_reset SGR_bold SGR_red SGR_green SGR_yellow SGR_blue SGR_magenta
  fi
  FATAL_COLOR="${SGR_fg_magenta}${SGR_bold}"
  ERROR_COLOR="${SGR_fg_red}${SGR_bold}"
  WARNING_COLOR="${SGR_fg_yellow}${SGR_bold}"
  INFO_COLOR="${SGR_fg_green}${SGR_bold}"
  DEBUG_COLOR="${SGR_fg_blue}${SGR_bold}"
}

# EMBED_END: hhs_sgr.bash
# EMBED_BEGIN: hhs_debug.bash

THRESHOLD_OF_FATAL=0
THRESHOLD_OF_ERROR=1
THRESHOLD_OF_WARNING=2
THRESHOLD_OF_INFO=3
THRESHOLD_OF_DEBUG=4

function abort () #= [EXITCODE=1 [CALLSTACKSKIP=0]]
{
  dump_callstack $(( 2 + ${2:-0} ))
  exit "${1:-1}"
}

function assert_command_exists () #= [COMMAND ...]
{
  local command
  for command in $@; do
    hash $command 2>/dev/null || { error "Required command [$command] not found."; exit 1; }
  done
}

function dump_callstack () #= [N=1]
#? N is a depth to skip the callstack.
#? $CALLSTACK_SKIP is depth to skip the callstack too.
#? N and CALLSTACK_SKIP is summed before skipping.
#? The default value is N = 1 and CALLSTACK_SKIP = 0.
{
  local i
  echo "Callstack:"
  for i in `seq "$(( ${#FUNCNAME[@]} - 1 ))" -1 $(( ${1:-1} + ${CALLSTACK_SKIP:-0} ))`; do
    echo -e "\t${BASH_SOURCE[i]}: ${FUNCNAME[i]}: ${BASH_LINENO[i-1]}"
  done
} #/dump_callstack

function source_at () #= [N=1]
{
  local i=${1:-1}
  echo -e "${DEBUG_COLOR}at :${SGR_reset} ${BASH_SOURCE[i-1]}: ${FUNCNAME[i]}: ${BASH_LINENO[i-1]}"
}

function fatal () #= [MESSAGES ...]
{
  (( THRESHOLD_OF_FATAL <= ${VERBOSE:-0} )) || return 1
  echo -e "${FATAL_COLOR}Fatal:${SGR_reset} $@"
  (( THRESHOLD_OF_FATAL <= SOURCE_AT )) && source_at 2
} >&2 #/fatal

function error () #= [MESSAGES ...]
{
  (( THRESHOLD_OF_ERROR <= ${VERBOSE:-0} )) || return 1
  echo -e "${ERROR_COLOR}Error:${SGR_reset} $@"
  (( THRESHOLD_OF_ERROR <= SOURCE_AT )) && source_at 2
} >&2 #/error

function warning () #= [MESSAGES ...]
{
  (( THRESHOLD_OF_WARNING <= ${VERBOSE:-0} )) || return 1
  echo -e "${WARNING_COLOR}Warning:${SGR_reset} $@"
  (( THRESHOLD_OF_WARNING <= SOURCE_AT )) && source_at 2
} >&2 #/warning

function info () #= [MESSAGES ...]
{
  (( THRESHOLD_OF_INFO <= ${VERBOSE:-0} )) || return 1
  echo -e "${INFO_COLOR}Info:${SGR_reset} $@"
  (( THRESHOLD_OF_INFO <= SOURCE_AT )) && source_at 2
} >&2 #/info

function debug () #= [MESSAGES ...]
{
  (( THRESHOLD_OF_DEBUG <= ${VERBOSE:-0} )) || return 1
  echo -e "${DEBUG_COLOR}Debug:${SGR_reset} $@"
  (( THRESHOLD_OF_DEBUG <= SOURCE_AT )) && source_at 2
} >&2 #/debug

# EMBED_END: hhs_debug.bash

: ${VERBOSE:=$THRESHOLD_OF_WARNING}
: ${SOURCE_AT:=$THRESHOLD_OF_WARNING}
: ${COLORIZE:=1}
init_SGR

# EMBED_END: hhs_embed

TRUSTEDKEYS=( CYGWIN );
# ./pubring.asc
# ------------
# pub   4096R/E2E56300 2020-02-27 [expires: 2024-02-27]
# uid                  Cygwin <cygwin@cygwin.com>
TRUSTEDKEY_CYGWIN_SUM="019b3d2efe208f1441c660a34d08151b5c9675523b85e0a4ca748137ed45d086a4eb06e0efd4d86a1479246e62641a0c284ffb8c4b0b637063681a9282beca3d"
TRUSTEDKEY_CYGWIN_FPR="56405CF6FCC81574682A5D561A698DE9E2E56300"
TRUSTEDKEY_CYGWIN_URL_LATEST="https://cygwin.com/key/pubring.asc"

# this script requires some packages

read WGET < <( type -p wget 2>/dev/null )
read TAR  < <( type -p tar  2>/dev/null )
read GAWK < <( type -p awk  2>/dev/null )
read GPG  < <( type -p gpg2 2>/dev/null || type -p gpg   2>/dev/null )
if [ -z "$WGET" -o -z "$TAR" -o -z "$GAWK" ]; then
  echo You must install wget, tar and gawk to use apt-cyg.
  exit 1
fi

function usage()
{
  cat<<-EOD
	Usage: apt-cyg [<options>] [<subcommand> [<parameters> ...]]
	  Installs and removes Cygwin packages.
	Subcommands:
	  install <package names>  : to install packages
	  remove <package names>   : to remove packages
	  update                   : to update setup.ini
	  show                     : to show installed packages
	  find <patterns> ...      : to find packages matching patterns
	  describe <patterns> ...  : to describe packages matching patterns
	  packageof <command or file names> ... :
	                             to locate parent packages
	  pathof {cache|mirror|mirrordir|cache/mirrordir|setup.ini} :
	                             to show path
	  key-add <files> ...      : to add keys contained in <files>
	  key-del <keyids> ...     : to remove keys <keyids>
	  key-list                 : to list keys
	  key-finger               : to list fingerprints
	  upgrade-self             : to upgrade apt-cyg
	  depends <package names> ... :
	                             to show forward dependency information
	                             for packages with depth.
	  rdepends <package names> ... :
	                             to show reverse dependency information
	                             for packages with depth.
	  completion-install       : to install completion.
	  completion-uninstall     : to uninstall completion.
	  mirrors-list             : to show list of mirros.
	  mirrors-list-long        : to show list of mirros with full details.
	  mirrors-list-online      : to show list of mirros from online.
	  benchmark-mirrors <urls> ... :
	                             to benchmark mirrors.
	  benchmark-parallel-mirrors <urls> ... :
	                             to benchmark mirrors in parallel.
	  benchmark-parallel-mirrors-list :
	                             to benchmark mirrors-list in parallel.
	  scriptinfo               : to show script information.
	  show-packages-busyness <package names> ... :
	                             to show packages are busy or not.
	  dist-upgrade             : to upgrade all packages that is installed.
	                             This subcommand uses setup.exe
	  update-setup             : to update setup.exe
	  setup [<params> ...]     : to call setup.exe
	  packages-total-count     : count number of total packages from setup.ini
	  packages-total-size [<pattern of section>] :
	                             count size of total packages from setup.ini
	  packages-cached-count    : count number of cached packages
	                             in cache/mirrordir.
	  packages-cached-size     : count size of cached packages
	                             in cache/mirrordir.
	  repair-acl               : repair acl.
	  repair-postinstall       : Repair postinstall scripts.
	  source <package names> ... :
	                             download source archive.
	  mirror-source <package names> ... :
	                             download the source package
	                             into the current cache/mirrordir as mirror.
	  download <package names> ... :
	                             download the binary package
	                             into the current directory.
	  mirror <package names> ... :
	                             download the binary package
	                             into the current cache/mirrordir as mirror.
	  browse-homepage-with-mirror-source [<package names> ...] :
	                             Browse homepages of packages with mirror-source.
	  browse-homepage [<package names> ...] :
	                             Browse homepages of packages.
	  browse-summary [<package names> ...] :
	                             Browse summaries of packages.
	  listfiles [<package names> ...] :
	                             List files 'owned' by package(s).
	  get-proxy                : Get proxies for eval.
	  ls-categories            : List categories.
	  ls-pkg-with-category     : List packages with category.
	  category <category>      : List all packages in given <category>.
	  setuprc-get <section>    : Get section from 'setup.rc'.
	  set-cache [<cache>]      : Set cache.
	  set-mirror [<mirrors> ...] :
	                             Set mirror.
	                             Note: setup-x86{,_64}.exe uses all of them
	                             but currently apt-cyg uses the first one only.
	  mark-auto [<packages> ...] :
	                             Mark the given packages
	                             as automatically installed.
	  mark-manual [<packages> ...] :
	                             Mark the given packages as manually installed.
	  mark-showauto            : Print the list of
	                             automatically installed packages.
	  mark-showmanual          : Print the list of manually installed packages.
	  call [<internal_function> [<args> ...]] :
	                             Call internal function in apt-cyg.
	  time [<internal_function> [<args> ...]] :
	                             Report time consumed
	                             to call internal function in apt-cyg.
	  filelist [<package>]     : File list like apt-file list.
	  filesearch [<pattern>]   : File search like apt-file search.
	Options:
	  --ag                     : use the silver searcher
	                             (currently work only at packageof subcommand)
	  --benchmark-timeout <duration> :
	                             Truncate items that take longer than <duration>
	                             when benchmarking.
	  --ignore-case, -i        : ignore case distinctions for <patterns>
	  --force-remove           : force remove
	  --force-fetch-trustedkeys :
	                             force fetch trustedkeys
	  --force-update-packageof-cache :
	                             force update packageof cache
	  --no-verify, -X          : Don't verify setup.ini signatures
	  --no-check-certificate   : Don't validate the server's certificate
	  --no-update-setup        : Don't update setup.exe
	  --no-header              : Don't print header
	  --proxy, -p {auto|inherit|none|<url>} :
	                             set proxy (default: \${APT_CYG_PROXY:-auto})
	  --completion-get-subcommand :
	                             get subcommand (for completion internal use)
	  --completion-disable-autoupdate :
	                             disable completion autoupdate
	  --max-jobs, -j <n>       : Run <n> jobs in parallel
	  --mirror, -m <url>       : set mirror
	  --cache, -c <dir>        : set cache
	  --file, -f <file>        : read package names from <file>
	  --noupdate, -u           : don't update setup.ini from mirror
	  --ipv4, -4               : wget prefer ipv4
	  --no-progress            : hide the progress bar in any verbosity mode
	  --quiet, -q              : quiet (no output)
	  --verbose, -v            : verbose
	  --help                   : Display help and exit
	  --version                : Display version and exit
	EOD
}



function git_status ()
{
  local origin status
  [ ! -d "${SCRIPT_REALDIR%/}/.git" ] && return 1
  pushd "${SCRIPT_REALDIR}" >/dev/null
  echo
  origin="$(git config --get remote.origin.url)" && echo "origin: $origin"
  echo "commit: $(git log -1 --pretty=format:"%ai %h%d")"
  status="$(git status --short "${SCRIPT_REALNAME}")"
  [ -n "$status" ] && echo "status: $status"
  popd >/dev/null
}

function version ()
{
  cat<<-EOD
	kou1okada/apt-cyg
	forked from transcode-open/apt-cyg$(git_status)
	
	This script is based on apt-cyg version 0.57
	Copyright (c) 2005-9 Stephen Jungels. Released under the GPL.
	Copyright (c) 2013-7 Stephen Jungels. Republished under the MIT license.
	EOD
}

# Usage: verbose [level [msg ...]]
function verbose ()
{
  (( OPT_VERBOSE_LEVEL < "${1:-1}" )) && return
  (( 1 < $# )) && echo "${@:2}" || cat
} >&2

# Usage: verbosefor [level]
function verbosefor ()
{
  (( OPT_VERBOSE_LEVEL < ${1:-1} )) && echo /dev/null || echo /dev/stderr
}

function update_verbosefor ()
{
  local i
  for i in {0..5}; do
    (( OPT_VERBOSE_LEVEL < i )) && verbosefor[i]=/dev/null || verbosefor[i]=/dev/stderr
  done
}

function detect_field_width ()
# Read stdin and detect field width.
# Return NF+2 values as follorwing:
# w(0) w(1) w(2) ... w(NF) sum(w(1),w(2), ..., w(NF))+NF-1
# Now NF is a number of fields,
# w(x) is a function of maxium width of field x th
# (note that 0 does not mean the field but the whole line) 
# and sum(v1,v2, ..., vn) is a function to sum all arguments.
{
  awk '
    function max(a,b){return a<b?b:a}
    {for(i=0;i<=NF;i++)w[i]=max(w[i],length($i));nf=max(nf,NF)}
    END{for(i=0;i<=nf;i++){wall+=w[i];printf("%s%d",i?" ":"",w[i])};print " "wall-w[0]+nf-1}
  '
}

function align_columns ()
# Read stdin and aligne columns indent.
{
  local lines w fmt
  readarray -t lines
  w=( $(join_str $'\n' "${lines[@]}" | detect_field_width) )
  fmt="$(printf " %%-%ds" "${w[@]:1:${#w[@]}-2}")"
  printf "${fmt:1}\n" ${lines[@]}
}

function join_str () # <separator> [<strings> ...]
# Join strings with separator.
# Note that separator must be single character.
# If you want separator with multiple character, use join_str_ex.
{
  (IFS="$1"; printf "%s" "${*:2}")
}

function join_str_ex () # <separator> [<strings> ...]
# Join strings with separator.
# Different for join_str the separator can take multiple character.
{
  local str="$(join_str $'\x1c' "${@:2}")"
  printf "%s" "${str//$'\x1c'/$1}"
}

function split_str () # <separator> [<strings> ...]
# Split strings with separator.
{
  local str=( "${@:2}" )
  (IFS=$'\n'; printf "%s" "${str[*]//$1/$'\n'}")
}

function join_str_uniq () # <separator> [<values> ...]
# Append values to head of joined strings.
{
  local str
  readarray -t str < <(split_str "$@" | uniqex)
  join_str "$1" "${str[@]}"
}

function mkdirp () # <dir>
{
  [ -d "$1" ] || mkdir -p "$1" || { error "mkdir failed: $1"; exit 1; }
}

function mktmpfn () # -v <var>
# Temporary filename
{
  printf ${@:1:2} "/tmp/${SCRIPT_NAME}.$$.%04x%04x" $RANDOM $RANDOM
}

function init_comspec ()
{
  : ${SYSTEMPATH:=$(join_str_uniq : "$(cygpath -u "${SYSTEMROOT:-$WINDIR}")"{/system32,} "$PATH")}
}

function comspec () # [<parameters> ...]
# Call $COMSPEC.
{
  init_comspec
  PATH="$SYSTEMPATH" "$(cygpath "$COMSPEC")" "$@"
}

function cygstart () # [<parameters> ...]
{
  init_comspec
  PATH="$SYSTEMPATH" cygstart.exe "$@"
}

function update_cache_for_mirrors_list_online ()
{
  pushd "$apt_cyg_cachedir" >/dev/null
  wget -qN https://cygwin.com/mirrors.lst
  popd >/dev/null
}

function is_official_mirrors_of_cygwin () # <mirrors> ...
# Check <mirrors> whether are listed in official mirrors list of cygwin.
# Official mirros list provides on https://cygwin.com/mirrors.html.
# Args:
#   <mirrors> : URLs of the cygwin mirror.
# Return:
#   Return zero if all mirrors are known, non-zero otherwise.
{
  local mirror
  local result=0
  local local_list="$(apt-cyg-mirrors-list)"
  local online_list="$(apt-cyg-mirrors-list-online)"
  for mirror; do
    if ! grep -q "$mirror" <<< "$local_list"; then
      warning "/etc/setup/setup.rc doesn't know your mirror: ${SGR_bold}$mirror${SGR_reset}" >&2
      result=1
    fi
    if ! grep -q "$mirror" <<< "$online_list"; then
      warning "Official mirrors.lst doesn't know your mirror: ${SGR_bold}$mirror${SGR_reset}" >&2
      result=1
    fi
  done
  return $result
}

function mirror_to_mirrordir () # <mirror>
{
  local tmp="${1//:/%3a}"
  echo "${tmp//\//%2f}"
}

function get_utf8_setuprc ()
{
  local utf8_setuprc="$apt_cyg_cachedir/setup.rc.utf8"
  mkdirp "$apt_cyg_cachedir"
  [ "$utf8_setuprc" -nt /etc/setup/setup.rc ] && {
    cat "$utf8_setuprc"
  } || {
    cp2utf8 /etc/setup/setup.rc | tee "$utf8_setuprc"
  }
}

function setuprc_import_sections () # <section> ...
{
  local IFS="|"
  get_utf8_setuprc \
  | awk -vRS='\n\\<|\n\\'\' -vFS='\n\t' -vsections="^${*//\\/\\\\}$" '
    match($1, sections) {
      s = gensub(/-/, "_", "g", $1) "=("
      for(i = 2; i <= NF; i++) s = s " \x27" $i "\x27";
      s = s " )";
      print s;
    }
  '
}

function setuprc_get_section () # <section>
{
  get_utf8_setuprc \
  | awk -vRS='\n\\<|\n\\'\' -vFS='\n\t' -vsection="${1//\\/\\\\}" '
    $1 == section {for(i = 2; i <= NF; i++) print $i;}
  '
}

function setuprc_set_section () # <section> [<values> ...]
{
  local s="$(join_str_ex $'\n\t' "${@}")"
  local setuprc="/etc/setup/setup.rc"
  local setuprc_bak="${setuprc},$(date -r "$setuprc" "+%Y%m%d_%H%M%S")"
  
  cp -a "$setuprc" "$setuprc_bak"
  
  cp2utf8 "$setuprc_bak" \
  | awk -vRS='\n\\<|\n\\'\' -vFS='\n\t' -vsection="${1//\\/\\\\}" -vs="${s//\\/\\\\}" '
    $1 != section
    $1 == section {print s; done=1;}
    END {if(!done) print s;}
  ' \
  | utf82cp > "$setuprc"
}

function set_cachefile () #
#   Set filename of cachevars to $cachefile.
# Returns:
#   $cachefile : Overwrite with filename of cachevars.
# Note:
#   This is internal function for load_vars_from_cache and save_vars_to_cache.
{
  cachefile="$apt_cyg_cachedir/cache_for_vars_of_${FUNCNAME[2]}"
}

function load_vars_from_cache () # [<depended files> ...] || { init_vars ...; save_vars_to_cache [<varnames> ...]; }
#   restore vars from cache.
{
  local cachefile; set_cachefile
  local i
  set -- "$SCRIPT_PATH" "$@"
  for i; do [ "$cachefile" -nt "$i" ] || return 1; done
  source "$cachefile"
}

function save_vars_to_cache () # [<varnames> ...]
#   save vars to cache.
{
  local cachefile; set_cachefile
  declare -p "$@" | sed -E 's/^declare .. //g' >"$cachefile"
}

function findworkspace()
{
  local cachevars=( last_cache last_mirror mirror cache arch mirrordir )
  load_vars_from_cache /etc/setup/setup.rc || {
    eval "$(setuprc_import_sections last-cache last-mirror)"

    mirror="$last_mirror"
    cache="$(cygpath -au "$last_cache")"
    
    cache="${cache%/}"
    mirror="${mirror%/}"
    
    mirrordir="$(mirror_to_mirrordir "$mirror/")"

    save_vars_to_cache "${cachevars[@]}"
  }

  verbose 1 "Cache directory is $cache"
  verbose 1 "Mirror is $mirror"

  mkdirp "$cache/$mirrordir/$arch"
  cd "$cache/$mirrordir/$arch"
  
  init_gnupg
  fetch_trustedkeys
}

function download_and_verify () # <url>
{
  local urls=( "$1"{,.sig} )
  wget -N "${urls[@]}" || return 1
  if [ -z "$no_verify" ]; then
    [ -e "${1##*/}.sig" ] && verify_signatures "${1##*/}.sig" || return 1
  fi
  [ -e "${1##*/}" ]
}

function files_backup ()
{
  local file
  for file; do
    [ -e "${file}~" ] && mv    "${file}~" "${file}"
    [ -e "${file}"  ] && cp -a "${file}"  "${file}~"
  done
}

function files_restore ()
{
  local file
  for file; do
    [ -e "${file}"  ] && rm    "${file}"
    [ -e "${file}~" ] && mv    "${file}~" "${file}"
  done
}

function files_backup_clean ()
{
  local file
  for file; do
    [ -e "${file}~" ] && rm    "${file}~"
  done
}

function setupini_download ()
{
  local BASEDIR="$cache/$mirrordir/$arch"
  mkdirp "$BASEDIR"
  
  [ $noscripts -ne 0 -o $noupdate -ne 0 ] && return
  
  pushd "$BASEDIR" > /dev/null
  files_backup setup.{zst,xz,bz2,ini}{,.sig}
  
  while true; do
    verbose 1 "Updating setup.ini"
    false \
    || { download_and_verify "$mirror/$arch/setup.zst" && { zstd  -dfkq setup.zst && mv setup{,.ini} && rm -f setup.ini.sig || ! rm -fv setup.zst; }; } \
    || { download_and_verify "$mirror/$arch/setup.xz"  && { xz    -dfk  setup.xz  && mv setup{,.ini} && rm -f setup.ini.sig || ! rm -fv setup.xz;  }; } \
    || { download_and_verify "$mirror/$arch/setup.bz2" && { bzip2 -dfk  setup.bz2 && mv setup{,.ini} && rm -f setup.ini.sig || ! rm -fv setup.bz2; }; } \
    ||   download_and_verify "$mirror/$arch/setup.ini" || break
    
    files_backup_clean setup.{zst,xz,bz2,ini}{,.sig}
    popd > /dev/null
    verbose 1 "Updated setup.ini"
    return
  done
  files_restore setup.{zst,xz,bz2,ini}{,.sig}
  popd > /dev/null
  error "updating setup.ini failed, reverting."
  return 1
}

function getsetup ()
{
  setupini_download || return 1
}

function checkpackages()
{
  if [ $# -eq 0 ]; then
    echo Nothing to do, exiting
    exit 0
  fi
}

function init_gnupg ()
{
  [ -z "$GPG" -o -n "$no_verify" ] && return
  export GNUPGHOME="$cache/.apt-cyg"
  if [ ! -d "$GNUPGHOME" ]; then
    if ! { mkdir -p "$GNUPGHOME" && chmod 700 "$GNUPGHOME"; } then
      error "Cannot initialize directory: $GNUPGHOME"
      exit 1
    fi
  fi
}

# Usage: ask_user [MESSAGE [OPTIONS]]
function ask_user ()
{
  local answer retcode option
  local MESSAGE="$1"
  local OPTIONS="${2:-y/N}"
  local DEFAULT="$(echo "$OPTIONS" | awk -v FS=/ '{for(i=1;i<=NF;i++)if(match(substr($i,1,1),/[A-Z]/)){print $i; exit}}')"
  local SPLIT_OPTIONS
  readarray -t SPLIT_OPTIONS < <(echo "$OPTIONS" | sed -e 's:/:\n:g')
  while true; do
    echo -n "${MESSAGE}${MESSAGE:+ }[${OPTIONS}] "
    read answer
    retcode=0
    for option in "${SPLIT_OPTIONS[@]}"; do
      if [ "$option" = "${answer:-$DEFAULT}" ]; then
        return $retcode
      fi
      retcode=$(( retcode + 1 ))
    done
  done
}

unset INIT_WGET
function init_wget ()
{
  [ -n "$INIT_WGET" ] && return

  eval "$(
    "$WGET" --help \
    |& awk '
      /--show-progres/{print "HAVE_SHOW_PROGRESS=--show-progress"}
      /--no-verbose/  {print "HAVE_NO_VERBOSE=--no-verbose"}
    ')"
  (( OPT_VERBOSE_LEVEL < 0 )) && WGET+=( --quiet ) || \
  (( OPT_VERBOSE_LEVEL < 1 )) && WGET+=( --quiet $HAVE_SHOW_PROGRESS ) || \
  (( OPT_VERBOSE_LEVEL < 2 )) && WGET+=( $HAVE_NO_VERBOSE $HAVE_SHOW_PROGRESS )
  
  proxy_setup
  
  INIT_WGET=DONE
}

function wget ()
{
  init_wget
  "${WGET[@]}" "$@"
}

# Reference:
# https://www.gnu.org/software/wget/manual/wget.html#Exit-Status
# Usage: wget-exitstatus EXITSTATUS
function wget-exitstatus ()
{
  case $1 in
  0) echo "No problems occurred.";;
  1) echo "Generic error code.";;
  2) echo "Parse error?for instance, when parsing command-line options, the '.wgetrc' or '.netrc'...";;
  3) echo "File I/O error.";;
  4) echo "Network failure.";;
  5) echo "SSL verification failure.";;
  6) echo "Username/password authentication failure.";;
  7) echo "Protocol errors.";;
  8) echo "Server issued an error response.";;
  *) echo "Unknown errors.";;
  esac
}

function wget_advice ()
{
  local status=${1:-$?}
  case $status in
  5) echo "If you can tolerate security risks, use --no-certification option." ;;
  esac
  return $status
}

# Usage: get_advice cmd
function get_advice ()
{
  local status=${2:-$?}
  type "${1}_advice" >&/dev/nul && { ( exit $status ); "${1}_advice"; }
  return $status
}

# Usage: push_advice cmd
function push_advice ()
{
  local status=${2:-$?}
  ADVICE+=( "$( get_advice "${1}_advice")" )
  return $status
}

function show_advice ()
{
  local i
  for i in "${ADVICE[@]}"; do
    echo "$i" >&2
  done
}

# Usage: wget_and_hash_check label hash url file
function wget_and_hash_check ()
{
  local LABEL="$1"
  local SUM="$2"
  local URL="$3"
  local FILE="$4"
  if ! { wget "$URL" -O "$FILE" || get_advice wget; }; then
    echo "$LABEL: FAILED: Could not download $URL."
    return 1
  fi
  if ! hash_check <<<"$SUM *$FILE" >&/dev/null; then
    echo "$LABEL: FAILED: Hash does not match $URL."
    return 2
  fi
  echo "$LABEL: OK"
}

function get_gpg_fingerprint_to_assoc () # <assoc varname>
{
  local -n result="$1"
  local line lines
  readarray -t lines < <("${GPG[@]}" --with-colons --fingerprint)
  for line in "${lines[@]}"; do
    [[ "$line" =~ ^fpr:+([0-9A-Za-z]+):* ]] && result[${BASH_REMATCH[1]}]=1
  done
}

function fetch_trustedkeys ()
{
  [ -z "$GPG" -o -n "$no_verify" ] && return
  local i
  local FILE       ; mktmpfn -v FILE
  local FILE_LATEST; mktmpfn -v FILE_LATEST
  local -A FPRS_assoc; get_gpg_fingerprint_to_assoc FPRS_assoc
  for i in "${TRUSTEDKEYS[@]}"; do
    local LABEL="TRUSTEDKEY_${i}"
    local -n SUM="${LABEL}_SUM"
    local -n FPR="${LABEL}_FPR"
    local -n URL="${LABEL}_URL"
    local -n URL_LATEST="${LABEL}_URL_LATEST"
    local CASE=""
    if [ -z "$force_fetch_trustedkeys" -a -n "${FPRS_assoc[$FPR]}" ]; then
      continue
    fi
    if [ -n "$URL" ]; then
      wget_and_hash_check "$LABEL" "$SUM" "$URL" "$FILE"
      CASE+="$?"
    else
      CASE+="-"
    fi
    if [ -n "$URL_LATEST" ]; then
      wget_and_hash_check "$LABEL" "$SUM" "$URL_LATEST" "$FILE_LATEST"
      CASE+="$?"
    else
      CASE+="-"
    fi
    case "$CASE" in
      00|01|0-)
        "${GPG[@]}" --import "$FILE"
        ;;
      02)
        warning "${LABEL} has been updated."
        "${GPG[@]}" --import "$FILE"
        ;;
      -0)
        "${GPG[@]}" --import "$FILE_LATEST"
        ;;
      10|20)
        error "${LABEL} has miss configuration."
        exit 1
        ;;
      11|1-|-1)
        error "Could not download ${LABEL}."
        exit 1
        ;;
      12|-2)
        error "${LABEL} has been updated, maybe. But sometimes it may has been cracked. Be careful !!!"
        exit 1
        ;;
      21|22|2-)
        error "${LABEL} has been cracked, maybe"
        exit 1
        ;;
      --)
        error "${LABEL} has no URL."
        exit 1
        ;;
    esac
    rm "$FILE" "$FILE_LATEST" &>/dev/null
  done
}

# Usage: verify_signatures files ...
function verify_signatures ()
{
  while [ $# -gt 0 ]; do
    if ! "${GPG[@]}" --verify "$1" &>"${verbosefor[2]}"; then
      error "BAD signature: $1"
      return 1
    else
      verbose 0 -e "${SGR_fg_green}${SGR_bold}signature verified:${SGR_reset} $1"
    fi
    shift
  done
}

# Usage: apt-cyg-key-add pkey ...
function apt-cyg-key-add ()
{
  [ -z "$GPG" ] && { error "GnuPG is not installed. Prease install gnupg package"; exit 1; }
  local pkeys
  for pkey; do
    pkeys+=( "$(cygpath -a "$pkey" )" )
  done
  findworkspace
  for pkey in "${pkeys[@]}"; do
    "${GPG[@]}" --import "$pkey"
  done
}

# Usage: apt-cyg-key-add keyid ...
function apt-cyg-key-del ()
{
  [ -z "$GPG" ] && { error "GnuPG is not installed. Prease install gnupg package"; exit 1; }
  local keyid
  findworkspace
  for keyid; do
    "${GPG[@]}" --batch --yes --delete-key "$keyid"
  done
}

function apt-cyg-key-list ()
{
  [ -z "$GPG" ] && { error "GnuPG is not installed. Prease install gnupg package"; exit 1; }
  findworkspace
  "${GPG[@]}" --list-keys
}

function apt-cyg-key-finger ()
{
  [ -z "$GPG" ] && { error "GnuPG is not installed. Prease install gnupg package"; exit 1; }
  findworkspace
  "${GPG[@]}" --fingerprint
}

function apt-cyg-pathof ()
{
  findworkspace >& /dev/null
  while [ "$#" -gt 0 ]; do
    case "$1" in
      cache)            echo "$cache" ;;
      mirror)           echo "$mirror" ;;
      mirrordir)        echo "$mirrordir" ;;
      cache/mirrordir)  echo "$cache/$mirrordir" ;;
      setup.ini)        echo "$cache/$mirrordir/$arch/setup.ini" ;;
      *)
        error "unknown parameter: $1"
        exit 1
        ;;
    esac
    shift
  done
}

function upgrade-self-with-git ()
{
  if [ ! -d "$SCRIPT_REALDIR/.git" ]; then
    warning "apt-cyg is not under the git version control."
    return 1
  fi
  proxy_setup
  pushd "$SCRIPT_REALDIR" > /dev/null
  git pull -v
  popd > /dev/null
}

function upgrade-self-with-wget ()
{
  local updated_url='https://raw.githubusercontent.com/kou1okada/apt-cyg/master/apt-cyg' # TODO: Do not use MAGIC NUMBER
  local temp_file; mktmpfn -v temp_file
  wget "${updated_url}" -q -O "$temp_file"
  chmod +x "$temp_file"
  { diff "$temp_file" "$SCRIPT_REALPATH" >/dev/null && exit 0; } || cp "$temp_file" "$SCRIPT_REALPATH"
  rm "$temp_file"
}

function apt-cyg-upgrade-self ()
{
  upgrade-self-with-git && return
  echo "Fall back to wget for upgrade-self."
  upgrade-self-with-wget
}

function proxy_auto ()
{
  local hash=( $(ipconfig |& md5sum -b) )
  local cache="/tmp/apt-cyg.proxy.$hash"
  local last="$(stat -c %Y "$cache" 2>/dev/null)"
  local now="$(printf "%(%s)T")"
  local proxy
  
  [ -n "$OPT_PROXY_FORCE_REFRESH" ] && last=0
  if (( (now - ${last:-0}) < OPT_PROXY_REFRESH_INTERVAL )); then
    proxy="$(<"$cache")"
  else
    proxy=$("${WGET[@]}" --no-proxy -q -O - wpad/wpad.dat \
    | grep PROXY \
    | sed -e 's/^.*PROXY\s*\([^"]*\).*$/http:\/\/\1/g')
    echo "$proxy" > "$cache"
  fi
  [ -n "$proxy" ] && proxy_set "$proxy"
}

function proxy_set ()
{
  export http_proxy="$1"
  export https_proxy="$1"
  export ftp_proxy="$1"
}

function proxy_unset ()
{
  export -n http_proxy
  export -n https_proxy
  export -n ftp_proxy
}

unset PROXY_SETUP
function proxy_setup ()
{
  [ -n "$PROXY_SETUP" ] && return
  case "$OPT_PROXY" in
    auto)
      proxy_auto
      ;;
    inherit)
      ;;
    none)
      proxy_unset
      ;;
    *)
      proxy_set "$OPT_PROXY"
      ;;
  esac
  PROXY_SETUP=DONE
}

function apt-cyg-get-proxy ()
{
  proxy_setup
  declare -p ftp_proxy http_proxy https_proxy
}

# Usage: get_pkgname PKGFILE
function get_pkgname ()
{
  local tarball="${1##*/}"
  echo "$tarball" | sed -re's/-[0-9].*//g'
}

# PACKAGE_DB is defined at package_db.cc in the cygwin-app setup.exe
# See blow:
# https://www.sourceware.org/cygwin-apps/setup.html
# https://sourceware.org/cgi-bin/cvsweb.cgi/setup/package_db.cc?cvsroot=cygwin-apps
PACKAGE_DB="/etc/setup/installed.db"

function package_db-version_check ()
{
  [ -n "$PACKAGE_DB_VERSION_CHECK_DONE" ] && return
  
  local vernhdr='INSTALLED\.DB [0-9]+'
  local line1; read line1 < "${PACKAGE_DB}"
  local dbver=1
  
  if [[ "$line1" =~ ^INSTALLED\.DB[[:space:]]+([0-9]+)$ ]]; then
    dbver="${BASH_REMATCH[1]}"
  else
    warning "${PACKAGE_DB} does not have version header. The first line is below:\n" \
            "$(head -n1 "${PACKAGE_DB}")\n"
    
    # The earlyer version of apt-cyg was not treat version header correctly.
    if grep -EHnx "${vernhdr}" "${PACKAGE_DB}" >&2; then
      echo "The above line looks like version header, but it is not the first line." >&2
    fi
  fi
  
  if (( dbver < 3 )); then
    warning "${PACKAGE_DB} version is less than 3.\n" \
            "Before continuing, recommend to execute below command:\n" \
            "    apt-cyg dist-upgrade"
    ask_user "Do you continue?" >&2 && {
      echo "continue" >&2
    } || {
      echo "abort" >&2
      exit 1
    }
  elif (( 3 < dbver )); then
    error "${PACKAGE_DB} has unknown version header.\n" \
          "Currently apt-cyg supports the DB of ver 3 or ealyer, but your DB is ver $dbver.\n"
    ask_user "Do you want to continue at your own risk?" >&2 && {
      echo "continue" >&2
    } || {
      echo "abort" >&2
      exit 1
    }
  fi
  
  PACKAGE_DB_VERSION_CHECK_DONE=1
}

# Usage: package_db-is_registered PKGNAME
function package_db-is_registered ()
{
  package_db-version_check
  
  awk '
    $1 == PKGNAME && NF != 2 {found = 1; exit}
    END {exit !found}
  ' PKGNAME="$1" "${PACKAGE_DB}"
}

function package_db-list ()
{
  package_db-version_check
  
  awk '
    NF == 3 {
      version = gensub(/^(.*)\.(tgz|tbz|tbz2|tb2|taz|tz|tlz|txz|tar\.(gz|bz2|Z|lz|lzma|xz|zst))$/, "\\1", 1, substr($2, length($1) + 2));
      printf("%s %s %s\n", $1, version, $3);
    }
  ' "${PACKAGE_DB}"
}

# Usage: package_db-register PKGFILE USER_PICKED=0
function package_db-register ()
{
  local pkgfile="${1##*/}"
  local pkgname="$(get_pkgname "$pkgfile")"
  local user_picked="${2:-0}"
  local work="/tmp/apt-cyg.$$.${PACKAGE_DB##*/}"
  
  package_db-version_check
  
  awk '
    function register() {print PKGNAME " " PKGFILE " " USER_PICKED; registered = 1;}
    !registered && PKGNAME < $1 && NF != 2 {register()}
    {print $0}
    END {if (!registered) register()}
  ' PKGNAME="$pkgname" PKGFILE="${pkgfile}" USER_PICKED="${user_picked}" "${PACKAGE_DB}" > "${work}"
  
  mv "${PACKAGE_DB}" "${PACKAGE_DB}-save"
  mv "${work}"       "${PACKAGE_DB}"
}

function package_db_change_mark () # <user_picked> [<package_names> ...]
# Change marks of package in PACKAGE_DB.
# <user_picked> means that 0 was automatically installed
# and 1 was manually installed.
{
  local user_picked="$1"
  local PACKAGE_NAMES="$(join_str $'\x1c' "${@:2}")"
  local work="/tmp/apt-cyg.$$.${PACKAGE_DB##*/}"
  
  package_db-version_check
  
  awk -vPACKAGE_NAMES="$PACKAGE_NAMES" -vUSER_PICKED="$user_picked" '
    BEGIN {
      split(PACKAGE_NAMES, package_names, "\x1c");
      for(i in package_names) target[package_names[i]]=1;
      label = USER_PICKED ? "manually" : "automatically";
    }
    NF==2
    NF!=2&&!target[$1]
    NF!=2&& target[$1] {
      msg = $1 " was " ($3==USER_PICKED?"already ":"") "set to " label " installed.";
      $3 = USER_PICKED;
      print $0;
      print msg > "/dev/stderr"
    }
  ' "${PACKAGE_DB}" 2>&1 >"${work}"
  
  mv "${PACKAGE_DB}" "${PACKAGE_DB}-save"
  mv "${work}"       "${PACKAGE_DB}"
}


# Usage: package_db-unregister PKGNAME
function package_db-unregister ()
{
  local work="/tmp/apt-cyg.$$.${PACKAGE_DB##*/}"
  
  package_db-version_check
  
  awk '!(PKGNAME == $1 && NF != 2) {print $0}' PKGNAME="$1" "${PACKAGE_DB}" > "${work}"
  
  mv "${PACKAGE_DB}" "${PACKAGE_DB}-save"
  mv "${work}"       "${PACKAGE_DB}"
}

# Usage: dep_check DIR ROOTPKGS ...
# Parameters:
#   DIR is "depends" or "rdepends".
#   ROOTPKGS is root package names to check dependency.
# Return:
#   package_name available shallow_depth deep_depth
function dep_check ()
{
  awk \
  '
    function min(x,y) {return x < y ? x : y}
    function max(x,y) {return x < y ? y : x}
    function update_result(dir, rootpkg, pkg, depth, _, i) {
      if (0 + result[rootpkg, pkg, "deep"]) {
        result[rootpkg, pkg, "shallow"] = min(depth, result[rootpkg, pkg, "shallow"]);
        result[rootpkg, pkg, "deep"]    = max(depth, result[rootpkg, pkg, "deep"]);
      } else {
        result[rootpkg, pkg, "deep"] = result[rootpkg, pkg, "shallow"] = depth;
        result[rootpkg, pkg, "available"] = 0 + available[pkg];
        result[rootpkg, result[rootpkg, "n"]++] = pkg;
        for (i = 0; i < dep[dir, pkg, "n"]; i++) {
          update_result(dir, rootpkg, dep[dir, pkg, i], depth + 1);
        }
      }
    }
    $1 == "@" {
      pkg = $2;
      available[pkg] = 1;
    }
    $1 == "requires:" || $1 == "depends2:" {
      for (req = 2; req <= NF; req++) {
        reqpkg = gensub(/,$/, "", "g", $req);
        dep["rdepends", reqpkg, dep["rdepends", reqpkg, "n"]++] = pkg;
        dep["depends" , pkg   , dep["depends" , pkg   , "n"]++] = reqpkg;
      }
    }
    END {
      split(ROOTPKGS, rootpkgs, "\x1c");
      for (k in rootpkgs) {
        update_result(DIR, rootpkgs[k], rootpkgs[k], 1);
        for(i = 0; i < result[rootpkgs[k], "n"]; i++) {
          printf("%-40s %d\t%d\t%d\n",
            result[rootpkgs[k], i],
            result[rootpkgs[k], result[rootpkgs[k], i], "available"],
            result[rootpkgs[k], result[rootpkgs[k], i], "shallow"],
            result[rootpkgs[k], result[rootpkgs[k], i], "deep"]);
        }
      }
    }
  ' \
  DIR="$1" ROOTPKGS="$(join_str $'\x1c' "${@:2}")" "$(apt-cyg-pathof "setup.ini")" \
  | awk '
    function min(x,y) {return x < y ? x : y}
    function max(x,y) {return x < y ? y : x}
    {
      packages[$1] = pkg = $1;
      status[pkg, "available"] = $2;
      status[pkg, "shallow"  ] = 0 + status[pkg, "shallow"] == 0 ? $3 : min(status[pkg, "shallow"], $3);
      status[pkg, "deep"     ] = max(0 + status[pkg, "deep"], $4);
    }
    END {
      for (pkg in packages) {
        printf("%-40s %d\t%d\t%d\n",
          pkg,
          status[pkg, "available"],
          status[pkg, "shallow"],
          status[pkg, "deep"])
      }
    }
  ' | sort -nrk4
}

function apt-cyg-depends ()
{
  local pkg
  [ -z "$OPT_NO_HEADER" ] && printf "%-40s %s\t%s\t%s\n" "PKGNAME" "AVAIL" "SHALLOW" "DEEP"
  dep_check depends "$@"
}

function apt-cyg-rdepends ()
{
  local pkg
  [ -z "$OPT_NO_HEADER" ] && printf "%-40s %s\t%s\t%s\n" "PKGNAME" "AVAIL" "SHALLOW" "DEEP"
  dep_check rdepends "$@"
}

function get_module_line () # <pattern> [<file>]
# Return line numbers of module about begin and end.
{
  grep -nE "^#\s*(BEGIN|END)_MODULE\s*:\s*${1}" "${@:2:1}" | grep -Eo '^[0-9]+'
}

# EMBED_BEGIN: hhs_utils.bash

function headtail () # <first_line> <last_line> [<file>]
# Split lines from <first_line> to <last_line>.
{
  head -n+$2 "${@:3:1}" | tail -n+$1
}

function uniqex ()
# An alternative uniq command which is not required sort
{
  awk '!c[$0]++'
}

# EMBED_END: hhs_utils.bash

function replace_range # <first_line> <last_line> <content> [<file>]
{
  awk -vl1=$1 -vl2=$2 -vs="${3//\\/\\\\}" '
    NR < l1 || l2 < NR
    !done && l1 <= NR {print s;done=1;}
  ' "${@:4:1}"
}

function replace_module # <pattern> <src> <dst>
{
  local tmpfile; mktmpfn -v tmpfile
  local srclines=( $(get_module_line "bash completion for apt-cyg" "$2") )
  local dstlines=( $(get_module_line "bash completion for apt-cyg" "$3") )
  replace_range "${dstlines[@]}" "$(headtail "${srclines[@]}" "$2")" "$3" > "$tmpfile"
  mv "$tmpfile" "$3"
}

function apt-cyg-completion-install ()
{
  if [ ! -d "/etc/bash_completion.d" ]; then
    error "/etc/bash_completion.d is not exist."
    exit 1
  fi
  if ! package_db-is_registered "bash-completion"; then
    error "bash-completion is not installed."
    exit 1
  fi
  
  local __APT_CYG_SUBCMDS
  local __APT_CYG_OPTIONS
  local __APT_CYG_SCRIPTPATH="$(realpath "$(type -p apt-cyg)")"
  local __APT_CYG_SCRIPTDIR="${__APT_CYG_SCRIPTPATH%/*}"
  local __APT_CYG_COMPLETION_DISABLE_AUTOUPDATE="$OPT_COMPLETION_DISABLE_AUTOUPDATE"
  
  readarray -t __APT_CYG_SUBCMDS < <(grep "^function " "$__APT_CYG_SCRIPTPATH" | awk 'match($2, /apt-cyg-([-_0-9A-Za-z]+)/,m){print m[1]}')
  readarray -t __APT_CYG_OPTIONS < <(
    awk '
      /^function *parse_args *\(\)/ {proc=1}
      /^} *# *\/parse_args( |$)/    {proc=0}
      proc && match($0, /^ *(-[^()*]+)\)/, m) {
        split(m[1], x, "|");
        for (i in x) print x[i];
      }
      ' "$__APT_CYG_SCRIPTPATH"
    )
  
  cat <<-EOD > /etc/bash_completion.d/apt-cyg
		$(declare -p __APT_CYG_{SUBCMDS,OPTIONS,SCRIPTPATH,SCRIPTDIR,COMPLETION_DISABLE_AUTOUPDATE} | sed -E 's/^declare/\0 -g/g')
		
		# BEGIN_MODULE: bash completion for apt-cyg
		# END_MODULE:   bash completion for apt-cyg
		
		complete -o filenames -F __apt-cyg apt-cyg
		EOD
  
  replace_module "bash completion for apt-cyg" "${BASH_SOURCE}" /etc/bash_completion.d/apt-cyg
  touch -r "$__APT_CYG_SCRIPTPATH" "/etc/bash_completion.d/apt-cyg"

  echo "A bash completion /etc/bash_completion.d/apt-cyg is installed"
}

# BEGIN_MODULE: bash completion for apt-cyg

function __apt-cyg ()
{
  local cur prev getsubcmd subcmd
  
  # Auto update for completion script.
  if [ -z "$__APT_CYG_COMPLETION_DISABLE_AUTOUPDATE" -a "$__APT_CYG_SCRIPTPATH" -nt "/etc/bash_completion.d/apt-cyg" ]; then
    apt-cyg completion-install >/dev/null 2>&1
    . /etc/bash_completion.d/apt-cyg
    __apt-cyg "$@"
    return
  fi
  
  _get_comp_words_by_ref -n : cur prev
  
  getsubcmd=( apt-cyg --completion-get-subcommand $(echo "${COMP_LINE}" | sed -E 's/^[ \t]*[^ \t]+//g;s/[^ \t]+$//g') )
  subcmd="$( "${getsubcmd[@]}" )"
  
  case "$subcmd" in
    install|depends|rdepends|describe|find|category)
      COMPREPLY=( $(awk '/^@ /{print $2}' "$(apt-cyg pathof setup.ini)") )
      ;;
    remove)
      COMPREPLY=( $(apt-cyg --no-header show 2>/dev/null | awk '$0=$1') )
      ;;
    pathof)
      COMPREPLY=( cache mirror mirrordir cache/mirrordir setup.ini )
      ;;
    *)
      COMPREPLY=( "${__APT_CYG_SUBCMDS[@]}" )
      ;;
  esac
  case "$prev" in
    --cache|-c)
      readarray -t COMPREPLY < <(compgen -d -- "$cur")
      ;;
    --mirror|-m)
      COMPREPLY=( $(apt-cyg mirrors-list) )
      ;;
    --file|-f)
      readarray -t COMPREPLY < <(compgen -f -- "$cur")
      ;;
    --proxy|-p)
      COMPREPLY=( auto inherit none http:// )
      ;;
    *)
      COMPREPLY+=( "${__APT_CYG_OPTIONS[@]}" )
      ;;
  esac
  if [ -n "$DEBUG_COMPLETION" ]; then
    echo
    declare -p BASH_SOURCE "${!COMP@}" getsubcmd subcmd cur prev
    echo -n "${PS1@P}"
    [[ "$COMP_LINE" =~ ^\  ]] && echo -n "time" # maybe...
    echo -n "${COMP_LINE}"
    (( ${#COMP_LINE} - COMP_POINT )) && echo -ne "\e[$(( ${#COMP_LINE} - COMP_POINT ))D"
  fi
  readarray -t COMPREPLY < <(compgen -W "${COMPREPLY[*]@Q}" -- "$cur")
  __ltrim_colon_completions "$cur"
}

# END_MODULE:   bash completion for apt-cyg

function apt-cyg-completion-uninstall ()
{
  if [ ! -f /etc/bash_completion.d/apt-cyg ]; then
    error "/etc/bash_completion.d/apt-cyg is not exist."
    exit 1
  fi
  rm /etc/bash_completion.d/apt-cyg
  echo "A bash completion /etc/bash_completion.d/apt-cyg is uninstalled"
}

function apt-cyg-mirrors-list ()
{
  setuprc_get_section mirrors-lst | sed -re 's/;.*//g'
}

function apt-cyg-mirrors-list-long ()
{
  setuprc_get_section mirrors-lst | column -t -s ";"
}

function apt-cyg-mirrors-list-online ()
{
  update_cache_for_mirrors_list_online
  cat "${apt_cyg_cachedir}/mirrors.lst"
}

function apt-cyg-benchmark-mirrors ()
{
  local mirror result exitcode
  local WGET=( wget )
  [ -n "$OPT_BENCHMARK_TIMEOUT" ] && WGET=( timeout "$OPT_BENCHMARK_TIMEOUT" "${WGET[@]}" )
  for mirror; do
    result="$( { time "${WGET[@]}" -qO/dev/null -T3 -t 1 "${mirror%/}/$arch/setup.bz2"; } 2>&1 )"
    exitcode=$?
    if [ $exitcode -ne 0 ];then
      if [ $exitcode -eq 124 ]; then
        echo -e "Timeout:\t${mirror}" >&2
      else
        warning "benchmark failed with wget exitcode $exitcode: $(wget-exitstatus $exitcode): $1"
      fi
      continue
    fi
    [[ "$result" =~ real[^0-9]*([0-9]*m[0-9.]*s) ]] && echo -e "${BASH_REMATCH[1]}\t${mirror}"
  done
}

function apt-cyg-benchmark-parallel-mirrors ()
{
  local result; mktmpfn -v result
  local mirror
  for mirror; do echo $mirror; done | lesser-parallel apt-cyg-benchmark-mirrors {} | tee "$result"
  echo Finished benchmark.
  echo ========================================
  echo Sorted result.
  sort -rV "$result"
  rm "$result"
}

function apt-cyg-benchmark-parallel-mirrors-list ()
{
  local mirrors
  readarray -t mirrors < <(apt-cyg-mirrors-list)
  apt-cyg-benchmark-parallel-mirrors "${mirrors[@]}"
}

function apt-cyg-scriptinfo ()
{
  cat<<-EOD
	SCRIPT_PATH     = "$SCRIPT_PATH"
	SCRIPT_FILE     = "$SCRIPT_FILE"
	SCRIPT_NAME     = "$SCRIPT_NAME"
	SCRIPT_DIR      = "$SCRIPT_DIR"
	SCRIPT_REALPATH = "$SCRIPT_REALPATH"
	SCRIPT_REALFILE = "$SCRIPT_REALFILE"
	SCRIPT_REALNAME = "$SCRIPT_REALNAME"
	SCRIPT_REALDIR  = "$SCRIPT_REALDIR"
	EOD
}

# Usage: isbusy [file ...]
function isbusy ()
{
  perl -e 'foreach $i(@ARGV){if(-f $i){open(DATAFILE,"+<",$i)||exit(0);close(DATAFILE);}}exit(1);' -- "$@"
}

function apt-cyg-show-packages-busyness ()
{
  local pkg lst files
  for pkg; do
    lst="/etc/setup/${pkg}.lst.gz"
    if [ -e "$lst" ]; then
      readarray -t files < <(gzip -dc "$lst"|sed 's:^:/:g')
      isbusy "${files[@]}" && echo -n "busy: " || echo -n "free: "
      echo "$pkg"
    fi
  done
}

unset CODEPAGE

function get_codepage_wt_chcp ()
{
  comspec /c chcp.com \
  | awk 'match($NF, /([0-9]+)/, m) { print m[1] == "20127" ? "US-ASCII" : ("CP" m[1]) }'
}

function get_codepage ()
{
  echo -n "${CODEPAGE:=$(get_codepage_wt_chcp)}"
}

function cp2utf8 () # [<iconv_args> ...]
{
  iconv -f $(get_codepage) -t UTF-8 "$@"
}

function utf82cp () # [<iconv_args> ...]
{
  iconv -f UTF-8 -t $(get_codepage) "$@"
}

# dummy command for unknown sum.
# Usage: unknownsum
function unknownsum ()
{
  return 1
}

# Determine the hash method from a HASH.
# Usage: hash_method HASH
function hash_method ()
{
  case "${#1}" in
    32)  echo md5     ;;
    40)  echo sha1    ;;
    56)  echo sha224  ;;
    64)  echo sha256  ;;
    96)  echo sha384  ;;
    128) echo sha512  ;;
    *)   echo unknown ;;
  esac
}

# Read md5 and sha{1,224,256,384,512} sums from the FILEs and check them.
# Usage: hash_check [FILE ...]
function hash_check ()
{
  local f0 f1 f2 method count=0
  while true; do
    while read f0; do
      f1="${f0% *}"
      f2="${f0#*\*}"
      method="$(hash_method "$f1")sum"
      if echo "$f0" | "$method" -c --status; then
        verbose 0 "hash_check: $method: $f2: OK"
      else
        verbose 0 "hash_check: $method: $f2: FAILED"
        count=$(( count + 1 ))
      fi
    done 0<${1:-<(cat)}
    shift
    (( $# <= 0 )) && break
  done
  if (( 0 < count )); then
    verbose 0 "hash_check: WARNING: $count computed checksum did NOT match"
    return 1
  fi
  return 0
}

function kill_all_cygwin_process ()
{
  local family
  local pid=$BASHPID
  local pslog=$(ps -f | tail -n +2 | awk '{print $2, $3}')
  readarray -t family < <(echo "$pslog" | awk -v pid=$pid '
    {ppids[$1] = $2;}
    END {while (1 < pid) {print "-e\n"pid; pid = ppids[pid];}}
  ')
  kill -9 $(echo "$pslog" | grep -v "${family[@]}" | awk '{print $1}') ${family[$((${#family[@]} - 1))]}
}

function apt-cyg-update-setup ()
{
  [ -n "$OPT_NO_UPDATE_SETUP" ] && return
  local TARGETS=( "${SETUP_EXE}" "${SETUP_EXE}.sig" )
  
  pushd . > /dev/null
  findworkspace
  popd > /dev/null
  pushd "$(apt-cyg-pathof cache)" > /dev/null
  files_backup "${TARGETS[@]}"
  if download_and_verify "https://cygwin.com/${SETUP_EXE}"; then
    files_backup_clean "${TARGETS[@]}"
    chmod +x "${SETUP_EXE}"
    ls -l "${SETUP_EXE}" >"${verbosefor[2]}"
  else
    files_restore "${TARGETS[@]}"
    error "${SETUP_EXE} could not be downloaded: Rollbacked it and aborted."
    exit 1
  fi

  popd > /dev/null
}

function apt-cyg-setup ()
{
  pushd "$(apt-cyg-pathof cache)" > /dev/null
  
  local setup=( "$PWD/${SETUP_EXE}" -R "$(cygpath -wa /)" "$@" )
  
  apt-cyg-update-setup
  cd /etc/setup
  "${setup[@]}"
  
  popd > /dev/null
}

function apt-cyg-dist-upgrade-no-ask ()
{
  pushd "$(apt-cyg-pathof cache)" > /dev/null

  local setup=( "$(cygpath -wa "$PWD")\\${SETUP_EXE}" -R "$(cygpath -wa /)" -B -q -n -g)
  
  apt-cyg-update-setup
  cd /etc/setup
  cygstart "$COMSPEC" /c 'ECHO Press any key to start dist-upgrade for cygwin '"$arch"' && PAUSE && START /WAIT '"${setup[@]}"' && ash -c "/bin/rebaseall -v" && ECHO dist-upgrade is finished && ECHO Press any key to exit && PAUSE' # '
  kill_all_cygwin_process
  
  popd > /dev/null
}

function apt-cyg-dist-upgrade ()
{
  echo "Kill all cygwin process and start dist-upgrade."
  ask_user "Are you sure ?" && {
    echo "Start dist-upgrade ..."
    sleep 1
    apt-cyg-dist-upgrade-no-ask
  } || {
    echo "Abort."
  }
}

function apt-cyg-packages-total-count ()
{
  grep ^@ <"$(apt-cyg-pathof setup.ini)" | wc -l
}

# Usage: apt-cyg-packages-total-size [pattern_of_section]
function apt-cyg-packages-total-size ()
{
  local section="^$"
  (( 0 < $# )) && section="$1"
  
  awk -v SECTION="$section" '
    /^@ [ -~]+ *$/ {section = ""}
    match($0,/^\[([ -~]*)\] *$/,m) {section = m[1]}
    match(section, SECTION) && $1 == "install:" {sum += $3}
    END {print sum}
  ' "$(apt-cyg-pathof setup.ini)"
}

function apt-cyg-packages-cached-count ()
{
  pushd "$(apt-cyg-pathof cache/mirrordir)" > /dev/null
  find . -type f | grep tar | wc -l
  popd > /dev/null
}

function apt-cyg-packages-cached-size ()
{
  pushd "$(apt-cyg-pathof cache/mirrordir)" > /dev/null
  find . -type f -iname '*tar*' -exec ls -l {} + \
  | awk '{sum+=$5}END{print sum}'
  popd > /dev/null
}

function apt-cyg-repair-acl ()
{
  local target="${1:-/}"
  local aclbackup="/tmp/$(date +%Y%m%d_%H%M%S)_acl"
  ask_user "$(cat <<-EOD
	This subcommand tries to repair the ACL for "${target}".
	Maybe it repairs a cygwin that was installed by setup.exe with -B and --no-admin options.
	But some package, that are failed to install by the ACL problem, need to be reinstalled.
	
	And unfortunately, perchance, this might cause some corruptions about the ACL.
	You can find a backup of the ACL before being rewritten by this subcommand at below:
	  "${aclbackup}.bin"
	  "${aclbackup}.txt"
	
	Are you sure ?
	EOD
  )" || exit 1
  echo
  
  comspec /c icacls.exe "$(cygpath -w "${target}")" /save "$(cygpath -w "${aclbackup}.bin")" > /dev/null
  comspec /c icacls.exe "$(cygpath -w "${target}")" | cp2utf8 > "$(cygpath -w "${aclbackup}.txt")"
  
  comspec /c icacls.exe "$(cygpath -w "${target}")" \
    /grant \
      "%USERDOMAIN%\\%USERNAME%:F" \
      "*S-1-3-1:RX" \
      "Everyone:RX" \
      "CREATOR OWNER:(OI)(CI)(IO)F" \
      "CREATOR GROUP:(OI)(CI)(IO)RX" \
      "Everyone:(OI)(CI)(IO)RX" \
    /remove \
      "NT AUTHORITY\\Authenticated Users" \
      "NT AUTHORITY\\SYSTEM" \
      "BUILTIN\\Administrators" \
      "BUILTIN\\Users" \
      "NULL SID" \
    /inheritance:r \
  | cp2utf8
}

function apt-cyg-repair-postinstall ()
# Repair postinstall scripts
{
  # Repair type p scripts that were marked "done" incorrectly. 
  local i done_marked_p=( /etc/postinstall/[0_z]p_*.done )
  [[ ! -e "$done_marked_p" ]] && return

  verbose 0 -e "${SGR_fg_green}${SGR_bold}Repairing:${SGR_reset} type p postinstall scripts that were marked .done incorrectly."
  for i in "${done_marked_p[@]}"; do
    if [[ "$i" -ot "${i%.done}" ]]; then
      rm -v "$i"
    else
      mv -v "$i" "${i%.done}"
    fi
  done
  echo
}

function get_archives_list () #= section type [package_names ...]
#? get archives list by format below:
#?    path size digest
#?    ...
#? @param section takes section name like curr, prev, test and etc,,,
#? @param type takes type name like install or source.
{
  local section="$1"
  local type="$2"
  shift 2
  awk -v RS="\n\n@ " -v FS="\n" -v PKGS="$(join_str $'\x1c' "$@")" -v MIRROR="${mirror%/}/" \
    -v SECTION="$section" -v TYPE="$type" '
    BEGIN {
      split(PKGS, tmp, "\x1c");
      for (k in tmp) pkgs[tmp[k]] = k; # swap key value
    }
    {
      if(pkgs[$1] == "") {
        delete pkgs[$1];
      } else {
      section = "curr";
      for (i = 2; i <= NF; i++) {
        if (match($i, /^\[(.*)\]$/, m)) {
          section = m[1];
        } else if (match($i, TYPE ": *(.*)", m) && section == SECTION) {
          result[0+n++]=m[1];
          delete pkgs[$1]
        }
      }
    }
    }
    END {
      for (i =0; i < n; i++) {
        print result[i];
      }
      if (0 < length(pkgs)) {
        printf("\x1b[33;1mWarning:\x1b[0m following packages are not found:")>"/dev/stderr";
        for (pkg in pkgs) printf(" %s", pkg)>"/dev/stderr";
        printf("\n")>"/dev/stderr";
      }
    }
  ' "$(apt-cyg-pathof "setup.ini")"
}

function download_packages () #= pos section type [package_names ...]
#? download packages
#? @param pos takes here or mirror.
#? @param section takes section name like curr, prev, test and etc,,,
#? @param type takes type name like install or source.
{
  local pos="$1"
  local section="$2"
  local type="$3"
  shift 3
  
  case "$pos" in
  here)
    ;;
  mirror)
    cd "$(apt-cyg-pathof cache/mirrordir)"
    ;;
  *)
    error "unknown param: $pos"
    exit 1
    ;;
  esac
  
  local mirror="$(apt-cyg-pathof mirror)"
  local pkgs; readarray -t pkgs < <(get_archives_list "$section" "$type" "$@")
  local total="$(printf "%s\n" "${pkgs[@]}" | awk '{sum+=$2}END{print sum;}')"
  local n=${#pkgs[@]}
  echo "$n packages, total $total bytes will be downloaded."
  local line reply=()
  for line in "${pkgs[@]}"; do
    local tmp=( $line )
    local path="${tmp[0]%/*}/"
    local file="${tmp[0]##*/}"
    local url="${mirror%/}/${tmp[0]}"
    local size="${tmp[1]}"
    local digest="${tmp[2]}"
    if [ "$pos" = "mirror" ]; then  
      mkdirp "$path"
      pushd "$path"
    fi
    wget -N "$url"
    if ! hash_check <<<"${digest} *${file}" ; then
      error "hash did not match: $file"
    else
      reply+=( "$(cygpath -a "$file" )" )
    fi
    [ "$pos" = "mirror" ] && popd
  done
  REPLY=( "${reply[@]}" )
}

function apt-cyg-source ()
{
  download_packages here curr source "$@"
}

function apt-cyg-mirror-source ()
{
  download_packages mirror curr source "$@"
}

function apt-cyg-download ()
{
  download_packages here curr install "$@"
}

function apt-cyg-mirror ()
{
  download_packages mirror curr install "$@"
}

function apt-cyg-browse-homepage-with-mirror-source () # [<package names> ...]
#   Browse homepages of packages with mirror-source. 
{
  apt-cyg-mirror-source "$@"

  local source sources=( "${REPLY[@]}" )

  for source in "${sources[@]}"; do
    local PN="${source##*/}";PN="${PN%%-[0-9]*}"
    local cygport cygports=()
    readarray -t cygports < <(tar tf "$source" | grep '\.cygport$')
    if (( ${#cygports} <= 0 )); then
      readarray -t cygports < <(tar tf "$source" | grep '\.patch$')
    fi
    for cygport in "${cygports[@]}"; do
      local url
      read url < <(
        tar xf "$source" "$cygport" -O \
        | grep -E '^\+?HOMEPAGE=' \
        | sed -E "s/\+?HOMEPAGE=(.*?)\s*/\1/g;s/[\"']+//g;s/\\$\\{PN\\}/$PN/g" \
        )
      if [[ "$url" =~ ^https?:// ]]; then
        echo "Open: $url"
        cygstart "$url"
      else
        error "Invalid URL: $url"
      fi
    done
  done
}

function apt-cyg-browse-homepage () # [<package name> ...]
#   Browse homepages of packages.
{
  local basepath="cygwin.com/packages/summary/"
  local pkg summary_path srcsummary_path homepage_url

  pushd "$(apt-cyg-pathof cache)" > /dev/null
  for pkg; do
    summary_path="${basepath}${pkg}.html"
    [ -e "$summary_path" ] || wget -q -N -x "https://$summary_path"
    read srcsummary_path < <(
      cat "$summary_path" \
      | grep "source package" \
      | sed -E 's@.*\shref="([^"]+)".*@'"${basepath}"'\1@g'
      )
    [ -e "$srcsummary_path" ] || wget -q -N -x "https://$srcsummary_path"
    read homepage_url < <(
      cat "$srcsummary_path" \
      | grep "homepage" \
      | sed -E 's/.*\shref="([^"]+)".*/\1/g'
      )
    cygstart "$homepage_url"
  done
  popd
}

function apt-cyg-browse-summary () # [<package names> ...]
#   Browse summaries of packages.
{
  local pkg
  for pkg; do
    cygstart "https://cygwin.com/packages/summary/${pkg}.html"
  done
}

function apt-cyg-listfiles () # [<package names> ...]
# List files 'owned' by package(s).
{
  local lst i sep
  for i; do
    echo -en "$sep"; sep="\n"
    lst="/etc/setup/${i}.lst.gz"
    if [ ! -e "$lst" ]; then
      echo "apt-cyg-listfiles: package '${i}' is not installed"
      continue
    fi
    zcat "${lst}" | sed -e 's:^:/:g'
  done
}

function apt-cyg-ls-categories ()
{
  cat "$(apt-cyg-pathof setup.ini)" \
  | grep category: \
  | sed -r 's/.*: *//g;s/ +/\n/g' \
  | sort \
  | uniq
}

function apt-cyg-ls-pkg-with-category ()
{
  cat "$(apt-cyg-pathof setup.ini)" \
  | awk '$1=="@"{pkg=$2;}$1=="category:"{for(i=2;i<=NF;i++)print $i,pkg}' \
  | align_columns
}

function apt-cyg-setuprc-get () # <section>
{
  setuprc_get_section "$1"
}

function apt-cyg-set-cache () # [<cache>]
{
  (( 0 <$# )) && setuprc_set_section last-cache "$(cygpath -aw "$1")"
}

function apt-cyg-set-mirror () # [<mirrors> ...]
{
  set -- "${@%/}"
  (( 0 < $# )) && setuprc_set_section last-mirror "${@/%/\/}"
  is_official_mirrors_of_cygwin "${@/%/\/}"
}

function apt-cyg-mark-auto () # [<packages> ...]
{
  package_db_change_mark 0 "$@"
}

function apt-cyg-mark-manual () # [<packages> ...]
{
  package_db_change_mark 1 "$@"
}

function apt-cyg-mark-showauto ()
{
  package_db-list | awk '!$3{print $1}'
}

function apt-cyg-mark-showmanual ()
{
  package_db-list | awk  '$3{print $1}'
}

function apt-cyg-call () # [<internal_function> [<args> ...]]
# Call internal function in apt-cyg.
{
  "$@"
}

function apt-cyg-time () # [<internal_function> [<args> ...]]
# Report time consumed to call internal function in apt-cyg.
{
  time "$@"
}

function apt-cyg-filelist () # [<package>]
#   File list like apt-file list
{
  local list_rel_urls
  readarray -t list_rel_urls < <(
    wget -qO- "https://cygwin.com/packages/summary/${1}.html" \
    | grep -E '<a[^>]*>list of files</a>' \
    | sed -E 's:.*<a +href="../([^"]+)".*:\1:g' \
    | grep "^$arch/"
  )
  if (( ${#list_rel_urls[@]} )); then
    wget -qO- "https://cygwin.com/packages/${list_rel_urls[-1]}" \
    | awk '/<pre>$/,/^<\/pre>/' | tail -n+2 | head -n-1 \
    | awk -vpkg="$1" '$0=pkg": /"$4'
  fi
}

function apt-cyg-filesearch () # [<pattern>]
#   File search like apt-file search
{
  local cachedir cachefile cachequery columns line package packages url
  cachedir="/tmp/.apt-cyg.cache/filesearch"
  cachequery="$cachedir/$(sha256sum <<<"$1"|awk '$0=$1')"
  mkdirp "$cachedir"
  
  [ ! -e "$cachequery" ] && cygcheck -p "$1" >"$cachequery"
  readarray -t packages < "$cachequery"
  packages=( "${packages[@]:1}" )
  for line in "${packages[@]}"; do
    columns=( $line )
    url="https://cygwin.com/packages/$arch/${columns[0]%%-[0-9]*}/${columns[0]}"
    cachefile="$cachedir/$(mirror_to_mirrordir "$url")"
    [ ! -e "$cachefile" ] && wget -qO"$cachefile" "$url"
    echo -n "${columns[0]}"
    awk '/<\/pre>/{f=0}/<pre>/{f=1}f' "$cachefile" \
    | grep -E "$1" \
    | awk '($1=$2=$3="")||1'
  done
}

PACKAGEOF_CACHE="/tmp/.apt-cyg-packageof.cache.gz"
function update_packageof_cache ()
{
  local i=0 p q path fn
  local chr=( "=" "-" "/" "|" "\\" )
  local lstgz_stamp="$(find /etc/setup/ -maxdepth 1 -type f -name '*.lst.gz' -exec stat -c %Y {} + | sort | tail -n1)"
  local cache_stamp="$(stat "$PACKAGEOF_CACHE" -c %Y 2>/dev/null || echo 0)"
  local n="$(ls -1 /etc/setup/*.lst.gz|wc -l)"
  
  if (( cache_stamp < lstgz_stamp )) || [ -n "$OPT_FORCE_UPDATE_PACKAGEOF_CACHE" ]; then
    verbose 1 "Updating packageof cache:"
    progress_init
    for path in /etc/setup/*.lst.gz; do
      progress_update $(( i++ )) $n
      local fn="${path##*/}"
      zcat "$path" | awk -v PKGNAME="${fn%.lst.gz}" '{print PKGNAME ": " $0;}'
#      printf "p=%d q=%d i=%d n=%d path=[%s]\n" $p $q $i $n "$path"
    done | gzip >"$PACKAGEOF_CACHE"
    progress_finish
  fi
}

PROGRESS_CHAR=( "=" "-" "/" "|" "\\" )
function progress_init ()
{
  [ -n "$OPT_NO_PROGRESS" ] && return
  #          0        1         2         3         4         5
  #          12345678901234567890123456789012345678901234567890
  echo -ne "|..................................................|\r" >"${verbosefor[0]}"
}

function progress_update () #= current total=100
{
  [ -n "$OPT_NO_PROGRESS" ] && return
  local n=${2:-100}
  local p=$((2 + 50 * ($1    ) / $n))
  local q=$((2 + 50 * ($1 + 1) / $n))
  if (( q - p <= 1 )); then
    printf "\e[%dG%s" $p "${PROGRESS_CHAR[($1 % 4 + 1) * (1 + $p - $q)]}" >"${verbosefor[0]}"
  else
    printf "\e[%dG%s" $p "$(seq $((q - p))|awk '{printf "="}')" >"${verbosefor[0]}"
  fi
}

function progress_finish ()
{
  [ -n "$OPT_NO_PROGRESS" ] && return
  echo >"${verbosefor[0]}"
}

# Lesser Parallel for Embedding
# Copyright (c) 2014 Koichi OKADA. All rights reserved.
# The official repository is:
# https://github.com/kou1okada/lesser-parallel
# This script is distributed under the MIT license.
# http://www.opensource.org/licenses/mit-license.php

LESSER_PARALLEL_MAX_JOBS=${LESSER_PARALLEL_MAX_JOBS:-8}

function lesser-parallel-get-jobs-count ()
{
  jobs -l >/dev/null
  jobs -l | wc -l
}

# Usage: lesser-parallel-restrict-jobs-count MAXJOBS
function lesser-parallel-restrict-jobs-count ()
{
  while [ $(lesser-parallel-get-jobs-count) -ge $1 ]; do
    sleep 0.2
  done
}

# Usage: lesser-parallel [command [arguments]] < list_ot_arguments
function lesser-parallel ()
{
  local cmd arg lines line basename ext PARALLEL_SEQ=1
  readarray -t lines
  for line in "${lines[@]}"; do
    basename="$(basename "$line")"
    ext="${basename##*.}"
    [ "$ext" = "$basename" ] && ext=""
    [ "$ext" != "" ] && ext=".$ext"
    cmd=( )
    for arg; do
      case "$arg" in
      "{}")
        cmd+=( "$line" )
        ;;
      "{.}")
        cmd+=( "$(basename "$line" "$ext")" )
        ;;
      "{/}")
        cmd+=( "$basename" )
        ;;
      "{//}")
        cmd+=( "$(dirname "$line")" )
        ;;
      "{/.}")
        cmd+=( "$(basename "$basename" "$ext")" )
        ;;
      "{#}")
        cmd+=( "$PARALLEL_SEQ" )
        ;;
      *)
        cmd+=( "$arg" )
        ;;
      esac
    done
    
    lesser-parallel-restrict-jobs-count $LESSER_PARALLEL_MAX_JOBS
    
    "${cmd[@]}" &
    PARALLEL_SEQ=$[$PARALLEL_SEQ + 1]
  done
  
  lesser-parallel-restrict-jobs-count 1
}

#/Lesser Parallel for Embedding

function apt-cyg-help ()
{
  usage
}

# process options

arch="${HOSTTYPE:-$(arch)}";arch="${arch/i686/x86}"
apt_cyg_cachedir="${tmp:-/tmp}/.apt-cyg.cache"
noscripts=0
OPT_USER_PICKED=1
noupdate=0
OPT_FILES=()
SUBCOMMAND=""
ignore_case=""
force_remove=""
force_fetch_trustedkeys=""
no_verify=""
OPT_PROXY=${APT_CYG_PROXY:-auto}
OPT_PROXY_REFRESH_INTERVAL=${APT_CYG_PROXY_REFRESH_INTERVAL:-86400} # 86400s = 1day
OPTS4INHERIT=()
SETUP_EXE="setup-$arch.exe"
YES_TO_ALL=false
INITIAL_ARGS=( "$@" )
ARGS=()

function parse_args ()
{
  local unknown_option END_OPTS
  
  while [ $# -gt 0 ]; do
    case "$1" in
      
      --ag)
        OPT_AG="$1"
        shift
        ;;
      
      --benchmark-timeout)
        OPT_BENCHMARK_TIMEOUT="$2"
        shift 2 || break
        ;;

      --use-setuprc)
        warning "Ignore --use-setuprc. This option was removed with issue-24."
        shift
        ;;
      
      --ignore-case|-i)
        ignore_case="$1"
        shift
        ;;
      
      --force-remove)
        force_remove=1
        shift
        ;;
      
      --force-fetch-trustedkeys)
        force_fetch_trustedkeys=1
        shift
        ;;
      
      --force-update-packageof-cache)
        OPT_FORCE_UPDATE_PACKAGEOF_CACHE="$1"
        shift
        ;;
      
      --no-verify|-X)
        OPTS4INHERIT+=( "$1" )
        no_verify=1
        shift
        ;;
      
      --no-check-certificate)
        OPTS4INHERIT+=( "$1" )
        WGET+=( "--no-check-certificate" )
        shift
        ;;
      
      --no-update-setup)
        OPT_NO_UPDATE_SETUP="$1"
        shift
        ;;
      
      --no-header)
        OPT_NO_HEADER="$1"
        shift
        ;;
      
      --proxy|-p)
        OPT_PROXY="$2"
        shift 2 || break
        ;;
      
      --proxy-force-refresh)
        OPT_PROXY_FORCE_REFRESH="$1"
        shift
        ;;
      
      --proxy-refresh-interval)
        OPT_PROXY_REFRESH_INTERVAL="$2"
        shift 2 || break
        ;;
      
      --completion-get-subcommand)
        OPT_COMPLETION_GET_SUBCOMMAND="$1"
        shift
        ;;
      
      --completion-disable-autoupdate)
        OPT_COMPLETION_DISABLE_AUTOUPDATE="$1"
        shift
        ;;
      
      --max-jobs|-j)
        LESSER_PARALLEL_MAX_JOBS="$2"
        shift 2 || break
        ;;
      
      --mirror|-m)
        OPT_MIRROR+=( "$2" )
        shift 2 || break
        ;;
      
      --cache|-c)
        OPT_CACHE="$2"
        shift 2 || break
        ;;
      
      --noscripts)
        noscripts=1
        shift
        ;;
      
      # It will not register the user_picked flag in PACKAGE_DB.
      # This option is for internal use only.
      --no-user-picked)
        OPT_USER_PICKED=0
        shift
        ;;
      
      --noupdate|-u)
        noupdate=1
        shift
        ;;
      
      --ipv4|-4)
        WGET+=( "--prefer-family=IPv4" )
        shift
        ;;
      
      --no-progress)
        OPT_NO_PROGRESS="$1"
        shift
        ;;
      
      --quiet|-q)
        OPT_VERBOSE_LEVEL=-1
        shift
        ;;
      
      --verbose|-v)
        OPT_VERBOSE_LEVEL=2
        shift
        ;;
      
      --help)
        usage
        exit 0
        ;;
      
      --version)
        version
        exit 0
        ;;
      
      --file|-f)
        [ -n "$2" ] && OPT_FILES+=( "$2" )
        shift 2 || break
        ;;
      
      --)
        END_OPTS="$1"
        shift
        break
        ;;
      
      -*)
        unknown_option="$1"
        break
        ;;
      
      *)
        if [ -z "$SUBCOMMAND" ]; then
          SUBCOMMAND="$1"
        else
          ARGS+=( "$1" )
        fi
        shift
        
        ;;
      
    esac
  done
  [ -n "$END_OPTS" ] && while (( 0 < "$#" )); do ARGS+=( "$1" ); shift; done
  
  if [ -n "$OPT_COMPLETION_GET_SUBCOMMAND" ]; then
    echo "$SUBCOMMAND"
    exit
  fi
  
  if [ -n "$unknown_option" ]; then
    error "Unknown option: $unknown_option"
    exit 1
  fi
  
  if [ $# -gt 0 ]; then
    error "Number of parameters is not enough: $@"
    exit 1
  fi
  
} #/parse_args

parse_args "$@"

: ${OPT_VERBOSE_LEVEL:=$VERBOSE}
VERBOSE=$OPT_VERBOSE_LEVEL

[ "${#OPT_MIRROR[@]}" -gt 0 ] && apt-cyg-set-mirror "${OPT_MIRROR[@]}"
[ "${#OPT_CACHE[@]}"  -gt 0 ] && apt-cyg-set-cache  "${OPT_CACHE}"

if [ -z "$GPG" -a -z "$no_verify" ]; then
  error "GnuPG is not installed. Prease install gnupg package or use -X option."
  exit 1
fi

for file in "${OPT_FILES[@]}"; do
  if [ -f "$file" ]; then
    readarray -t -O ${#ARGS[@]} ARGS < "$file"
  else
    echo File $file not found, skipping
  fi
done

update_verbosefor

if [ -n "$OPT_AG" ]; then
  read AG < <( type -p ag 2>/dev/null )
  if [ -z "$AG" ]; then
    warning "ag is not found. ignore option: $OPT_AG"
    unset OPT_AG
  fi
fi



function apt-cyg-update ()
{
  findworkspace
  getsetup
}


function apt-cyg-show ()
{
  package_db-version_check
  [ -z "$OPT_NO_HEADER" ] && echo "The following packages are installed:"
  package_db-list | awk '($3="")||1' | align_columns
}


function apt-cyg-find ()
{
  local pkg
  
  checkpackages "$@"
  findworkspace
  getsetup
  
  for pkg do
    echo ""
    echo "Searching for installed packages matching $pkg:"
    package_db-list | awk '$1~query{print $1}' query="$pkg" IGNORECASE="$ignore_case"
    echo ""
    echo "Searching for installable packages matching $pkg:"
    awk -v query="$pkg" -v IGNORECASE="$ignore_case" \
      'BEGIN{RS="\n\n@ "; FS="\n"; ORS="\n"} {if ($1 ~ query) {print $1}}' \
      setup.ini
  done
}


function apt-cyg-category () # <category>
#   List all packages in given <category>.
{
  apt-cyg-ls-pkg-with-category \
  | awk -vcategory="$1" '$1==category {print $2}'
}


function apt-cyg-describe ()
{
  local pkg exact
  
  checkpackages "$@"
  findworkspace
  getsetup
  for pkg do
    exact="$(grep -Fx "@ $pkg" setup.ini)"
    awk -v query="$pkg" -v exact="${exact:+1}" -v IGNORECASE="$ignore_case" \
      'BEGIN{RS="\n\n@ "; FS="\n"} (exact?$1==query:$1~query){print "@ "$0"\n"}' \
      setup.ini
  done
}

function apt-cyg-packageof ()
{
  if [ -z "$OPT_AG" ]; then
    update_packageof_cache
    zcat "$PACKAGEOF_CACHE" | grep "$@"
  else
    "$AG" -z "$@" /etc/setup/*.lst.gz
  fi
}

function apt-cyg-install ()
{
  local pkg
  local script
  
  package_db-version_check
  checkpackages "$@"
  findworkspace
  getsetup
  
  for pkg do
    if package_db-is_registered "$pkg"; then
      echo Package $pkg is already installed, skipping
      continue
    fi
    verbose 0 ""
    verbose 0 "Installing $pkg"
    
    # look for package and save desc file
    
    mkdirp "release/$pkg"
    awk > "release/$pkg/desc" -v package="$pkg" \
      'BEGIN{RS="\n\n@ "; FS="\n"} {if ($1 == package) {desc = $0; px++}} \
       END {if (px == 1 && desc != "") print desc; else print "Package not found"}' \
       setup.ini
    local desc="$(< "release/$pkg/desc")"
    if [ "$desc" = "Package not found" ]; then
      verbose 0 "Package $pkg not found or ambiguous name, exiting"
      rm -r "release/$pkg"
      exit 1
    fi
    verbose 0 "Found package $pkg"
    
    # download and unpack the bz2 file
    
    # pick the latest version, which comes first
    local install="$(awk '/^install: / { print $2; exit }' "release/$pkg/desc")"
    
    if [ -z "$install" ]; then
      verbose 0 "Could not find \"install\" in package description: obsolete package?"
      exit 1
    fi
    
    local file="$(basename "$install")"
    cd "release/$pkg"
    wget -Nc $mirror/$install
    
    # check the SHA512 hash
    local digest="$(awk '/^install: / { print $4; exit }' "desc")"
    if ! hash_check <<<"${digest} *${file}" ; then
      verbose 0 "error: hash did not match: $file"
      exit 1
    fi
    
    verbose 0 "Unpacking..."
    tar > "/etc/setup/$pkg.lst" xvf "$file" -C / --numeric-owner
    gzip -f "/etc/setup/$pkg.lst"
    cd ../..
    
    
    # update the package database
    
    package_db-register "$file" "$OPT_USER_PICKED"
    
    
    # recursively install required packages
    
    local requires="$(grep -E "^(requires|depends2): " "release/$pkg/desc" | sed -re 's/^(requires|depends2): *(.*[^ ]) */\2/g' -e 's/,? +/ /g')"
    
    local warn=0
    if [ -n "$requires" ]; then
      verbose 0 "Package $pkg requires the following packages, installing:"
      verbose 0 "$requires"
      for package in $requires; do
        if package_db-is_registered "$package"; then
          verbose 0 "Package $package is already installed, skipping"
          continue
        fi
        apt-cyg "${OPTS4INHERIT[@]}" --proxy inherit --noscripts --no-user-picked install $package
        if [ $? -ne 0 ]; then warn=1; fi
      done
    fi
    if [ $warn -ne 0 ]; then
      warning "some required packages did not install, continuing"
    fi
    
    # run all postinstall scripts
    
    apt-cyg-repair-postinstall
    
    local postinstalls=( /etc/postinstall/*.?(da)sh )
    if [ -e "$postinstalls" -a $noscripts -ne 1 ]; then
      verbose 0 "Running postinstall scripts"
      for script in "${postinstalls[@]}"; do
        $script \
        && [[ $script != /*/?p_* ]] \
        && mv $script $script.done
      done
    fi
    
    verbose 0 "Package $pkg installed"
    
  done
}


function apt-cyg-remove()
{
  local pkg
  local req
  
  package_db-version_check
  checkpackages "$@"
  for pkg do
    if ! package_db-is_registered "$pkg"; then
      verbose 0 "Package $pkg is not installed, skipping"
      continue
    fi
    
    local dontremove="cygwin coreutils gawk bzip2 tar xz wget bash"
    for req in $dontremove; do
      if [ "$pkg" = "$req" ]; then
        verbose 0 "apt-cyg cannot remove package $pkg, exiting"
        exit 1
      fi
    done
    
    if [ ! -e "/etc/setup/$pkg.lst.gz" -a -z "$force_remove" ]; then
      verbose 0 "Package manifest missing, cannot remove $pkg.  Exiting"
      exit 1
    fi
    verbose 0 "Removing $pkg"
    
    # run preremove scripts
    
    local i postinstalls preremoves
    readarray -t postinstalls < <(zgrep -E "^etc/postinstall/.*[.](da)?sh$" "/etc/setup/${pkg}.lst.gz" | awk '{print "/"$0}')
    readarray -t preremoves   < <(zgrep -E "^etc/preremove/.*[.](da)?sh$"   "/etc/setup/${pkg}.lst.gz" | awk '{print "/"$0}')
    for i in "${preremoves[@]}"; do
      verbose 0 "Running: ${i}"
      "${i}"
    done
    
    gzip -cd "/etc/setup/$pkg.lst.gz" | awk '/[^\/]$/{printf("/%s\0", $0)}' | xargs -0 rm -f
    gzip -cd "/etc/setup/$pkg.lst.gz" | awk '/[^./].*[/]$/{printf("/%s\0", $0)}' | sort -zr | xargs -0 rmdir --ignore-fail-on-non-empty
    rm -f "${postinstalls[@]/%/.done}" "/etc/setup/$pkg.lst.gz"
    package_db-unregister "$pkg"
    verbose 0 "Package $pkg removed"
    
  done
}

function invoke_subcommand ()
{
  local SUBCOMMAND="${@:1:1}"
  local ARGS=( "${@:2}" )
  local ACTION="apt-cyg-${SUBCOMMAND:-help}"
  if type "$ACTION" >& /dev/null; then
    assert_command_exists iconv
    "$ACTION" "${ARGS[@]}"
  else
    error "unknown subcommand: $SUBCOMMAND"
    exit 1
  fi
}

[ -d "$PWD" ] || {
  warning "Missing current directory: $PWD" \
          "\nNote that some functions, which require access to current directory, will be failed."
}

invoke_subcommand "$SUBCOMMAND" "${ARGS[@]}"
