#!/bin/bash

# Original script by Jim Pick <jim@jimpick.com>, GPL'd of course
#
# Completely rewritten several times with many new features and
# enhancements (plus numerous patches and by others, see the changelog)
# by Craig Sanders <cas@taz.net.au>

DLOCATE="$0"
DLOCATEDB=/var/lib/dlocate/dlocatedb
DPKGLIST=/var/lib/dlocate/dpkg-list
DPKG_INFO=/var/lib/dpkg/info
DPKG_ARCH=$(dpkg --print-architecture)

COMPRESS_DLOCATEDB=0
GREP='grep'
[ -e /etc/default/dlocate ] && . /etc/default/dlocate
[ "$COMPRESS_DLOCATE" = "1" ] && GREP='zgrep'


usage() {
  cat <<__EOF__
Usage: dlocate [<option>...] [<command>]

Commands:
  <pattern>...            List records that match either package or files names.
  -S <filename>...        List records that match filenames, regexp emulation
                            of 'dpkg -S'.
  -L <package>...         List all files in package.
  -l <package>...         Regexp-enhanced emulation of 'dpkg -l'.
  -s <package>...         Print package's status.
  --ls <package>...       'ls -ldF' of all files in package.
  --du <package>...       'du -sck' of all files in package.
  --conf <package>...     List conffiles in package.
  --lsconf <package>...   'ls -ldF' of conffiles in package.
  --md5sum <package>...   List package's md5sums (if any).
  --md5check <package>... Check package's md5sums (if any).
  --man <package>...      List package's man pages (if any).
  --lsman <package>...    List full path/filenames of man pages.
  --lsbin <package>...    List full path/filenames of executable files.
  --lsdir <package>...    List only the directories in package.
  -K                      List installed kernel & related packages.
  -k                      Detailed list of installed kernel & related packages.
  --                      Stop processing commands and options. Remainder of
                            command-line is filename(s)/package-name(s).

Options:
  -f, --filename-only     Strip 'package: ' prefix from search output.
  -p, --package-only      Output package names only when searching.

Regexp options (see grep(1) for details):
  -E, --extended-regexp
  -F, --fixed-strings
  -G, --basic-regexp
  -P, --perl-regexp
  -w, --word-regexp       Restrict matches to whole words.
  -i, --ignore-case       Case-insensitive match.

General options:
  -c, --columns [<cols>]  Set COLUMN width (defaults to entire terminal width).
  -C, --color             Colorize -l or -s output (needs 'supercat' command).
      --debug             Debug output.
  -v, --verbose           Verbose output.
  -V, --version           Display dlocate's version number and exit.
  -h, --help              Display this help message and exit.
__EOF__
}

usage_error() {
  printf "%s\n" "$*" >/dev/stderr
  exit 1
}

warn() {
  printf "%s\n" "$*" >&2
}

dlocate_version() {
  VERSION_BANNER=$(dpkg-query -W -f '${version}' dlocate)
  echo "dlocate version $VERSION_BANNER"
  exit 0
}

dlocate_option_error() {
  echo "dlocate: unknown option '$1'"
  echo
  echo "Use: 'dlocate -- $1' if you want to search for '$1'"
  exit 1
}

# Thanks to "A. Costa" <agcosta@gis.net> in https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=407412
# What provided an installed virtual package?
# What programs provide a given virtual package?
deb_provided_by() {
  grep-status -F Provides "$1" -a -F Status installed -ns Package:
}
list_deb_providers() {
  grep-available -F Provides "$1" -ns Package:
}


OPTION='DEFAULT'

# default to extended regexp
RE_TYPE="-E"
RE_SEPARATOR="|"

# default to case-sensitive
IGNORE_CASE=''
# default to non- word-based searches
WORD_RE=''
WSEP1='\<'
WSEP2='\>'

# output filters for -S and DEFAULT option
OUTPUT_FILTER="none"

output_filter() {
  if [ -n "$VERBOSE" ]; then
    warn "OUTPUT FILTER: $OUTPUT_FILTER"
  fi

  case "$OUTPUT_FILTER" in
  filenames)
    awk -F': ' '{ print $NF }'
    ;;
  packages)
    awk -F': ' '{ print  $1 }' | sort -u
    ;;
  *)
    cat
    ;;
  esac
}

colorize_dpkg() {
  case "$COLORIZE" in
  1)
    spc -c "$CCONFD"
    ;;
  *)
    cat
    ;;
  esac
}

colorize_packages() {
  case "$COLORIZE" in
  1)
    spc -c "$CCONFP"
    ;;
  *)
    cat
    ;;
  esac
}

VERBOSE=''
KERNEL=''

COLORIZE=''
CCONFP=''
CCONFD=''

for d in /etc/supercat /usr/share/dlocate ~/.spcrc ./; do
  [ -e "$d/spcrc-dpkg-l"  ] && CCONFD="$d/spcrc-dpkg-l"
  [ -e "$d/spcrc-package" ] && CCONFP="$d/spcrc-package"
done

# getopt is only safe if GETOPT_COMPATIBLE is not set.
unset GETOPT_COMPATIBLE

# POSIXLY_CORRECT disables getopt parameter shuffling, so nuke it.
# parameter shuffling moves all non-option args to the end, after
# all the option args.  e.g. args like "-x -y file1 file2 file3 -o optval"
# become "-x -y -o optval -- file1 file2 file3"
unset POSIXLY_CORRECT

OPTS_SHORT='hVvdc:KkSLlsiEFGPwfpC'
OPTS_LONG='help,version,verbose,debug,columns:,ls,du,conf,lsconf,md5sum,md5check,man,lsman,lsbin,ignore-case,extended-regexp,fixed-strings,basic-regexp,perl-regexp,filename-only,package-only,lsdir,colour,color'

# check options and shuffle them
# use getopt's `-a` option to provide backwards-compatibility
# with the old, broken option handling.
TEMP=$(getopt -a -o "$OPTS_SHORT" --long "$OPTS_LONG" -n "$0" -- "$@")

if [ $? != 0 ]; then
  usage_error "Terminating..."
fi

# assign the re-ordered options & args to this shell instance
eval set -- "$TEMP"

while true; do
  case "$1" in
  -h|--help)
    usage
    shift
    ;;
  -V|--vers*)
    dlocate_version
    shift
    ;;
  -v|--verb*|--deb*)
    VERBOSE=1
    shift
    ;;
  -C|--colo*)
    COLORIZE=1
    shift
    ;;
  -c|--col*)
    COLUMNS="${2:-80}"
    shift 2
    ;;

  -E|--ext*)
    RE_TYPE='-E'
    shift
    ;;
  -F|--fix*)
    RE_TYPE='-F'
    RE_SEPARATOR=$'\n'
    shift
    ;;
  -G|--bas*)
    RE_TYPE='-G'
    shift
    ;;
  -P|--per*)
    RE_TYPE='-P'
    WSEP1='\b'
    WSEP2='\b'
    shift
    ;;
  -w|--wor*)
    WORD_RE="-w"
    shift
    ;;
  -i|--ign*)
    IGNORE_CASE='-i'
    shift
    ;;

  -f|--fil*)
    OUTPUT_FILTER='filenames'
    shift
    ;;
  -p|--pac*)
    OUTPUT_FILTER='packages'
    shift
    ;;

  -K|-k)
    OPTION="$1"
    KERNEL=1
    shift
    ;;
  -S)
    OPTION="$1"
    shift
    ;;
  -L)
    OPTION="$1"
    shift
    ;;
  -l)
    OPTION="$1"
    shift
    ;;
  -s)
    OPTION="$1"
    shift
    ;;
  --ls)
    OPTION="$1"
    shift
    ;;
  --du)
    OPTION="$1"
    shift
    ;;
  --conf)
    OPTION="$1"
    shift
    ;;
  --lsconf)
    OPTION="$1"
    shift
    ;;
  --md5sum)
    OPTION="$1"
    shift
    ;;
  --md5check)
    OPTION="$1"
    shift
    ;;
  --man)
    OPTION="$1"
    shift
    ;;
  --lsman)
    OPTION="$1"
    shift
    ;;
  --lsbin)
    OPTION="$1"
    shift
    ;;
  --lsdir)
    OPTION="$1"
    shift
    ;;

  --)
    shift
    break
    ;;
  *)
    usage_error "unknown option $1"
    ;;
  esac
done

if [ -z "$*${KERNEL}" ]; then
  usage_error "missing search criteria"
fi

PKGS=("$@")

joinargs() {
  local IFS="$1"
  shift
  echo "$*"
}

PKGS_REGEXP=$(joinargs "$RE_SEPARATOR" "$@")
if [ "$RE_TYPE" = "-G" ]; then
  PKGS_REGEXP=$(printf "%s" "$PKGS_REGEXP" | sed -E -e 's/\|/\\|/g')
fi

FILES_REGEXP="($PKGS_REGEXP)"
if [ "$WORD_RE" = "-w" ]; then
  FILES_REGEXP="${WSEP1}$FILES_REGEXP${WSEP2}"
fi

if [ -z "$COLUMNS" ]; then
  COLUMNS=$(stty -a 2>&- \
    | sed -ne '/columns/s/.*columns \([0-9]*\)[^0-9].*/\1/p'
  )
fi

if [ 0"$COLUMNS" -lt 80 ]; then
  COLUMNS=80
fi

if [ -n "$VERBOSE" ]; then
  declare -p COLUMNS TEMP PKGS OPTION RE_SEPARATOR PKGS_REGEXP FILES_REGEXP \
    | sed -e 's/^declare [^ ]\+ //' >&2
  echo >&2
fi

if [ -z "$PKGS_REGEXP" ]; then
  PKGS_REGEXP='^$'
fi

if [ "$OPTION" = '-l' ]; then
  ((fieldw = (COLUMNS - 24) / 4))

  # dpkg uses ((fieldd = fieldw * 2 + 16));
  #
  # limiting the output to COLUMNS-2 characters and losing up to
  # additional 3 characters due to the rounding error.

  ((fieldd = COLUMNS - fieldw * 2 - 6))

  fmt_eq=$(echo ==================================================== \
    | cut -c-$fieldw)

  HEADER=$( printf \
"Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Installed/Config-files/Unpacked/Failed-config/Half-installed
|/ Err?=(none)/Hold/Reinst-required/X=both-problems (Status,Err: uppercase=bad)
||/ %-${fieldw}s %-${fieldw}s %s
+++-${fmt_eq}-${fmt_eq}-${fmt_eq}${fmt_eq}==================\\n" \
              Name Version Description)

  BODY_FMT_STRING="%-3s %-${fieldw}.${fieldw}s %-${fieldw}.${fieldw}s %-${fieldd}.${fieldd}s\\n"

  if [ -n "$VERBOSE" ]; then
    warn RUNNING: $GREP $RE_TYPE $WORD_RE $IGNORE_CASE -- "'$PKGS_REGEXP'" "$DPKGLIST"
  fi

  BODY=$($GREP $RE_TYPE $WORD_RE $IGNORE_CASE -- "$PKGS_REGEXP" "$DPKGLIST" \
    | awk -F'\t' -v fmt="$BODY_FMT_STRING" '{ printf fmt, $1, $2, $3, $4 }' \
    | sed -e 's/ *$//g'
  )

  if [ -n "$BODY" ]; then
    printf "%s\n" "$HEADER" "$BODY" | colorize_dpkg
  fi
elif [ "$OPTION" = '-S' ]; then
  if [ "$RE_TYPE" = '-F' ]; then
    warn "Error: -F Fixed String searches are incompatible with -S"
    exit 1
  fi
  PREFIX="^([-a-zA-Z0-9_.+]+:|diversion by )"
  if [ "$RE_TYPE" = "-G" ]; then
    PREFIX="^\([-a-zA-Z0-9_.+]+:\|diversion by \)"
  fi

  if [ -n "$VERBOSE" ]; then
    warn RUNNING: $GREP $RE_TYPE $IGNORE_CASE -- "'$PREFIX.*$FILES_REGEXP'" "$DLOCATEDB"
  fi
  $GREP $RE_TYPE $IGNORE_CASE -- "$PREFIX.*$FILES_REGEXP" "$DLOCATEDB" \
    | output_filter
  result=${PIPESTATUS[0]}
elif [ "$OPTION" = 'DEFAULT' ]; then
  if [ -n "$VERBOSE" ]; then
    warn RUNNING: $GREP $RE_TYPE $IGNORE_CASE -- $WORD_RE "'$PKGS_REGEXP'" "$DLOCATEDB"
  fi
  $GREP $RE_TYPE $IGNORE_CASE $WORD_RE -- "$PKGS_REGEXP" "$DLOCATEDB" \
    | output_filter
  result=${PIPESTATUS[0]}
  #[ "$result" -eq 1 ] && echo "not in package: $PKGS_REGEXP" >&2
elif [ "$OPTION" = '-s' ]; then
  for p in "${PKGS[@]}"; do
    grep-status -P -X "$p" | colorize_packages
    result=$?
    if [ "$result"0 -ne 0 ]; then
      (
        printf "%s\n" "Package: $p (virtual package, currently provided by '$(deb_provided_by "$p")')"
        printf "%s\n\n" "X-Available-Providers: $(list_deb_providers "$p" \
          | sort -u \
          | xargs \
          | sed -e 's/ / | /g' \
        )"
      ) | colorize_packages
      # use X-Available-Provides: to provide a hint that it's a synthetic status field.
    fi
  done
elif [ "$OPTION" = '-k' ]; then
  kmodules=(
    $(grep-status -s Package -n -e ' (module-assistant|dkms)([, ]|$)' \
        | grep -v 'module-assistant')
  )
  kmods_re=$(joinargs '|' "${kmodules[@]}")

  kpkgs_re='^(linux-(image|source|headers|doc|debug|kbuild|perf|support|tools|manual)|gnumach-(image|common|dev))'
  if [ -n "$kmods_re" ]; then
    kpkgs_re="$kpkgs_re|^($kmods_re)$"
  fi

  if [ -n "$VERBOSE" ]; then
    warn RUNNING: grep-status -P -e "'$kpkgs_re'"
  fi
  grep-status -P -e "$kpkgs_re" \
    | grep-dctrl -n -s Package -F status " installed" - \
    | sort -V
  result=$?
elif [ "$OPTION" = '-K' ]; then
  KERN_RE=$(joinargs '|' $("$DLOCATE" -k))

  if [ -n "$VERBOSE" ]; then
    warn RUNNING: dlocate -E -w -l "'($KERN_RE)'"
  fi
  "$DLOCATE" -c "$COLUMNS" -E -w -l "($KERN_RE)" | grep -v '^.n'
  result=$?
else
  for PKG in "${PKGS[@]}"; do
    PLIST="$DPKG_INFO/$PKG.list"
    PLISTARCH="$DPKG_INFO/$PKG:$DPKG_ARCH.list"
    if [ ! -s "$PLIST" ] && [ -s "$PLISTARCH" ]; then
      PLIST="$PLISTARCH"
    fi

    PCONF="$DPKG_INFO/$PKG.conffiles"
    PCONFARCH="$DPKG_INFO/$PKG:$DPKG_ARCH.conffiles"
    if [ ! -s "$PCONF" ] && [ -s "$PCONFARCH" ]; then
      PCONF="$PCONFARCH"
    fi

    PMD5="$DPKG_INFO/$PKG.md5sums"
    PMD5ARCH="$DPKG_INFO/$PKG:$DPKG_ARCH.md5sums"
    if [ ! -s "$PMD5" ] && [ -s "$PMD5ARCH" ]; then
      PMD5="$PMD5ARCH"
    fi

    case "$OPTION" in
    -L)
      if [ -s "$PLIST" ]; then
          cat "$PLIST"
          result=$?
      else
          warn "Package $PKG is not installed or $PLIST is empty."
          result=1
      fi
      ;;
    --ls)
      if [ -s "$PLIST" ]; then
          xargs -d '\n' -r ls -ldF < "$PLIST"
          result=$?
      else
          warn "Package $PKG is not installed or $PLIST is empty."
          result=1
      fi
      ;;
    --du)
      if [ -s "$PLIST" ]; then
        sed '1d' "$PLIST" \
          | xargs -d '\n' -r ls -1dF \
          | sed -e '/@$\|\/$/d; s/\*$//;' \
          | xargs du -sck
          result=$?
      else
        warn "Package $PKG is not installed or $PLIST is empty."
        result=1
      fi
      ;;
    --conf)
      if [ -s "$PCONF" ]; then
        cat "$PCONF"
        result=$?
      else
        warn "Package $PKG is not installed or has no conffiles."
        result=1
      fi
      ;;
    --lsconf)
      if [ -s "$PCONF" ]; then
        xargs -a "$PCONF" -d '\n' -r ls -ldF
        result=$?
      else
        warn "Package $PKG is not installed or has no conffiles."
        result=1
      fi
      ;;
    --md5sum)
      if [ -s "$PMD5" ]; then
        cat "$PMD5"
        result=$?
      else
        warn "Package $PKG is not installed or has no md5sums."
        result=1
      fi
      ;;
    --md5check)
      MD5TMP=$(mktemp)
      DIVTMP=$(mktemp)
      #MD5TMP='/tmp/md5temp'
      #DIVTMP='/tmp/divtemp'
      if [ -s "$PMD5" ]; then
        pushd / >/dev/null 2>&1
        cat "$PMD5" \
          | md5sum -c - 2>/dev/null \
          | tee "$MD5TMP"
        result=$?
        if grep -q FAILED "$MD5TMP"; then
          echo
          echo "Some files in package '$PKG' may have been diverted by other packages"
          echo "which will cause false FAILED errors:"
          awk -F: '/FAILED/ { print "/"$1 }' "$MD5TMP" \
            | xargs -d'\n' -r dpkg-divert --list \
            | tee "$DIVTMP"
          if [ -s "$DIVTMP" ]; then
            echo
            echo Checking diverted files:
            awk '/^diversion of/ {
                   gsub(/ \//," ");
                   print "s|"$3"|"$5"|gp"
                 }' "$DIVTMP" \
              | sed -n -f /dev/stdin <("$DLOCATE" -md5sum perl) \
              | md5sum -c - 2>/dev/null
          fi
        fi
        popd >/dev/null 2>&1
      else
        warn "Package $PKG is not installed or has no md5sums."
        result=1
      fi
      rm -f "$MD5TMP" "$DIVTMP"
      ;;
    --lsman)
      if [ -s "$PLIST" ]; then
        "$DLOCATE" -L "$PKG" \
          | grep -E '/man[0-9]+/'
        result=$?
      else
        warn "Package $PKG is not installed or $PLIST is empty."
        result=1
      fi
      ;;
    --man)
      if [ -s "$PLIST" ]; then
        "$DLOCATE" -L "$PKG" \
          | grep -E '/man[0-9]+/' \
          | sed -e 's/\.gz$//' \
                -e 's:.*/::' \
                -e 's/\(^.*\)\.\(.*\)/\2 \1/' \
          | sort -u
        result=$?
      else
        warn "Package $PKG is not installed or $PLIST is empty."
        result=1
      fi
      ;;
    --lsbin)
      if [ -s "$PLIST" ]; then
        "$DLOCATE" -L $PKG \
          | xargs -d '\n' -r stat -c '%A %n' \
          | awk '/^-.{2,8}[xs]/ { print $2 }'
        result=$?
      else
        warn "Package $PKG is not installed or $PLIST is empty."
        result=1
      fi
      ;;
    --lsdir)
      if [ -s "$PLIST" ]; then
        "$DLOCATE" -L $PKG \
          | xargs -d '\n' -r stat -c '%A %n' \
          | awk '/^d/ { print $2 }'
        result=$?
      else
        warn "Package $PKG is not installed or $PLIST is empty."
        result=1
      fi
      ;;
    esac
  done
fi

if [ -n "$result" ]; then
  exit $result
fi

# ex: ts=2 sts=2 sw=2 et filetype=sh
