#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # Protected by the BSD3 license. Please read bellow for details. # # * Redistribution and use in source and binary forms, # * with or without modification, are permitted provided # * that the following conditions are met: # * # * Redistributions of source code must retain the above # * copyright notice, this list of conditions and the # * following disclaimer. # * # * Redistributions in binary form must reproduce the above # * copyright notice, this list of conditions and the following # * disclaimer in the documentation and/or other materials # * provided with the distribution. # * # * Neither the name of the copyright holder nor the names # * of any other contributors may be used to endorse or # * promote products derived from this software without # * specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR # * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ load_conf "compress" # ------------------------------------------------------------------------------ # Smartly uncompress archives # Usage: utaz [option] [directorie(s)|file(s)] # Options: # -h, --help Display that help screen # -d, --delete If decompression succeeded, delete the source file # -c, --create-dir Always create a host directory # -n, --no-dir Never create a host directory utaz() { # Extractor helpers. Every one takes the archive path as $1 and an existing # target directory as $2, and returns the extractor exit status (0 = ok, # 1 = warning, anything else = failure). The matching one is selected by # file extension and invoked by name (see the dispatch "case" below), and # the name minus its "_un" prefix is the binary checked beforehand. # NOTE: bash defines functions globally, so these helpers are (re)defined in # the shell on each call and the "local" lines below only declare an unused # variable of the same name; they are kept as a marker for shellcheck. local _ununzip # shellcheck disable=SC2329 _ununzip() { unzip -o "$1" -d "$2" >/dev/null 2>&1 } local _untar # shellcheck disable=SC2329 _untar() { tar -xf "$1" -C "$2" } local _ungzip # shellcheck disable=SC2329 _ungzip() { tar -xzf "$1" -C "$2" } local _unbzip2 # shellcheck disable=SC2329 _unbzip2() { tar -xjf "$1" -C "$2" } local _unxz # shellcheck disable=SC2329 _unxz() { tar -xJf "$1" -C "$2" } local _unlzop # shellcheck disable=SC2329 _unlzop() { lzop -d "$1" -o "$2/$(basename "${1%.*}")" } local _unlzip # shellcheck disable=SC2329 _unlzip() { # Only *.tar.lz / *.tlz reach this helper: decompress to stdout and # untar from the pipe. pipefail (subshell-scoped) makes a lzip failure # visible instead of being masked by tar's exit status. local lzcmd=lzip command -v plzip >/dev/null 2>&1 && lzcmd=plzip ( set -o pipefail "$lzcmd" -d -c -- "$1" | tar -xf - -C "$2" ) } local _ununrar # shellcheck disable=SC2329 _ununrar() { unrar x -o+ "$1" "$2/" >/dev/null 2>&1 } local _ununarj # shellcheck disable=SC2329 _ununarj() { unarj e "$1" "$2/" >/dev/null 2>&1 } local _unlha # shellcheck disable=SC2329 _unlha() { # lha extracts into the current directory, so we cd into the target # directory and feed it an absolute path to the archive. local src src=$(realpath -- "$1") || return 1 (cd "$2" && lha -x "$src") >/dev/null 2>&1 } local _ununace # shellcheck disable=SC2329 _ununace() { unace x "$1" "$2/" >/dev/null 2>&1 } local _un7z # shellcheck disable=SC2329 _un7z() { 7z x "$1" -o"$2/" >/dev/null 2>&1 } local _unzstd # shellcheck disable=SC2329 _unzstd() { # A tar.zst/.tzst is untarred; a bare .zst is a single compressed file # and is simply decompressed into the target directory. case $1 in *.tar.zst|*.tzst) tar --zstd -xf "$1" -C "$2" ;; *) zstd -d -q -c -- "$1" > "$2/$(basename "${1%.*}")" ;; esac } local _uncpio # shellcheck disable=SC2329 _uncpio() { # cpio extracts into the current directory, so we cd into the target # directory and read from an absolute path to the archive. local src src=$(realpath -- "$1") || return 1 (cd "$2" && cpio -id < "$src") >/dev/null 2>&1 } local _uncabextract # shellcheck disable=SC2329 _uncabextract() { # Requires 'cabextract' package cabextract "$1" -d "$2/" >/dev/null 2>&1 } local _undeb # shellcheck disable=SC2329 _undeb() { # Extracts data content from a Debian package dpkg-deb -x "$1" "$2/" >/dev/null 2>&1 } local _unrpm # shellcheck disable=SC2329 _unrpm() { # Extracts CPIO-based payload from an RPM package # Needs rpm2cpio and cpio. pipefail (subshell-scoped) so that a failing # rpm2cpio is not masked by cpio's exit status. ( set -o pipefail rpm2cpio "$1" | (cd "$2/" && cpio -idm) ) >/dev/null 2>&1 } # Move every entry (hidden ones included) of directory $1 up into its # parent, then remove $1. Refuses (returns 1, $1 left untouched) if any # entry would overwrite something already present in the parent. # The directory is first renamed to a temporary name so that an archive # holding a top-level entry named like the host directory (foo.zip -> foo/foo) # does not collide with itself. Runs in a subshell so the shopt calls do not # leak into the interactive shell. local _utaz_flatten # shellcheck disable=SC2329 _utaz_flatten() ( shopt -s nullglob dotglob local dir=$1 parent tmp child parent=$(dirname -- "$dir") tmp="${parent}/.utaz.$$.${RANDOM}" mv -- "$dir" "$tmp" || return 1 for child in "$tmp"/*; do if [[ -e "${parent}/${child##*/}" || -L "${parent}/${child##*/}" ]]; then disp W "${parent}/${child##*/} already exists, not overwriting." mv -- "$tmp" "$dir" return 1 fi done for child in "$tmp"/*; do mv -- "$child" "${parent}/" || return 1 done rmdir -- "$tmp" ) local PARSED PARSED=$(getopt -o hdcn --long help,delete,create-dir,no-dir -n 'utaz' -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [ $? -ne 0 ]; then disp E "Invalid options, use \"utaz --help\" to display usage." return 1 fi # getopt emits a shell-quoted, normalised argument list ("--" terminates the # options); re-evaluating it restores the arguments with spaces and quotes # intact. Declared local so flags never leak in from the caller's environment. local willrm="" createdir="" nodir="" eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "utaz: uncompress all the given files and/or the ones found in the given\n" printf " directories creating an host directory where needed.\n\n" printf "Usage: utaz [option] [directorie(s)|file(s)]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay that help screen\n" printf "\t-d, --delete\t\tIf decompression succeeded, delete the source file\n" printf "\t-c, --create-dir\tAlways create a host directory\n" printf "\t-n, --no-dir\t\tNever create a host directory\n\n" printf "Supported archive format:\n" printf "\t- zip\n" printf "\t- tar.gz, .tgz\n" printf "\t- tar.bz2, .tbz2\n" printf "\t- tar.xz, .txz\n" printf "\t- tar.lz, .tlz\n" printf "\t- lzo\n" printf "\t- rar\n" printf "\t- arj\n" printf "\t- lha, lzh\n" printf "\t- ace\n" printf "\t- 7z, p7z\n" printf "\t- tar\n" printf "\t- zst, tar.zst, .tzst\n" printf "\t- cpio\n" printf "\t- cab\n" printf "\t- deb\n" printf "\t- rpm\n" return 0 ;; -d|--delete) willrm=1 shift ;; -c|--create-dir) createdir=1 shift ;; -n|--no-dir) nodir=1 shift ;; --) shift break ;; *) disp E "Invalid option, use \"utaz --help\" to display options list" return 1 ;; esac done # The remaining arguments after -- are the files/directories to process, # "." is used if none is given local FILES=("$@") [[ ${#FILES[@]} -eq 0 ]] && FILES=(".") # Apply defaults from [compress] configuration if not overridden by flags [[ -z $willrm ]] && [[ ${UTAZ_DEFAULT_DELETE:-0} -eq 1 ]] && willrm=1 if [[ -z $createdir && -z $nodir ]]; then case "${UTAZ_DEFAULT_DIR_MODE:-auto}" in always) createdir=1 ;; never) nodir=1 ;; esac fi [[ -n ${createdir} && -n ${nodir} ]] && { disp E "The --create-dir and --no-dir options are mutually exclusive." return 1 } # Loop variables are local: this function lives in the interactive shell. local zitem f for zitem in "${FILES[@]}"; do # A name starting with "-" would be taken as an option by the tools below [[ $zitem == -* ]] && zitem="./${zitem}" # Build list of input files to process, with whitespace-safe handling. local targets=() if [[ -f "$zitem" ]]; then targets=("$zitem") elif [[ -d "$zitem" ]]; then # Only direct children are considered (no recursion); NUL-separated # so names containing spaces or newlines survive. mapfile -d '' -t targets < <(find "$zitem" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) if [[ ${#targets[@]} -eq 0 ]]; then disp I "Directory ${zitem} is empty, skipping." continue fi else disp W "Path ${zitem} is not a file or directory, skipping." continue fi for f in "${targets[@]}"; do # Host directory name: archive name without its extension. A # trailing ".tar" is dropped too, so foo.tar.gz gives "foo", not # "foo.tar". local dir="${f%.*}" [[ $dir == *.tar ]] && dir="${dir%.tar}" local extractor="" case "$f" in *.zip) extractor="_ununzip" ;; *.tar.gz|*.tgz) extractor="_ungzip" ;; *.tar.bz2|*.tbz2) extractor="_unbzip2" ;; *.tar.xz|*.txz) extractor="_unxz" ;; *.tar.lz|*.tlz) extractor="_unlzip" ;; *.lzo) extractor="_unlzop" ;; *.tar) extractor="_untar" ;; *.rar) extractor="_ununrar" ;; *.arj) extractor="_ununarj" ;; *.lzh|*.lha) extractor="_unlha" ;; *.ace) extractor="_ununace" ;; *.7z|*.p7z) extractor="_un7z" ;; *.zst|*.tzst) extractor="_unzstd" ;; *.cpio) extractor="_uncpio" ;; *.cab) extractor="_uncabextract" ;; *.deb) extractor="_undeb" ;; *.rpm) extractor="_unrpm" ;; *) disp I "File ${f} is not a supported archive, skipping." continue ;; # Skip non-archive files esac # Verify the extractor's binary exists. The helper name minus its # "_un" prefix is the program name (_un7z -> 7z); deb/rpm/lzip need # special handling because their programs are named differently. local cmd=${extractor//_un/} if [[ $cmd == "deb" ]]; then command -v -- dpkg-deb >/dev/null 2>&1 || { disp E "The program 'dpkg-deb' is not installed, aborting." continue } elif [[ $cmd == "rpm" ]]; then command -v -- rpm2cpio >/dev/null 2>&1 || { disp E "The program 'rpm2cpio' is not installed, aborting." continue } command -v -- cpio >/dev/null 2>&1 || { disp E "The program 'cpio' is not installed, aborting." continue } elif [[ $cmd == "lzip" ]]; then # plzip is a drop-in parallel replacement for lzip command -v -- lzip >/dev/null 2>&1 || command -v -- plzip >/dev/null 2>&1 || { disp E "Neither 'lzip' nor 'plzip' is installed, skipping ${f}." continue } else command -v -- "${cmd}" >/dev/null 2>&1 || { disp E "Binary ${cmd} necessary to extract ${f} is missing." continue } fi disp I "Processing archive ${f} with ${extractor}..." # Track whether we create the target directory so a corrupted # archive never wipes a pre-existing directory of the same name. local dir_created=0 [[ -d "${dir}" ]] || dir_created=1 if ! mkdir -p -- "${dir}"; then disp E "Can't create directory ${dir}, skipping ${f}." continue fi ${extractor} "${f}" "${dir}" case $? in 0) [[ -n ${willrm} ]] && rm -f -- "${f}" && disp I "File ${f} deleted." ;; 1) # Most extractors use 1 for "finished with warnings" if [[ -n ${willrm} ]]; then disp W "Compression program returned a warning: deletion canceled." else disp W "Compression program returned a warning." fi ;; *) disp E "The compressed file ${f} seems corrupted, failed." [[ ${dir_created} -eq 1 ]] && rm -rf -- "${dir}" >/dev/null 2>&1 continue ;; esac # Decide whether the host directory is kept. Flattening is never # done on a directory that existed before extraction, as it would # also move the user's own files out of it. local flatten=0 if [[ -n ${createdir} ]]; then : elif [[ ${dir_created} -ne 1 ]]; then : elif [[ -n ${nodir} ]]; then flatten=1 else # Auto mode: flatten only if the archive already holds a single # top-level directory (it then acts as the host directory). # nullglob makes the array empty when nothing matches; dotglob # makes hidden entries count, so ".git" alone is not ignored. local _old_glob _old_glob=$(shopt -p nullglob dotglob) shopt -s nullglob dotglob local contents=( "${dir}"/* ) eval "$_old_glob" [[ ${#contents[@]} -eq 1 && -d "${contents[0]}" && ! -L "${contents[0]}" ]] && flatten=1 fi if [[ $flatten -eq 1 ]] && _utaz_flatten "${dir}"; then disp I "Archive extracted successfully, no subdirectory needed." else disp I "Archive extracted successfully in subdirectory." fi done done } export -f utaz # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Compress directories or files into one or more archive # Usage: taz [option] [--parallel=] [--format=] [directory1 ... directoryN] # Options: # -h, --help Display that help screen # -d, --delete Delete source file or directory after success # -f, --format Chose archive format in the given list. If several format are # given, the smalest is kept # -K, --keep-all Keep every produced version instead of only the smallest # -p, --parallel Number of threads to use, or 'auto' to use detected CPU count # -v, --verbose Display progress where possible # -q, --quiet Display less messages (only errors and warnings) # -1, .., -9 Compression level to use [1=fast/biggest, 9=slow/smallest] taz() { # Compressor helpers. All share the same signature: # $1 = file to compress, $2 = thread count, $3 = level (1-9), # $4 = non-empty for verbose output # and keep the source (--keep), writing "$1." next to it. They return # 127 when no suitable program is installed (fatal for the whole run), any # other non-zero status on a compression failure (that format is skipped). # As with utaz, nested functions are global, "local" does not scope them. # Resolve runtime CPU count for --parallel=auto. local _taz_detect_cpus _taz_detect_cpus() { local cpus=1 if command -v nproc >/dev/null 2>&1; then cpus=$(nproc 2>/dev/null) elif command -v getconf >/dev/null 2>&1; then cpus=$(getconf _NPROCESSORS_ONLN 2>/dev/null) fi [[ $cpus =~ ^[1-9][0-9]*$ ]] || cpus=1 printf "%s\n" "$cpus" } # Render a byte count as a human-readable size (B/KB/MB/GB/...), base 1024. local _taz_human # shellcheck disable=SC2329 _taz_human() { if command -v numfmt >/dev/null 2>&1; then numfmt --to=iec --suffix=B -- "$1" 2>/dev/null && return 0 fi printf "%sB\n" "$1" } local _doxz # shellcheck disable=SC2329 _doxz() { command -v xz >/dev/null 2>&1 || { disp E "The program 'xz' is not installed, aborting." return 127 } local verb=() [[ $4 ]] && verb=('-v') # Display a warning for this format disp W "xz format is not suited for long term archiving." disp I "See https://www.nongnu.org/lzip/xz_inadequate.html for details." # Compress with xz (lzma2) - Deprecated xz "${verb[@]}" --compress --keep "-$3" -T "$2" "$1" return $? } local _dolz # shellcheck disable=SC2329 _dolz() { local procopt=(--threads "$2") local command=plzip command -v plzip >/dev/null 2>&1 || { command -v lzip >/dev/null 2>&1 || { disp E "Program 'plzip' or 'lzip' are not installed, aborting." return 127 } command=lzip local procopt=() [[ $2 -gt 1 ]] && disp W "lzip doesn't support multithreading, falling back to 1 thread." && disp W "Consider installing plzip to obtain multithreading abilities." } local opt=() [[ $4 ]] && opt=('-vv') opt+=("${procopt[@]}") # Compress with lzip (lzma) $command "${opt[@]}" --keep "-$3" "$1" return $? } local _dogz # shellcheck disable=SC2329 _dogz() { local procopt=(--processes "$2") local command=pigz command -v pigz >/dev/null 2>&1 || { command -v gzip >/dev/null 2>&1 || { disp E "Programs 'pigz' or 'gzip' are not installed, aborting." return 127 } local command=gzip local procopt=() [[ $2 -gt 1 ]] && disp W "gzip doesn't support multithreading, falling back to 1 thread." && disp W "Consider installing pigz to obtain multithreading abilities." } local opt=() [[ $4 ]] && opt=('--verbose') opt+=("${procopt[@]}") # Compress with gzip $command "${opt[@]}" --keep "-$3" "$1" return $? } local _dobz2 # shellcheck disable=SC2329 _dobz2() { local procopt=("-p$2") local command=pbzip2 command -v pbzip2 >/dev/null 2>&1 || { command -v bzip2 >/dev/null 2>&1 || { disp E "The program 'pbzip2' or 'bzip2' are not installed, aborting." return 127 } local command=bzip2 local procopt=() [[ $2 -gt 1 ]] && disp W "bzip2 doesn't support multithreading, falling back to 1 thread." && disp W "Consider installing pbzip2 to obtain multithreading abilities." } local opt=() [[ $4 ]] && opt=('-v') opt+=("${procopt[@]}") # Compress with bz2 $command "${opt[@]}" --compress --keep "-$3" "$1" return $? } local _dolzo # shellcheck disable=SC2329 _dolzo() { command -v lzop >/dev/null 2>&1 || { disp E "The program 'lzop' is not installed, aborting." return 127 } local verb=() [[ $4 ]] && verb=('-v') [[ $2 -gt 1 ]] && disp W "lzop doesn't support multithreading, falling back to 1 thread." # Compress with lzo lzop "${verb[@]}" --keep "-$3" "$1" return $? } local PARSED PARSED=$(getopt -o hdf:Kp:vq123456789 --long help,delete,format:,keep-all,parallel:,verbose,quiet --name "taz" -- "$@") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [ $? -ne 0 ]; then disp E "Invalid options, use \"taz --help\" to display usage." return 1 fi # See utaz: restore the getopt-normalised argument list, and keep every # flag local so nothing leaks in from the caller's environment. local willrm="" keepall="" verbose="" quiet="" nproc="" complevel="" eval set -- "$PARSED" local compforms=() local _f _list while true; do case "$1" in -h|--help) printf "taz: archive all files of a directory.\n\n" printf "Usage: taz [option] [--parallel=] [--format=] [directory1 ... directoryN]\n\n" printf "Options:\n" printf "\t-h, --help\tDisplay that help screen\n" printf "\t-d, --delete\tDelete source file or directory after success\n" printf "\t-f, --format\tChose archive format in the given list. If several format are\n" printf "\t\t\tgiven, the smalest produced archive is kept\n" printf "\t-K, --keep-all\tKeep every produced version instead of only the smallest\n" printf "\t-p, --parallel\tNumber of threads, or 'auto' for runtime CPU count\n" printf "\t-v, --verbose\tDisplay progress where possible\n" printf "\t-q, --quiet\tDisplay less messages (only errors and warnings)\n" printf "\t-1, .., -9\tCompression level to use [1=fast/biggest, 9=slow/smallest]\n\n" printf "Supported archive format:\n" printf "\tParam.| programs | Algo. | Description\n" printf "\t------+---------------+-------+----------------------------------------\n" printf "\t lz | plzip, lzip | lzma | Safe efficient default format\n" printf "\t xz | xz | lzma2 | Unsafe, not for long term\n" printf "\t bz2 | pbzip2, bzip2 | bzip2 | Historical but less efficient than lz\n" printf "\t gz | pigz, gzip | lz77 | Historical, safe, fast\n" printf "\t lzo | lzop | lzo | Very fast but no multithread\n" printf "\t tar | tar | tar | No compression\n" printf "\n" return 0 ;; -d|--delete) willrm=1 shift ;; -f|--format) # Accept comma/space separated lists and repeated -f flags. # "read -a" splits without pathname expansion (unlike an unquoted # expansion, which would glob "*" against the current directory). IFS=', ' read -ra _list <<< "$2" compforms+=("${_list[@]}") shift 2 ;; -K|--keep-all) keepall=1 shift ;; -p|--parallel) nproc=$2 shift 2 ;; -v|--verbose) verbose=1 shift ;; -q|--quiet) quiet=1 shift ;; -[1-9]) complevel="${1#-}" shift ;; --) shift break ;; *) disp E "Invalid option, use \"taz --help\" to display options list" return 1 ;; esac done # The remaining arguments after -- are the files/directories to process, # "." is used if none is given local FILES=("$@") [[ ${#FILES[@]} -eq 0 ]] && FILES=(".") # Defaults from the [compress] configuration for anything not set by flags [[ -z $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-auto} [[ -z $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6} if [[ ! $complevel =~ ^[1-9]$ ]]; then disp E "Invalid compression level '${complevel}' (expected 1 to 9)." return 1 fi [[ -n $verbose && -n $quiet ]] && { disp E "The --verbose and --quiet options can't be used together." return 1 } # disp() hides info messages when QUIET=1; being local, this only lasts # for the duration of this call (dynamic scoping). [[ -n $quiet ]] && local QUIET=1 # Fall back to the configured (or built-in) default format list. if [[ ${#compforms[@]} -eq 0 ]]; then IFS=', ' read -ra _list <<< "${TAZ_DEFAULT_FORMAT:-lz}" compforms+=("${_list[@]}") fi # Reject unknown formats early so we never dispatch to a missing _do*. # Membership test: the padded list is matched against " " so that # only whole words match (e.g. "z" or "lz xz" are rejected). local _supported=" lz xz bz2 gz lzo tar " for _f in "${compforms[@]}"; do [[ $_supported == *" $_f "* ]] || { disp E "Unsupported format '${_f}' (supported: lz xz bz2 gz lzo tar)." return 1 } done # Backward compatibility: 0 previously meant auto-detect. [[ $nproc == 0 ]] && nproc=auto if [[ $nproc == auto ]]; then nproc=$(_taz_detect_cpus) elif [[ ! $nproc =~ ^[1-9][0-9]*$ ]]; then disp E "Invalid value for --parallel: '$nproc' (expected auto or a positive integer)." return 1 fi # Map each format to the extension its compressor appends to the input # name. "tar" is handled separately (its output is always "$item.tar"). local -A _taz_ext=( [lz]=lz [xz]=xz [bz2]=bz2 [gz]=gz [lzo]=lzo ) local item for item in "${FILES[@]}"; do local donetar=0 # Arguments naming the tar members; the default keeps the path as typed local tar_src=() # Normalise the name, since archive names are built as "$item.": # - strip trailing slashes (shell completion adds them), otherwise # "dir/.tar" would be created inside the directory being archived; # - "." / ".." (or "x/..") have no usable name, so resolve them; # - protect names starting with "-" from being read as options. while [[ ${#item} -gt 1 && $item == */ ]]; do item=${item%/} done case ${item##*/} in .|..) item=$(realpath -- "$item") || continue # Store the bare directory name, not its absolute path tar_src=(-C "${item%/*}" -- "${item##*/}") ;; esac if [[ $item == / ]]; then disp E "Refusing to archive the root directory, skipping." continue fi [[ $item == -* ]] && item="./${item}" [[ ${#tar_src[@]} -eq 0 ]] && tar_src=(-- "$item") disp I "Processing $item..." if [[ ! -e $item ]]; then disp W "Path ${item} does not exist, skipping." continue fi if [[ -d "$item" ]]; then # Directories can only be compressed through an intermediate tar. if [[ -e "$item.tar" ]]; then disp E "${item}.tar already exists, not overwriting it, skipping." continue fi disp I "\t Creating $item.tar... " if ! tar -cf "$item.tar" "${tar_src[@]}"; then disp E "tar file creation failed, skipping to next item." rm -f -- "$item.tar" continue fi donetar=1 fi # File actually handed to the compressors local fname=$item [[ $donetar -gt 0 ]] && fname=$item.tar # Compress the archive with every requested format; the smallest # resulting file is kept and the others are discarded (unless --keep-all). local candidates=() fmt exec_code aborted=0 tar_requested=0 for fmt in "${compforms[@]}"; do if [[ $fmt == tar ]]; then tar_requested=1 if [[ $donetar -eq 0 ]]; then # Plain file: "tar" means wrapping it in a tar. It must be # a separate candidate, never the source file itself, as # non-winning candidates get deleted below. if [[ -e "$item.tar" ]]; then disp W "${item}.tar already exists, skipping the tar format." continue fi if ! tar -cf "$item.tar" "${tar_src[@]}"; then disp W "tar creation failed, skipping this format." rm -f -- "$item.tar" continue fi fi candidates+=("$item.tar") continue fi disp I "\t Compressing archive with ${fmt}..." # "_do" is resolved dynamically (formats were validated above). # "|| exec_code=$?" captures the status without tripping set -e. exec_code=0 "_do$fmt" "$fname" "$nproc" "$complevel" "$verbose" || exec_code=$? case $exec_code in 0) candidates+=("$fname.${_taz_ext[$fmt]}") ;; 127) disp E "Compression program for '${fmt}' unavailable, aborting." aborted=1 break ;; *) disp W "Compression with '${fmt}' failed, skipping this format." ;; esac done # Remove partial output produced before an unrecoverable abort. local c if [[ $aborted -eq 1 ]]; then for c in "${candidates[@]}"; do [[ $c != "$fname" ]] && rm -f -- "$c" done [[ $donetar -gt 0 ]] && rm -f -- "$fname" return 127 fi # Select the smallest archive among the produced candidates. # _csize maps each existing candidate to its size in bytes; winner_size # starts at -1 meaning "no candidate seen yet". local winner="" winner_size=-1 sz local -A _csize=() for c in "${candidates[@]}"; do [[ -f $c ]] || continue sz=$(stat -c%s -- "$c" 2>/dev/null) || continue _csize["$c"]=$sz if [[ $winner_size -lt 0 || $sz -lt $winner_size ]]; then winner_size=$sz winner=$c fi done if [[ -z $winner ]]; then disp E "No archive could be produced for ${item}, skipping to next item." [[ $donetar -gt 0 ]] && rm -f -- "$fname" continue fi # In verbose mode, list every produced archive with its size and how # much larger it is than the smallest one that will be kept. if [[ -n $verbose && ${#_csize[@]} -gt 0 ]]; then disp I "\t Produced archives:" for c in "${candidates[@]}"; do [[ -n ${_csize[$c]+x} ]] || continue if [[ $c == "$winner" ]]; then disp "\t ${c} $(_taz_human "${_csize[$c]}") (kept, smallest)" else disp "\t ${c} $(_taz_human "${_csize[$c]}") (+$(_taz_human "$(( ${_csize[$c]} - winner_size ))"))" fi done fi # Discard every archive but the smallest, unless --keep-all was given. # Either way, drop the intermediate tar when it was not itself a # requested output format. if [[ -z $keepall ]]; then for c in "${candidates[@]}"; do [[ $c != "$winner" ]] && rm -f -- "$c" done fi [[ $donetar -gt 0 && $tar_requested -eq 0 ]] && rm -f -- "$fname" if [[ -n $keepall ]]; then [[ ${#compforms[@]} -gt 1 ]] && disp I "\t Kept all produced archives (smallest: ${winner}, $(_taz_human "$winner_size"))." else [[ ${#compforms[@]} -gt 1 ]] && disp I "\t Kept smallest archive: ${winner} ($(_taz_human "$winner_size"))." fi if [[ $willrm ]]; then disp I "\t Deleting original source as asked... " rm -r -- "$item" fi done } export -f taz # ------------------------------------------------------------------------------ # EOF