diff --git a/profile.d/compress.sh b/profile.d/compress.sh index 3d1cc7b..dfbb81a 100644 --- a/profile.d/compress.sh +++ b/profile.d/compress.sh @@ -115,9 +115,11 @@ utaz() # shellcheck disable=SC2329 _unlha() { - # lha typically extracts into the current directory - # We ensure it hits the target directory - (cd "$2" && lha -x "../$1") >/dev/null 2>&1 + # 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 @@ -146,8 +148,11 @@ utaz() # shellcheck disable=SC2329 _uncpio() { - # CPIO requires careful directory handling - (cd "$2" && cpio -id < "../$1") >/dev/null 2>&1 + # 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 @@ -249,8 +254,10 @@ utaz() esac fi - [[ -n ${createdir} && -n ${nodir} ]] && \ + [[ -n ${createdir} && -n ${nodir} ]] && { disp E "The --create-dir and --no-dir options are mutually exclusive." + return 1 + } for zitem in "${FILES[@]}"; do # Build list of input files to process, with whitespace-safe handling. @@ -353,6 +360,10 @@ utaz() 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 "The filesystem can't create directories, exit!" && return 1 @@ -373,7 +384,7 @@ utaz() ;; *) disp E "The compressed file ${f} seems corrupted, failed." - rm -rf "${dir}" >/dev/null 2>&1 + [[ ${dir_created} -eq 1 ]] && rm -rf "${dir}" >/dev/null 2>&1 continue ;; esac @@ -444,6 +455,17 @@ taz() 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() @@ -575,6 +597,7 @@ taz() return 1 fi eval set -- "$PARSED" + local compforms=() while true; do case "$1" in -h|--help) @@ -608,7 +631,12 @@ taz() ;; -f|--format) - local compform=$2 + # Accept comma/space separated lists and repeated -f flags. + local _f + # shellcheck disable=SC2086 # intentional word splitting on the list + for _f in ${2//,/ }; do + compforms+=("$_f") + done shift 2 ;; @@ -628,7 +656,7 @@ taz() ;; -[1-9]) - complevel="${1#-}" + local complevel="${1#-}" shift ;; --) @@ -647,11 +675,32 @@ taz() local FILES=("$@") [[ ${#FILES[@]} -eq 0 ]] && FILES=(".") - [[ ! $compform ]] && compform=${TAZ_DEFAULT_FORMAT:-lz} - [[ ! $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-auto} - [[ ! $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6} - [[ $verbose -gt 1 && $quiet -gt 1 ]] && + [[ ! $compform ]] && local compform=${TAZ_DEFAULT_FORMAT:-lz} + [[ ! $nproc ]] && local nproc=${TAZ_DEFAULT_THREADS:-auto} + [[ ! $complevel ]] && local complevel=${TAZ_DEFAULT_LEVEL:-6} + [[ -n $verbose && -n $quiet ]] && { disp E "The --verbose and --quiet options can't be used together." + return 1 + } + + # Fall back to the configured (or built-in) default format list. + local _f + if [[ ${#compforms[@]} -eq 0 ]]; then + local _deffmt=${TAZ_DEFAULT_FORMAT:-lz} + # shellcheck disable=SC2086 # intentional word splitting on the list + for _f in ${_deffmt//,/ }; do + compforms+=("$_f") + done + fi + + # Reject unknown formats early so we never dispatch to a missing _do*. + 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 @@ -662,6 +711,10 @@ taz() return 1 fi + # Map each format to the extension its compressor appends. "tar" performs + # no compression, so its candidate is the tar (or source file) itself. + local -A _taz_ext=( [lz]=lz [xz]=xz [bz2]=bz2 [gz]=gz [lzo]=lzo [tar]="" ) + for item in "${FILES[@]}"; do local donetar=0 disp I "Processing $item..." @@ -680,31 +733,91 @@ taz() local fname=$item [[ $donetar -gt 0 ]] && fname=$item.tar - # Skip compression part if tar is asked - if [[ $compform != "tar" ]]; then - disp I "\t Compressing archive..." - local exec_code=0 - "_do$compform" "$fname" "$nproc" "$complevel" "$verbose" || exec_code=$? - [[ ! $exec_code -eq 0 ]] && case $exec_code in + # Compress the archive with every requested format; the smallest + # resulting file is kept and the others are discarded. + local candidates=() fmt exec_code aborted=0 + for fmt in "${compforms[@]}"; do + if [[ $fmt == tar ]]; then + candidates+=("$fname") + continue + fi + + disp I "\t Compressing archive with ${fmt}..." + 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 unavailable, aborting." - return 127 + disp E "Compression program for '${fmt}' unavailable, aborting." + aborted=1 + break ;; *) - disp E "Compression program returned an error, not deleting anything if asked, skipping to next item." - continue + disp W "Compression with '${fmt}' failed, skipping this format." ;; esac + done - [[ $donetar -gt 0 ]] && rm "$fname" + # 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. + 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, then drop the intermediate + # tar unless it is the kept archive. + for c in "${candidates[@]}"; do + [[ $c != "$winner" ]] && rm -f -- "$c" + done + [[ $donetar -gt 0 && $fname != "$winner" ]] && rm -f -- "$fname" + + [[ ${#compforms[@]} -gt 1 ]] && + disp I "\t Kept smallest archive: ${winner} ($(_taz_human "$winner_size"))." + if [[ $willrm ]]; then disp I "\t Deleting original source as asked... " rm -r "$item" fi done - unset quiet } export -f taz # ------------------------------------------------------------------------------