~dricottone/my-utils

330d00717dc2d3f79860a72999250f9b764b65a7 — Dominic Ricottone 2 years ago 9c7265d
Refactored archive logic

Almost all (un)archive code now lives in bash libraries. To add support
for a new compression or encryption algorithm, updates only need to be
made in `archive.bash`, `unarchive.bash`, and the parser in
`mktar-batch`.

`mktar-batch` has eclipsed `mktar` when it comes to explicit command
line options. Much of the logic wqas removed. `mktar` now soleyl exists
as a clever utility that guesses encryption/compression preferences
based on the output filename. Filename is now mandatory.

Refactored tests to reflect these changes.

Standardized use of here-strings in a couple of places.
M archives/Makefile => archives/Makefile +6 -0
@@ 15,6 15,9 @@ install:
	install -m755 untar $(BIN_DIR)/untar
	install -m755 zipls $(BIN_DIR)/zipls

	install -m755 archive.bash $(LIB_DIR)/archive.bash
	install -m755 unarchive.bash $(LIB_DIR)/unarchive.bash

	install -m755 completion.bash $(COMP_DIR)/myutils_archives.bash

uninstall:


@@ 28,6 31,9 @@ uninstall:
	rm $(BIN_DIR)/untar
	rm $(BIN_DIR)/zipls

	rm $(LIB_DIR)/archive.bash
	rm $(LIB_DIR)/unarchive.bash

	rm $(COMP_DIR)/myutils_archives.bash

test: clean

M archives/README.md => archives/README.md +18 -5
@@ 11,19 11,32 @@ expect `tar` to support Zstandard, which isn't necessarily POSIX standard.*
Executable      |Description                                                   |Extra Dependencies
:---------------|:-------------------------------------------------------------|:------------------------------------------
epub            |Dumps HTML from an 'epub' e-book archive                      |`bash`, `zipinfo`, `unzip`, `w3m`
mktar           |Archive utility                                               |`bash`
mktar-batch     |Archive utility for scripting                                 |`bash`, `expect`
mktar           |Archive utility                                               |`bash`, `age`\*
mktar-batch     |Archive utility for scripting                                 |`bash`, `age`\*
rmtar           |Delete 'tar' archive files                                    |
rmzip           |Delete 'zip' archive files                                    |
tarcat          |Print contents of `tar` archive file(s)                       |
tarls           |List files within `tar` archive file(s)                       |
untar           |Wrapper around `tar` for easier decompression                 |
tarcat          |Unarchive utility for scripting                               |`bash`, `age`\*
tarls           |List files within archive files                               |`bash`, `age`\*
untar           |Unarchive utility                                             |`bash`, `age`\*
zipls           |List files within `zip` archive file(s)                       |`zipinfo`

*All* scripts support `-h` and `--help` for printing built-in documentation.

*All* scripts do nothing if no input arguments are given.

\*These utilities use a fork of `age` that supports plaintext passphrases.
See [git.dominic-ricottone.com/age.git].

## Notes

Per FreeBSD's `tar(1)`:

> For maximum portability, scripts that invoke tar should use the bundled-
> argument format above, should limit themselves to the c, t, and x modes,
> and the b, f, m, v, and w options.

I have noted that pretty much any viable implementation also supports `O` (extract to stdout).
That includes BusyBox.

## To-Do


A archives/archive.bash => archives/archive.bash +273 -0
@@ 0,0 1,273 @@
#!/bin/bash

# See mktar-batch for simple usage

archive() {
  local tar_bin=/usr/bin/tar
  local gpg_bin=/usr/bin/gpg
  local age_bin=age

  local encrypt=""
  local encrypt_cmd=""
  local encrypt_flags=""
  local passphrase_flags=""
  local passphrase=""
  local archive_cmd=""
  local archive_flags=""
  local archive_late_flags=""
  local archive_fn=""

  local positional=()

  while [[ $# -gt 0 ]]; do
    case $1 in

    --archive=tar)
      encrypt=0
      encrypt_cmd=""
      encrypt_flags=""
      passphrase_flags=""
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cf"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.age)
      encrypt=1
      encrypt_cmd="${age_bin}"
      encrypt_flags="${encrypt_flags} --encrypt --passphrase --output"
      passphrase_flags="--plaintext"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}c"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.gpg)
      encrypt=1
      encrypt_cmd="${gpg_bin}"
      encrypt_flags="${encrypt_flags} --symmetric --output"
      passphrase_flags="--batch --passphrase"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}c"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.gz)
      encrypt=0
      encrypt_cmd=""
      encrypt_flags=""
      passphrase_flags=""
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}czf"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.gz.age)
      encrypt=1
      encrypt_cmd="${age_bin}"
      encrypt_flags="${encrypt_flags} --encrypt --passphrase --output"
      passphrase_flags="--plaintext"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cz"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.gz.gpg)
      encrypt=1
      encrypt_cmd="${gpg_bin}"
      encrypt_flags="${encrypt_flags} --symmetric --output"
      passphrase_flags="--batch --passphrase"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cz"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.xz)
      encrypt=0
      encrypt_cmd=""
      encrypt_flags=""
      passphrase_flags=""
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cJf"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.xz.age)
      encrypt=1
      encrypt_cmd="${age_bin}"
      encrypt_flags="${encrypt_flags} --encrypt --passphrase --output"
      passphrase_flags="--plaintext"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cJ"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.xz.gpg)
      encrypt=1
      encrypt_cmd="${gpg_bin}"
      encrypt_flags="${encrypt_flags} --symmetric --output"
      passphrase_flags="--batch --passphrase"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cJ"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.zst)
      encrypt=0
      encrypt_cmd=""
      encrypt_flags=""
      passphrase_flags=""
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cf"
      archive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.zst.age)
      encrypt=1
      encrypt_cmd="${age_bin}"
      encrypt_flags="${encrypt_flags} --encrypt --passphrase --output"
      passphrase_flags="--plaintext"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}c"
      archive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.zst.gpg)
      encrypt=1
      encrypt_cmd="${gpg_bin}"
      encrypt_flags="${encrypt_flags} --symmetric --output"
      passphrase_flags="--batch --passphrase"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}c"
      archive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.bz2)
      encrypt=0
      encrypt_cmd=""
      encrypt_flags=""
      passphrase_flags=""
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cjf"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.bz2.age)
      encrypt=1
      encrypt_cmd="${age_bin}"
      encrypt_flags="${encrypt_flags} --encrypt --passphrase --output"
      passphrase_flags="--plaintext"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cj"
      archive_late_flags=""
      shift
      ;;

    --archive=tar.bz2.gpg)
      encrypt=1
      encrypt_cmd="${gpg_bin}"
      encrypt_flags="${encrypt_flags} --symmetric --output"
      passphrase_flags="--batch --passphrase"
      archive_cmd="${tar_bin}"
      archive_flags="${archive_flags}cj"
      archive_late_flags=""
      shift
      ;;

    --archive=*)
      error_msg "unsupported archive type '${1}'"
      ;;

    --name)
      archive_fn="$2"
      shift; shift
      ;;

    --passphrase)
      passphrase="$2"
      shift; shift
      ;;

    *)
      positional+=("$1")
      shift
      ;;
    esac
  done

  # error if no input files given
  if [[ "${#positional[@]}" -lt 1 ]]; then
    error_msg "no input files given"
    usage_msg
  fi

  # error if input file does not exist
  for target in "${positional[@]}"; do
    if [[ ! -f "$target" ]] && [[ ! -d "$target" ]]; then
      error_msg "no such file '${target}'"
    fi
  done

  # error if no archive file given
  if [[ -z "$archive_fn" ]]; then
    error_msg "no archive file given"
    usage_msg
  fi

  # error if archive exists and should not be overwritten
  if ! prompt_overwrite "$archive_fn"; then
    error_msg "archive '${archive_fn}' cannot be overwritten"
    usage_msg
  fi

  # error if archive is an input file
  if contains "$archive_fn" "${positional[@]}"; then
    error_msg "archive '${archive_fn}' cannot be an input file"
  fi

  # handle passphrase
  if [[ ! -z "$passphrase_flags" && ! -z "$passphrase" ]]; then
    passphrase_flags="${passphrase_flags} ${passphrase}"
  else
    passphrase_flags=
  fi

  # handle debugging information
  local verbosity=/dev/stderr
  if [[ "$verbose" -ne 1 ]]; then
    verbosity=/dev/null
  elif [[ "$encrypt" -eq 1 ]]; then
    debug_msg "$archive_cmd $archive_flags $archive_late_flags ${positional[@]} 2>$verbosity | $encrypt_cmd $encrypt_flags $archive_fn $passphrase_flags 2>$verbosity"
  else
    debug_msg "$archive_cmd $archive_flags $archive_fn $archive_late_flags ${positional[@]} 2>$verbosity"
  fi

  # archive routine
  if [[ "$encrypt" -eq 1 ]]; then
    ( \
      $archive_cmd $archive_flags $archive_late_flags "${positional[@]}" 2>$verbosity \
        || error_msg "could not archive '${archive_fn}'" \
    ) \
    | ( \
      $encrypt_cmd $encrypt_flags "$archive_fn" $passphrase_flags 2>$verbosity \
        || error_msg "could not encrypt '${archive_fn}'" \
    )
  else
    $archive_cmd $archive_flags "$archive_fn" $archive_late_flags "${positional[@]}" 2>$verbosity \
      || error_msg "could not archive '${archive_fn}'"
  fi
}


M archives/mktar => archives/mktar +52 -303
@@ 1,74 1,34 @@
#!/bin/bash

name="mktar"
version="1.3"
version="1.4"
read -r -d '' help_message <<-EOF
	Archive utility
	Usage: mktar [OPTIONS] INFILES
	Usage: mktar -n OUTFILE [OPTIONS] INFILES
	Options:
	 -c, --compress        compress archive with gzip
	 --compress=ALGO       compress archive with [none|gzip|xz|zstd|bzip2]
	 -C, --checksum        create checksum with SHA1
	 --checksum=ALGO       create checksum with [none|sha1|sha256]
	 -e, --encrypt         encrypt files with GPG
	 --encrypt=ALGO        encrypt files with [none|gpg|age]
	 -h, --help            print this message
	 -n FILE, --name FILE  name of backup file (Default: archive.tar)
	 -q, --quiet           suppress error messages and prompts
	 -v, --verbose         show additional messages
	 -V, --version         print version number and exit
	 --checksum=ALGO             create checksum with [none|sha1|sha256]
	 -h, --help                  print this message
	 -n FILE, --name FILE        name of archive
	 -p PASS, --passphrase PASS  passphrase for encryption
	 -q, --quiet                 suppress error messages and prompts
	 -v, --verbose               show additional messages
	 -V, --version               print version number and exit
EOF

source /usr/local/lib/mylib.bash
source /usr/local/lib/archive.bash

archive_fn=""
checksum=-1
positional=()
quiet=0
verbose=0
compress=-1
encrypt=-1
checksum=-1
archive_fn=""
while [[ $# -gt 0 ]]; do
  case $1 in

  --compress=gz|--compress=gzip|--compress=1)
    debug_msg "Setting compress option to 1 (=gzip) (was ${compress})"
    compress=1
    shift
    ;;

  --compress=xz|--compress=2)
    debug_msg "Setting compress option to 2 (=xz) (was ${compress})"
    compress=2
    shift
    ;;

  --compress=zst|--compress=zstd|--compress=3)
    debug_msg "Setting compress option to 3 (=zstd) (was ${compress})"
    compress=3
    shift
    ;;

  --compress=bz2|--compress=bzip2|--compress=4)
    debug_msg "Setting compress option to 4 (=bzip2) (was ${compress})"
    compress=4
    shift
    ;;

  --compress=no|--compress=none|--compress=0)
    debug_msg "Setting compress option to 0 (=none) (was ${compress})"
    compress=0
    shift
    ;;

  --compress=*)
    attempted_compression="$(/usr/bin/printf "%s\n" "$1" | sed -e 's/^.*=//' )"
    error_msg "Unknown compression '${attempted_compression}'"
    ;;

  -c|--compress)
    debug_msg "Setting compress option to 1 (=gzip) (was ${compress})"
    compress=1
  --checksum=no|--checksum=none|--checksum=0)
    debug_msg "Setting checksum option to 0 (=none) (was ${checksum})"
    checksum=0
    shift
    ;;



@@ 84,52 44,11 @@ while [[ $# -gt 0 ]]; do
    shift
    ;;

  --checksum=no|--checksum=none|--checksum=0)
    debug_msg "Setting checksum option to 0 (=none) (was ${checksum})"
    checksum=0
    shift
    ;;

  --checksum=*)
    attempted_checksum="$(/usr/bin/printf "%s\n" "$1" | sed -e 's/^.*=//' )"
    error_msg "Unknown checksum '${attempted_checksum}'"
    ;;

  -C|--checksum)
    debug_msg "Setting checksum option to 1 (=SHA1) (was ${checksum})"
    checksum=1
    shift
    ;;

  --encrypt=gpg|--encrypt=1)
    debug_msg "Setting encrypt option to 1 (=GPG) (was ${encrypt})"
    encrypt=1
    shift
    ;;

  --encrypt=age|--encrypt=2)
    debug_msg "Setting encrypt option to 2 (=Age) (was ${encrypt})"
    encrypt=2
    shift
    ;;

  --encrypt=no|--encrypt=none|--encrypt=0)
    debug_msg "Setting encrypt option to 0 (=none) (was ${encrypt})"
    encrypt=0
    shift
    ;;

  --encrypt=*)
    attempted_encryption="$(/usr/bin/printf "%s\n" "$1" | sed -e 's/^.*=//' )"
    error_msg "Unknown encryption '${attempted_encryption}'"
    ;;

  -e|--encrypt)
    debug_msg "Setting encrypt option to 1 (=GPG) (was ${encrypt})"
    encrypt=1
    shift
    ;;

  -h|--help)
    help_msg
    shift


@@ 141,6 60,12 @@ while [[ $# -gt 0 ]]; do
    shift; shift
    ;;

  -p|--passphrase)
    debug_msg "Setting passphrase to ${2} (was ${passphrase})"
    passphrase="--passphrase $2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1


@@ 165,233 90,57 @@ while [[ $# -gt 0 ]]; do
  esac
done

# error if no filenames given
if [[ "${#positional[@]}" -lt 1 ]]; then
  debug_msg "No input filenames were given"
# error if no archive file given
if [[ -z "$archive_fn" ]]; then
  error_msg "no archive file given"
  usage_msg
fi

# determine compress action
if [[ "$compress" -eq 1 ]]; then
  archive_action="tar.gz"
elif [[ "$compress" -eq 2 ]]; then
  archive_action="tar.xz"
elif [[ "$compress" -eq 3 ]]; then
  archive_action="tar.zst"
elif [[ "$compress" -eq 4 ]]; then
  archive_action="tar.bz2"
elif [[ "$compress" -eq -1 ]]; then
  debug_msg "No explicit compress option; parsing archive name..."
  case "$archive_fn" in
  *.tar)
    debug_msg "Detected tarball (no compress)"
    archive_action="tar"
    ;;

  *.tar.gz)
    debug_msg "Detected gzip compress"
    archive_action="tar.gz"
    ;;

  *.tar.xz)
    debug_msg "Detected xz compress"
    archive_action="tar.xz"
    ;;

  *.tar.zst)
    debug_msg "Detected zstandard compress"
    archive_action="tar.zst"
    ;;

  *.tar.bz2)
    debug_msg "Detected bz2 compress"
    archive_action="tar.bz2"
    ;;

  *)
    debug_msg "Could not parse archive name; defaulting to no compress"
    archive_action="tar"
    ;;
  esac
else
  archive_action="tar"
fi

# determine encrypt action
if [[ "$encrypt" -eq 1 ]]; then
  archive_action="${archive_action}.gpg"
elif [[ "$encrypt" -eq 2 ]]; then
  archive_action="${archive_action}.age"
elif [[ "$encrypt" -eq -1 ]]; then
  debug_msg "No explicit encrypt option; parsing archive name..."
  case "$archive_fn" in
  *.gpg)
    debug_msg "Detected gpg encrypt"
    archive_action="${archive_action}.gpg"
    ;;

  *.age)
    debug_msg "Detected age encrypt"
    archive_action="${archive_action}.age"
    ;;
# determine archive action
archive_action="$(archive_extension "$archive_fn" | tee >(>&2 /usr/bin/head -n -1) | /usr/bin/tail -n 1)"

  *)
    debug_msg "No explicit encrypt option and could not parse archive name; defaulting to no encrypt"
    archive_action="${archive_action}"
    ;;
  esac
else
  archive_action="${archive_action}"
fi

# determine checksum action
# determine checksum action and filename
if [[ "$checksum" -eq 1 ]]; then
  checksum_action="sha1"
elif [[ "$checksum" -eq 2 ]]; then
  checksum_action="sha256"
fi
checksum_fn="$(fn_basename "$archive_fn").${checksum_action}"

# output filename
if [[ -z "$archive_fn" ]]; then
  archive_fn="archive.${archive_action}"
  checksum_fn="archive.${checksum_action}"
  debug_msg "No output filename was given, defaulting to '${archive_fn}'"
else
  checksum_fn="$(fn_basename "$archive_fn").${checksum_action}"
# check checksum files
if [[ "$checksum" -ge 1 ]]; then
  if contains "$checksum_fn" "${positional[@]}"; then
    error_msg "checksum '${checksum_fn}' cannot be an input file"
  elif ! prompt_overwrite "checksum_fn"; then
    error_msg "checksum '${checksum_fn}' cannot be overwritten"
  fi
fi

# check files
if contains "$archive_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file ('${archive_fn}')"
elif [[ "$checksum" -ge 1 ]] && contains "$checksum_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file ('${checksum_fn}')"
# check passphrase
if [[ "$encrypt" -ge 1 && -z "$passphrase" ]]; then
  passphrase=
fi
if ! prompt_overwrite "$archive_fn"; then
  exit 1
elif [[ "$checksum" -ge 1 ]] && ! prompt_overwrite "$checksum_fn"; then

if ! archive --archive=$archive_action --name "$archive_fn" $passphrase "${positional[@]}"; then
  exit 1
fi
for target in "${positional[@]}"; do
  if [[ ! -f "$target" ]] && [[ ! -d "$target" ]]; then
    error_msg "No such file '${target}'"
  fi
done

# main routine
code=0
case "$archive_action" in
tar)
  if ! /usr/bin/tar -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gpg)
  if ! /usr/bin/tar -c "${positional[@]}" | /usr/bin/gpg --symmetric --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.age)
  if ! /usr/bin/tar -c "${positional[@]}" | /usr/bin/age --encrypt --passphrase --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.gz)
  if ! /usr/bin/tar -czf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gz.gpg)
  if ! /usr/bin/tar -cz "${positional[@]}" | /usr/bin/gpg --symmetric --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.gz.age)
  if ! /usr/bin/tar -cz "${positional[@]}" | /usr/bin/age --encrypt --passphrase --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.xz)
  if ! /usr/bin/tar -cJf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.xz.gpg)
  if ! /usr/bin/tar -cJ "${positional[@]}" | /usr/bin/gpg --symmetric --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.xz.age)
  if ! /usr/bin/tar -cJ "${positional[@]}" | /usr/bin/age --encrypt --passphrase --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.zst)
  if ! /usr/bin/tar --zstd -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.zst.gpg)
  if ! /usr/bin/tar --zstd -c "${positional[@]}" | /usr/bin/gpg --symmetric --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.zst.age)
  if ! /usr/bin/tar --zstd -c "${positional[@]}" | /usr/bin/age --encrypt --passphrase --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.bz2)
  if ! /usr/bin/tar -cjf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.bz2.gpg)
  if ! /usr/bin/tar -cj "${positional[@]}" | /usr/bin/gpg --symmetric --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.bz2.age)
  if ! /usr/bin/tar -cj "${positional[@]}" | /usr/bin/age --encrypt --passphrase --output "$archive_fn"; then
    code=1
  fi
  ;;
esac

# checksum routine
if [[ "$checksum" -ge 1 ]]; then
  if [[ -f "$archive_fn" ]]; then
    case "$checksum_action" in
    sha1)
      if ! /usr/bin/sha1sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
        code=1
      fi
      ;;
  case "$checksum_action" in
  sha1)
    if ! /usr/bin/sha1sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
      code=1
    fi
    ;;

    sha256)
      if ! /usr/bin/sha256sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
        code=1
      fi
      ;;
    esac
  else
    error_msg "No such file '${archive_fn}'"
  fi
  sha256)
    if ! /usr/bin/sha256sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
      code=1
    fi
    ;;
  esac
fi

# return stored code
exit "$code"


M archives/mktar-batch => archives/mktar-batch +32 -195
@@ 1,7 1,7 @@
#!/bin/bash

name="mktar-batch"
version="1.0"
version="1.1"
read -r -d '' help_message <<-EOF
	Archive utility for scripting
	Usage: mktar-batch [--compress=ALGO] [--encrypt=ALSO --passphrase PASSWD] [--checksum=ALGO] [--name OUTFILE] INFILES


@@ 17,14 17,15 @@ read -r -d '' help_message <<-EOF
EOF

source /usr/local/lib/mylib.bash
source /usr/local/lib/archive.bash

positional=()
verbose=0
archive_fn=""
checksum=-1
compress=-1
encrypt=-1
checksum=-1
archive_fn=""
passphrase=""
positional=()
verbose=0
while [[ $# -gt 0 ]]; do
  case $1 in



@@ 105,7 106,7 @@ while [[ $# -gt 0 ]]; do
    ;;

  --encrypt=*)
    attempted_encryption="$(/usr/bin/printf "%s\n" "$1" | sed -e 's/^.*=//' )"
    attempted_encryption="$(/usr/bin/sed -e 's/^.*=//' <<<"$1")"
    error_msg "Bad encrypt option '${attempted_encryption}'"
    ;;



@@ 142,13 143,6 @@ while [[ $# -gt 0 ]]; do
  esac
done

# error if no input or output filenames given
if [[ "${#positional[@]}" -lt 1 ]]; then
  error_msg "No input filenames given"
elif [[ -z "$archive_fn" ]]; then
  error_msg "No output filename given"
fi

# construct archive action
if [[ "$compress" -eq 1 ]]; then
  archive_action="tar.gz"


@@ 177,197 171,40 @@ fi
# construct checksum filename
checksum_fn="$(fn_basename "$archive_fn").${checksum_action}"

# check files
if contains "$archive_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file ('${archive_fn}')"
elif [[ "$checksum" -ge 1 ]] && contains "$checksum_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file ('${checksum_fn}')"
elif [[ -f "$archive_fn" ]]; then
  error_msg "Output file already exists ('${archive_fn}')"
elif [[ "$checksum" -ge 1 && -f "$checksum_fn" ]]; then
  error_msg "Output file already exists ('${checksum_fn}')"
fi
for target in "${positional[@]}"; do
  if [[ ! -f "$target" ]] && [[ ! -d "$target" ]]; then
    error_msg "No such file '${target}'"
# check checksum files
if [[ "$checksum" -ge 1 ]]; then
  if contains "$checksum_fn" "${positional[@]}"; then
    error_msg "checksum '${checksum_fn}' cannot be an input file"
  elif ! prompt_overwrite "checksum_fn"; then
    error_msg "checksum '${checksum_fn}' cannot be overwritten"
  fi
done
fi

# check passphrase
if [[ "$checksum" -ge 1 && -z "$passphrase" ]]; then
if [[ "$encrypt" -ge 1 && -z "$passphrase" ]]; then
  error_msg "No passphrase given"
fi

# main routine
code=0
case "$archive_action" in
tar)
  if ! /usr/bin/tar -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gpg)
  if ! /usr/bin/tar -c "${positional[@]}" | /usr/bin/gpg --symmetric --batch --passphrase "$passphrase" --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.age)
  /usr/bin/expect <<EOF
spawn sh -c "/usr/bin/tar -c \"${positional[@]}\" | /usr/bin/age --encrypt --passphrase --output \"$archive_fn\""
expect "*:"
send "$passphrase\r"
expect "*:"
send "$passphrase\r"
expect eof

foreach {pid spawnid os_error_flag value} [wait] break
exit $value
EOF
  if [[ $? -ne 0 ]]; then
    code=1
  fi
  ;;

tar.gz)
  if ! /usr/bin/tar -czf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gz.gpg)
  if ! /usr/bin/tar -cz "${positional[@]}" | /usr/bin/gpg --symmetric --batch --passphrase "$passphrase" --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.gz.age)
  /usr/bin/expect <<EOF
spawn sh -c "/usr/bin/tar -cz \"${positional[@]}\" | /usr/bin/age --encrypt --passphrase --output \"$archive_fn\""
expect "*:"
send "$passphrase\r"
expect "*:"
send "$passphrase\r"
expect eof

foreach {pid spawnid os_error_flag value} [wait] break
exit $value
EOF
  if [[ $? -ne 0 ]]; then
    code=1
  fi
  ;;

tar.xz)
  if ! /usr/bin/tar -cJf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.xz.gpg)
  if ! /usr/bin/tar -cJ "${positional[@]}" | /usr/bin/gpg --symmetric --batch --passphrase "$passphrase" --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.xz.age)
  /usr/bin/expect <<EOF
spawn sh -c "/usr/bin/tar -cJ \"${positional[@]}\" | /usr/bin/age --encrypt --passphrase --output \"$archive_fn\""
expect "*:"
send "$passphrase\r"
expect "*:"
send "$passphrase\r"
expect eof

foreach {pid spawnid os_error_flag value} [wait] break
exit $value
EOF
  if [[ $? -ne 0 ]]; then
    code=1
  fi
  ;;

tar.zst)
  if ! /usr/bin/tar --zstd -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.zst.gpg)
  if ! /usr/bin/tar --zstd -c "${positional[@]}" | /usr/bin/gpg --symmetric --batch --passphrase "$passphrase" --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.zst.age)
  /usr/bin/expect <<EOF
spawn sh -c "/usr/bin/tar --zstd -c \"${positional[@]}\" | /usr/bin/age --encrypt --passphrase --output \"$archive_fn\""
expect "*:"
send "$passphrase\r"
expect "*:"
send "$passphrase\r"
expect eof

foreach {pid spawnid os_error_flag value} [wait] break
exit $value
EOF
  if [[ $? -ne 0 ]]; then
    code=1
  fi
  ;;

tar.bz2)
  if ! /usr/bin/tar -cjf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.bz2.gpg)
  if ! /usr/bin/tar -cj "${positional[@]}" | /usr/bin/gpg --symmetric --batch --passphrase "$passphrase" --output "$archive_fn"; then
    code=1
  fi
  ;;

tar.bz2.age)
  /usr/bin/expect <<EOF
spawn sh -c "/usr/bin/tar -cj \"${positional[@]}\" | /usr/bin/age --encrypt --passphrase --output \"$archive_fn\""
expect "*:"
send "$passphrase\r"
expect "*:"
send "$passphrase\r"
expect eof

foreach {pid spawnid os_error_flag value} [wait] break
exit $value
EOF
  if [[ $? -ne 0 ]]; then
    code=1
  fi
  ;;
esac
if ! archive --archive=$archive_action --name "$archive_fn" --passphrase "$passphrase" "${positional[@]}"; then
  exit 1
fi

# checksum routine
code=0
if [[ "$checksum" -ge 1 ]]; then
  if [[ -f "$archive_fn" ]]; then
    case "$checksum_action" in
    sha1)
      if ! /usr/bin/sha1sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
        code=1
      fi
      ;;

    sha256)
      if ! /usr/bin/sha256sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
        code=1
      fi
      ;;
    esac
  else
    error_msg "No such file '${archive_fn}'"
  fi
  case "$checksum_action" in
  sha1)
    if ! /usr/bin/sha1sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
      code=1
    fi
    ;;

  sha256)
    if ! /usr/bin/sha256sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
      code=1
    fi
    ;;
  esac
fi

# return stored code
exit "$code"


M archives/tarcat => archives/tarcat +53 -111
@@ 1,125 1,67 @@
#!/bin/sh
#!/bin/bash

name="tarcat"
version="1.1"
version="1.2"
help_message=$(/usr/bin/cat <<-EOF
	Print contents of target archive file(s)
	Unarchive utility for scripting
	Usage: tarcat [TARGET ..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
	 -h, --help                  print this message and exit
	 -p PASS, --passphrase PASS  passphrase for decryption
	 -q, --quiet                 suppress error messages
	 -v, --version               print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh
source /usr/local/lib/mylib.bash
source /usr/local/lib/unarchive.bash

quiet=0
passphrase=""
positional=()
verbose=0
while [[ $# -gt 0 ]]; do
  case $1 in

  -h|--help)
    help_msg
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -p|--passphrase)
    passphrase="--passphrase $2"
    shift; shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

code=0

for target; do
  if [ ! -f "$target" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 /usr/bin/printf "%s: No such file '%s'\n" "$name" "$target")
    fi
for archive_fn in "${positional[@]}"; do
  archive_action="$(archive_extension "$archive_fn" | tee >(>&2 /usr/bin/head -n -1) | /usr/bin/tail -n 1)"
  if ! unarchive --archive=$archive_action --stdout "$archive_fn" $passphrase; then
    code=1
  else
    case "$target" in
    *.tar)
      if ! /usr/bin/tar -xOf "$target"; then
        code=1
      fi
      ;;

    *.tar.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xO; then
        code=1
      fi
      ;;

    *.tar.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xO; then
        code=1
      fi
      ;;

    *.tar.gz)
      if ! /usr/bin/tar -xzOf "$target"; then
        code=1
      fi
      ;;

    *.tar.gz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xzO; then
        code=1
      fi
      ;;

    *.tar.gz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xzO; then
        code=1
      fi
      ;;

    *.tar.xz)
      if ! /usr/bin/tar -xJOf "$target"; then
        code=1
      fi
      ;;

    *.tar.xz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xJO; then
        code=1
      fi
      ;;

    *.tar.xz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xJO; then
        code=1
      fi
      ;;

    *.tar.zst|*.tar.zstd)
      if ! /usr/bin/tar --zstd -xOf "$target"; then
        code=1
      fi
      ;;

    *.tar.zst.gpg|*.tar.zstd.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar --zstd -xO; then
        code=1
      fi
      ;;

    *.tar.zst.age|*.tar.zstd.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar --zstd -xO; then
        code=1
      fi
      ;;

    *.tar.bz2)
      if ! /usr/bin/tar -xjOf "$target"; then
        code=1
      fi
      ;;

    *.tar.bz2.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xjO; then
        code=1
      fi
      ;;

    *.tar.bz2.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xjO; then
        code=1
      fi
      ;;

    *)
      if ! /usr/bin/tar -xOf "$target"; then
        code=1
      fi
      ;;
    esac
  fi
done


M archives/tarls => archives/tarls +53 -119
@@ 1,133 1,67 @@
#!/bin/sh
#!/bin/bash

name="tarls"
version="1.1"
version="1.2"
help_message=$(/usr/bin/cat <<-EOF
	List files within 'tar' archive file(s)
	List files within archive files
	Usage: tarls TARGET [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
	 -h, --help                  print this message and exit
	 -p PASS, --passphrase PASS  passphrase for decryption
	 -q, --quiet                 suppress error messages
	 -v, --version               print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: tarls TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi
source /usr/local/lib/mylib.bash
source /usr/local/lib/unarchive.bash

quiet=0
passphrase=""
positional=()
verbose=0
while [[ $# -gt 0 ]]; do
  case $1 in

  -h|--help)
    help_msg
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -p|--passphrase)
    passphrase="--passphrase $2"
    shift; shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# main routine
code=0
for target; do
  if [ ! -f "$target" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 /usr/bin/printf "%s: No such file '%s'\n" "$name" "$target")
    fi
for archive_fn in "${positional[@]}"; do
  archive_action="$(archive_extension "$archive_fn" | tee >(>&2 /usr/bin/head -n -1) | /usr/bin/tail -n 1)"
  if ! unarchive --archive=$archive_action --list "$archive_fn" $passphrase; then
    code=1
  else
    case "$target" in
    *.tar)
      if ! /usr/bin/tar -tf "$target"; then
        code=1
      fi
      ;;

    *.tar.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -t; then
        code=1
      fi
      ;;

    *.tar.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -t; then
        code=1
      fi
      ;;

    *.tar.gz)
      if ! /usr/bin/tar -tzf "$target"; then
        code=1
      fi
      ;;

    *.tar.gz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -tz; then
        code=1
      fi
      ;;

    *.tar.gz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -tz; then
        code=1
      fi
      ;;

    *.tar.xz)
      if ! /usr/bin/tar -tJf "$target"; then
        code=1
      fi
      ;;

    *.tar.xz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -tJ; then
        code=1
      fi
      ;;

    *.tar.xz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -tJ; then
        code=1
      fi
      ;;

    *.tar.zst|*.tar.zstd)
      if ! /usr/bin/tar --zstd -tf "$target"; then
        code=1
      fi
      ;;

    *.tar.zst.gpg|*.tar.zstd.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar --zstd -t; then
        code=1
      fi
      ;;

    *.tar.zst.age|*.tar.zstd.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar --zstd -t; then
        code=1
      fi
      ;;

    *.tar.bz2)
      if ! /usr/bin/tar -tjf "$target"; then
        code=1
      fi
      ;;

    *.tar.bz2.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -tj; then
        code=1
      fi
      ;;

    *.tar.bz2.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -tj; then
        code=1
      fi
      ;;

    *)
      if ! /usr/bin/tar -tf "$target"; then
        code=1
      fi
      ;;
    esac
  fi
done


M archives/test_mktar_tarcat.sh => archives/test_mktar_tarcat.sh +44 -17
@@ 11,22 11,32 @@ test_compression() {
    NONE)
      arg_compression="none"
      fn_compressed="archive.tar"
      fn_compressed_gpg="archive.tar.gpg"
      fn_compressed_age="archive.tar.age"
      ;;
    GZIP)
      arg_compression="gzip"
      fn_compressed="archive.tar.gz"
      fn_compressed_gpg="archive.tar.gz.gpg"
      fn_compressed_age="archive.tar.gz.age"
      ;;
    XZ)
      arg_compression="xz"
      fn_compressed="archive.tar.xz"
      fn_compressed_gpg="archive.tar.xz.gpg"
      fn_compressed_age="archive.tar.xz.age"
      ;;
    ZSTD)
      arg_compression="zstd"
      fn_compressed="archive.tar.zst"
      fn_compressed_gpg="archive.tar.zst.gpg"
      fn_compressed_age="archive.tar.zst.age"
      ;;
    BZIP2)
      arg_compression="bzip2"
      fn_compressed="archive.tar.bz2"
      fn_compressed_gpg="archive.tar.bz2.gpg"
      fn_compressed_age="archive.tar.bz2.age"
      ;;
    *)
      printf "Failure in compression tests: not a valid scheme: '%s'\n" "$1"


@@ 34,50 44,67 @@ test_compression() {
      ;;
  esac

  # try basic compression
  ../mktar --compress="$arg_compression" compression_target.txt
  #
  # mktar tests
  #

  test="implicit compression with short name flag using"
  ../mktar -n "$fn_compressed" compression_target.txt
  ../tarcat "$fn_compressed" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: basic compression: '%s'\n" "$1"
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed"

  # try basic compression with misordered positional arguments
  ../mktar compression_target.txt --compress="$arg_compression"
  test="implicit compression with long name flag using"
  ../mktar --name "$fn_compressed" compression_target.txt
  ../tarcat "$fn_compressed" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: basic compression with misordered positional arguments: '%s'\n" "$1"
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed"

  # try implicit compression with short name flag
  ../mktar -n "$fn_compressed" compression_target.txt
  #
  # mktar-batch tests
  #

  test="compression using"
  ../mktar-batch --compress="$arg_compression" --name "$fn_compressed" compression_target.txt
  ../tarcat "$fn_compressed" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: implicit compression with short name flag: '%s'\n" "$1"
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed"

  # try implicit compression with long name flag
  ../mktar --name "$fn_compressed" compression_target.txt
  test="compression with misordered positional arguments using"
  ../mktar-batch compression_target.txt --compress="$arg_compression" --name "$fn_compressed"
  ../tarcat "$fn_compressed" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: implicit compression with long name flag: '%s'\n" "$1"
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed"

  # try batch compression
  ../mktar-batch --compress="$arg_compression" --name "$fn_compressed" compression_target.txt
  ../tarcat "$fn_compressed" > compression_result.txt
  test="compression using gpg and"
  ../mktar-batch --compress="$arg_compression" --encrypt="gpg" --passphrase test123 --name "$fn_compressed_gpg" compression_target.txt
  ../tarcat --passphrase test123 "$fn_compressed_gpg" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: basic compression: '%s'\n" "$1"
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed"
  rm -f compression_result.txt "$fn_compressed_gpg"

  test="compression using age and"
  ../mktar-batch --compress="$arg_compression" --encrypt="age" --passphrase test123 --name "$fn_compressed_age" compression_target.txt
  ../tarcat --passphrase test123 "$fn_compressed_age" > compression_result.txt
  if ! cmp compression_result.txt compression_target.txt >/dev/null 2>&1; then
    printf "Failure in compression tests: %s '%s'\n" "$test" "$1"
    exit 1
  fi
  rm -f compression_result.txt "$fn_compressed_age"
}

test_compression NONE

A archives/unarchive.bash => archives/unarchive.bash +251 -0
@@ 0,0 1,251 @@
#!/bin/bash

# See tarcat for simple usage

unarchive() {
  local tar_bin=/usr/bin/tar
  local gpg_bin=/usr/bin/gpg
  local age_bin=age

  local decrypt=""
  local decrypt_cmd=""
  local decrypt_flags=""
  local passphrase_flags=""
  local passphrase=""
  local unarchive_cmd=""
  local unarchive_flags=""
  local unarchive_late_flags=""

  while [[ $# -gt 0 ]]; do
    case $1 in

    --archive=tar)
      decrypt=0
      decrypt_cmd=""
      decrypt_flags=""
      passphrase_flags=""
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xf"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.age)
      decrypt=1
      decrypt_cmd="${age_bin}"
      decrypt_flags="${decrypt_flags} --decrypt"
      passphrase_flags="--plaintext"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}x"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.gpg)
      decrypt=1
      decrypt_cmd="${gpg_bin}"
      decrypt_flags="${decrypt_flags} --decrypt --quiet"
      passphrase_flags="--batch --passphrase"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}x"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.gz)
      decrypt=0
      decrypt_cmd=""
      decrypt_flags=""
      passphrase_flags=""
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xzf"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.gz.age)
      decrypt=1
      decrypt_cmd="${age_bin}"
      decrypt_flags="${decrypt_flags} --decrypt"
      passphrase_flags="--plaintext"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xz"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.gz.gpg)
      decrypt=1
      decrypt_cmd="${gpg_bin}"
      decrypt_flags="${decrypt_flags} --decrypt --quiet"
      passphrase_flags="--batch --passphrase"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xz"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.xz)
      decrypt=0
      decrypt_cmd=""
      decrypt_flags=""
      passphrase_flags=""
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xJf"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.xz.age)
      decrypt=1
      decrypt_cmd="${age_bin}"
      decrypt_flags="${decrypt_flags} --decrypt"
      passphrase_flags="--plaintext"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xJ"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.xz.gpg)
      decrypt=1
      decrypt_cmd="${gpg_bin}"
      decrypt_flags="${decrypt_flags} --decrypt --quiet"
      passphrase_flags="--batch --passphrase"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xJ"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.zst)
      decrypt=0
      decrypt_cmd=""
      decrypt_flags=""
      passphrase_flags=""
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xf"
      unarchive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.zst.age)
      decrypt=1
      decrypt_cmd="${age_bin}"
      decrypt_flags="${decrypt_flags} --decrypt"
      passphrase_flags="--plaintext"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}x"
      unarchive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.zst.gpg)
      decrypt=1
      decrypt_cmd="${gpg_bin}"
      decrypt_flags="${decrypt_flags} --decrypt --quiet"
      passphrase_flags="--batch --passphrase"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}x"
      unarchive_late_flags="--zstd"
      shift
      ;;

    --archive=tar.bz2)
      decrypt=0
      decrypt_cmd=""
      decrypt_flags=""
      passphrase_flags=""
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xjf"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.bz2.age)
      decrypt=1
      decrypt_cmd="${age_bin}"
      decrypt_flags="${decrypt_flags} --decrypt"
      passphrase_flags="--plaintext"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xj"
      unarchive_late_flags=""
      shift
      ;;

    --archive=tar.bz2.gpg)
      decrypt=1
      decrypt_cmd="${gpg_bin}"
      decrypt_flags="${decrypt_flags} --decrypt --quiet"
      passphrase_flags="--batch --passphrase"
      unarchive_cmd="${tar_bin}"
      unarchive_flags="${unarchive_flags}xj"
      unarchive_late_flags=""
      shift
      ;;

    --archive=*)
      error_msg "unsupported archive type '${1}'"
      ;;

    --list)
      unarchive_flags="$(/usr/bin/sed -e 's/x/t/' <<<"$unarchive_flags")"
      shift
      ;;

    --passphrase)
      passphrase="$2"
      shift; shift
      ;;

    --stdout)
      unarchive_flags="O${unarchive_flags}"
      shift
      ;;

    *)
      archive_fn="$1"
      shift
      ;;
    esac
  done

  # error if archive does not exist
  if [[ ! -f "$archive_fn" ]]; then
    error_msg "no such archive '${archive_fn}'"
    usage_msg
  fi

  # handle passphrase
  if [[ ! -z "$passphrase_flags" && ! -z "$passphrase" ]]; then
    passphrase_flags="${passphrase_flags} ${passphrase}"
  else
    passphrase_flags=
  fi

  # handle debugging information
  local verbosity=/dev/stderr
  if [[ "$verbose" -ne 1 ]]; then
    verbosity=/dev/null
  elif [[ "$decrypt" -eq 1 ]]; then
    debug_msg "$decrypt_cmd $decrypt_flags $archive_fn $passphrase_flags 2>$verbosity | $unarchive_cmd $unarchive_flags $unarchive_late_flags 2>$verbosity"
  else
    debug_msg "$unarchive_cmd $unarchive_flags $archive_fn $unarchive_late_flags 2>$verbosity"
  fi

  # unarchive routine
  if [[ "$decrypt" -eq 1 ]]; then
    ( \
      $decrypt_cmd $decrypt_flags $passphrase_flags "$archive_fn" 2>$verbosity \
        || error_msg "could not decrypt '${archive_fn}'" \
    ) \
    | ( \
      $unarchive_cmd $unarchive_flags $unarchive_late_flags 2>$verbosity \
        || error_msg "could not unarchive '${archive_fn}'" \
    )
  else
    $unarchive_cmd $unarchive_flags "$archive_fn" $unarchive_late_flags 2>$verbosity \
      || error_msg "could not unarchive '${archive_fn}'"
  fi
}


M archives/untar => archives/untar +53 -119
@@ 1,133 1,67 @@
#!/bin/sh
#!/bin/bash

name="untar"
version="1.1"
version="1.2"
help_message=$(/usr/bin/cat <<-EOF
	Wrapper around 'tar' for easier decompression
	Unarchive utility
	Usage: untar TARGET [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
	 -h, --help                  print this message and exit
	 -p PASS, --passphrase PASS  passphrase for decryption
	 -q, --quiet                 suppress error messages
	 -v, --version               print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: untar TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi
source /usr/local/lib/mylib.bash
source /usr/local/lib/unarchive.bash

quiet=0
passphrase=""
positional=()
verbose=0
while [[ $# -gt 0 ]]; do
  case $1 in

  -h|--help)
    help_msg
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -p|--passphrase)
    passphrase="--passphrase $2"
    shift; shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# main routine
code=0
for target; do
  if [ ! -f "$target" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 /usr/bin/printf "%s: No such file '%s'\n" "$name" "$target")
    fi
for archive_fn in "${positional[@]}"; do
  archive_action="$(archive_extension "$archive_fn" | tee >(>&2 /usr/bin/head -n -1) | /usr/bin/tail -n 1)"
  if ! unarchive --archive=$archive_action "$archive_fn" $passphrase; then
    code=1
  else
    case "$target" in
    *.tar)
      if ! /usr/bin/tar -xf "$target"; then
        code=1
      fi
      ;;

    *.tar.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -x; then
        code=1
      fi
      ;;

    *.tar.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -x; then
        code=1
      fi
      ;;

    *.tar.gz)
      if ! /usr/bin/tar -xzf "$target"; then
        code=1
      fi
      ;;

    *.tar.gz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xz; then
        code=1
      fi
      ;;

    *.tar.gz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xz; then
        code=1
      fi
      ;;

    *.tar.xz)
      if ! /usr/bin/tar -xJf "$target"; then
        code=1
      fi
      ;;

    *.tar.xz.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xJ; then
        code=1
      fi
      ;;

    *.tar.xz.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xJ; then
        code=1
      fi
      ;;

    *.tar.zst|*.tar.zstd)
      if ! /usr/bin/tar --zstd -xf "$target"; then
        code=1
      fi
      ;;

    *.tar.zst.gpg|*.tar.zstd.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar --zstd -x; then
        code=1
      fi
      ;;

    *.tar.zst.age|*.tar.zstd.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar --zstd -x; then
        code=1
      fi
      ;;

    *.tar.bz2)
      if ! /usr/bin/tar -xjf "$target"; then
        code=1
      fi
      ;;

    *.tar.bz2.gpg)
      if ! /usr/bin/gpg --decrypt "$target" | /usr/bin/tar -xj; then
        code=1
      fi
      ;;

    *.tar.bz2.age)
      if ! /usr/bin/age --decrypt "$target" | /usr/bin/tar -xj; then
        code=1
      fi
      ;;

    *)
      if ! /usr/bin/tar -xf "$target"; then
        code=1
      fi
      ;;
    esac
  fi
done


M core/mylib.bash => core/mylib.bash +4 -1
@@ 264,6 264,9 @@ fn_extension() {

# filename matches a pattern -> pattern without leading .
# else                       ->
# There is also debug output
# Suggested usage:
#   archive_action="$(archive_extension "$archive_fn" | tee >(>&2 /usr/bin/head -n -1) | /usr/bin/tail -n 1)"
archive_extension() {
  local ext=



@@ 348,6 351,6 @@ archive_extension() {
    ;;
  esac

  /usr/bin/printf "%s\n" "$ext"
  /usr/bin/printf '%s\n' "$ext"
}