849 lines
28 KiB
Bash
849 lines
28 KiB
Bash
#!/usr/bin/env bash
|
|
# ------------------------------------------------------------------------------
|
|
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
|
# 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()
|
|
{
|
|
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()
|
|
{
|
|
if command -v plzip >/dev/null 2>&1; then
|
|
plzip -d -c "$1" > "$2/$(basename "${1%.*}")"
|
|
else
|
|
lzip -d -c "$1" > "$2/$(basename "${1%.*}")"
|
|
fi
|
|
}
|
|
|
|
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()
|
|
{
|
|
# Zstd decompresses files directly, often requiring tar for archives
|
|
tar --zstd -xf "$1" -C "$2"
|
|
}
|
|
|
|
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
|
|
rpm2cpio "$1" | (cd "$2/" && cpio -idmv) >/dev/null 2>&1
|
|
}
|
|
|
|
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
|
|
local -a _pargs=()
|
|
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
|
|
set -- "${_pargs[@]}"
|
|
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- zst\n"
|
|
printf "\t- cpio\n"
|
|
printf "\t- cab\n"
|
|
printf "\t- deb\n"
|
|
printf "\t- rpm\n"
|
|
return 0
|
|
;;
|
|
-d|--delete)
|
|
local willrm=1
|
|
shift
|
|
;;
|
|
-c|--create-dir)
|
|
local createdir=1
|
|
shift
|
|
;;
|
|
-n|--no-dir)
|
|
local 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+x} ]] && [[ ${UTAZ_DEFAULT_DELETE:-0} -eq 1 ]] && willrm=1
|
|
if [[ -z ${createdir+x} && -z ${nodir+x} ]]; 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
|
|
}
|
|
|
|
for zitem in "${FILES[@]}"; do
|
|
# Build list of input files to process, with whitespace-safe handling.
|
|
local targets=()
|
|
if [[ -f "$zitem" ]]; then
|
|
targets=("$zitem")
|
|
elif [[ -d "$zitem" ]]; then
|
|
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
|
|
local dir="${f%.*}"
|
|
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)
|
|
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 binary existence
|
|
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
|
|
}
|
|
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 "The filesystem can't create directories, exit!" &&
|
|
return 1
|
|
fi
|
|
|
|
${extractor} "${f}" "${dir}"
|
|
case $? in
|
|
0)
|
|
[[ -n ${willrm} ]] &&
|
|
rm -f "${f}" && disp I "File ${zitem}/${f} deleted."
|
|
;;
|
|
1)
|
|
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
|
|
|
|
if [[ -n ${createdir} ]]; then
|
|
disp I "Archive extracted successfully in subdirectory."
|
|
elif [[ -n ${nodir} ]]; then
|
|
shopt -s nullglob
|
|
for child in "${dir}"/*; do
|
|
mv -- "$child" .
|
|
done
|
|
shopt -u nullglob
|
|
rmdir -- "${dir}"
|
|
disp I "Archive extracted successfully, no subdirectory needed."
|
|
else
|
|
# Set nullglob to ensure the array is empty if no files match
|
|
shopt -s nullglob
|
|
local contents=( "${dir}"/* )
|
|
|
|
# Check if exactly one item exists and if that item is a directory
|
|
if [[ ${#contents[@]} -eq 1 ]] && [[ -d "${contents[0]}" ]]; then
|
|
# Single directory detected
|
|
shopt -s nullglob
|
|
for child in "${contents[0]}"/*; do
|
|
mv -- "$child" .
|
|
done
|
|
shopt -u nullglob
|
|
rmdir -- "${dir}"
|
|
disp I "Archive extracted successfully, no subdirectory needed."
|
|
else
|
|
disp I "Archive extracted successfully in subdirectory."
|
|
fi
|
|
shopt -u nullglob
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
export -f utaz
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Compress directories or files into one or more archive
|
|
# Usage: taz [option] [--parallel=<n|auto>] [--format=<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()
|
|
{
|
|
# 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 --compress"
|
|
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
|
|
local -a _pargs=()
|
|
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
|
|
set -- "${_pargs[@]}"
|
|
local compforms=()
|
|
while true; do
|
|
case "$1" in
|
|
-h|--help)
|
|
printf "taz: archive all files of a directory.\n\n"
|
|
printf "Usage: taz [option] [--parallel=<n|auto>] [--format=<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 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)
|
|
local willrm=1
|
|
shift
|
|
;;
|
|
|
|
-f|--format)
|
|
# 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
|
|
;;
|
|
|
|
-K|--keep-all)
|
|
local keepall=1
|
|
shift
|
|
;;
|
|
|
|
-p|--parallel)
|
|
local nproc=$2
|
|
shift 2
|
|
;;
|
|
|
|
-v|--verbose)
|
|
local verbose=1
|
|
shift
|
|
;;
|
|
|
|
-q|--quiet)
|
|
local quiet=1
|
|
shift
|
|
;;
|
|
|
|
-[1-9])
|
|
local 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=(".")
|
|
|
|
[[ ! $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
|
|
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. "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..."
|
|
|
|
if [[ -d "$item" ]]; then
|
|
disp I "\t Creating $item.tar... "
|
|
|
|
if ! tar -cf "$item.tar" "$item"; then
|
|
disp E "tar file creation failed, skipping to next item."
|
|
continue
|
|
fi
|
|
|
|
local donetar=1
|
|
fi
|
|
|
|
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
|
|
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 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.
|
|
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
|