Working with compressed files on Linux often means remembering different commands for ZIP, TAR.GZ, XZ, Zstandard, 7-Zip, RAR, RPM and DEB packages. Two small Bash scripts can reduce that entire collection to two commands: extract for unpacking files and compress for creating timestamped archives with validation and format control.
The key facts about these Linux scripts in 30 seconds
extractrecognises the most common formats and can process several archives in one run.- Each archive is unpacked into a separate directory to prevent accidental overwrites.
- The script detects missing tools and rejects potentially dangerous paths inside TAR and ZIP files.
compresssupports TAR, Gzip, Bzip2, XZ, Zstandard and ZIP.- Archives are created in a temporary location and validated before being moved to their final destination.
The original idea is simple, but it benefits from a few additional safeguards. Running gunzip file.gz, for example, normally replaces the compressed file with its contents. Extracting several archives directly into the current directory may also mix folders, overwrite existing files or leave behind an incomplete result when the operation fails.
The versions below preserve the original archives, use temporary directories and do not publish the final result until the operation has completed successfully. They also display a clear message when unzip, 7z, unrar, zstd, dpkg-deb or another required dependency is missing.
GNU Tar can create and read archives compressed with Gzip, Bzip2, XZ and Zstandard, among other formats. When extracting from a file, it can usually detect the compression method automatically from the internal signature and, when necessary, from the extension.
One command to extract TAR, ZIP, 7Z, RAR, DEB and RPM files
The first script accepts one or more archives. Instead of unpacking everything into the directory from which it is launched, it creates a separate folder for each item.
For example, backup.tar.gz will be extracted into backup/. If that directory already exists, the script generates another name containing the date, time and a counter.
Save it as extract:
#!/usr/bin/env bash
# extract — cautious universal archive extractor for Linux
# Usage: extract [options] <archive> [archive ...]
# Options:
# -d, --directory DIR Root output directory (default: .)
# -l, --list List contents without extracting
# -v, --verbose Show processed files
# -h, --help Show help
set -u -o pipefail
ROOT="."
LIST_ONLY=0
VERBOSE=0
usage() {
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
}
error() {
printf 'extract: %s\n' "$*" >&2
}
need() {
command -v "$1" >/dev/null 2>&1 || {
error "required tool '$1' is not installed"
return 127
}
}
archive_stem() {
local name=${1##*/}
local lower=${name,,}
case "$lower" in
*.tar.gz)
printf '%s\n' "${name:0:${#name}-7}"
;;
*.tar.bz2)
printf '%s\n' "${name:0:${#name}-8}"
;;
*.tar.xz)
printf '%s\n' "${name:0:${#name}-7}"
;;
*.tar.zst)
printf '%s\n' "${name:0:${#name}-8}"
;;
*.tbz2)
printf '%s\n' "${name:0:${#name}-5}"
;;
*.tgz|*.txz)
printf '%s\n' "${name:0:${#name}-4}"
;;
*.zip|*.rar|*.deb|*.rpm|*.tar)
printf '%s\n' "${name%.*}"
;;
*.7z|*.gz|*.bz2|*.xz|*.zst)
printf '%s\n' "${name%.*}"
;;
*.z)
printf '%s\n' "${name:0:${#name}-2}"
;;
*)
printf '%s\n' "$name"
;;
esac
}
unique_destination() {
local base="$ROOT/$(archive_stem "$1")"
local candidate=$base
local n=1
while [[ -e "$candidate" ]]; do
candidate="${base}_$(date +%Y%m%d_%H%M%S)_$n"
((n++))
done
printf '%s\n' "$candidate"
}
check_member_paths() {
# Reject absolute paths and ".." components in TAR and ZIP archives.
awk '
{
p=$0
gsub(/\\/, "/", p)
if (
p ~ /^\// ||
p ~ /^[A-Za-z]:\// ||
p ~ /(^|\/)\.\.($|\/)/
) {
print \
"extract: unsafe path inside archive: " \
$0 > "/dev/stderr"
bad=1
}
}
END {
exit bad ? 1 : 0
}
'
}
list_archive() {
local file=$1
local lower=${file,,}
case "$lower" in
*.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tbz2|\
*.tar.xz|*.txz|*.tar.zst)
need tar && tar -tf "$file"
;;
*.zip)
need unzip && unzip -Z1 "$file"
;;
*.7z)
need 7z && 7z l "$file"
;;
*.rar)
need unrar && unrar lb "$file"
;;
*.deb)
need dpkg-deb && dpkg-deb -c "$file"
;;
*.rpm)
if command -v rpm2archive >/dev/null 2>&1; then
rpm2archive "$file" | tar -tzf -
else
need rpm2cpio &&
need cpio &&
rpm2cpio "$file" | cpio -t
fi
;;
*.gz|*.bz2|*.xz|*.zst|*.z)
printf '%s\n' "$(archive_stem "$file")"
;;
*)
error "unknown format: '$file'"
return 2
;;
esac
}
extract_archive() {
local file=$1
local lower=${file,,}
local destination
local tmp
local output_name
destination=$(unique_destination "$file")
tmp=$(mktemp -d "$ROOT/.extract.XXXXXX") || return 1
case "$lower" in
*.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tbz2|\
*.tar.xz|*.txz|*.tar.zst)
need tar || {
rm -rf -- "$tmp"
return 1
}
if ! tar -tf "$file" | check_member_paths; then
rm -rf -- "$tmp"
return 1
fi
if ((VERBOSE)); then
tar -xvf "$file" -C "$tmp"
else
tar -xf "$file" -C "$tmp"
fi
;;
*.zip)
need unzip || {
rm -rf -- "$tmp"
return 1
}
if ! unzip -Z1 "$file" | check_member_paths; then
rm -rf -- "$tmp"
return 1
fi
if ((VERBOSE)); then
unzip "$file" -d "$tmp"
else
unzip -q "$file" -d "$tmp"
fi
;;
*.7z)
need 7z || {
rm -rf -- "$tmp"
return 1
}
if ((VERBOSE)); then
7z x -y "-o$tmp" "$file"
else
7z x -y -bso0 -bsp0 "-o$tmp" "$file"
fi
;;
*.rar)
need unrar || {
rm -rf -- "$tmp"
return 1
}
if ((VERBOSE)); then
unrar x -o+ "$file" "$tmp/"
else
unrar x -idq -o+ "$file" "$tmp/"
fi
;;
*.deb)
need dpkg-deb || {
rm -rf -- "$tmp"
return 1
}
# Extract package files and metadata into DEBIAN/.
dpkg-deb -R "$file" "$tmp"
;;
*.rpm)
need tar || {
rm -rf -- "$tmp"
return 1
}
if command -v rpm2archive >/dev/null 2>&1; then
rpm2archive "$file" |
tar -xzf - -C "$tmp"
else
need rpm2cpio && need cpio || {
rm -rf -- "$tmp"
return 1
}
rpm2cpio "$file" |
(
cd "$tmp" &&
cpio -idm --quiet
)
fi
;;
*.gz)
need gzip || {
rm -rf -- "$tmp"
return 1
}
output_name=$(archive_stem "$file")
gzip -dc -- "$file" >"$tmp/$output_name"
;;
*.bz2)
need bzip2 || {
rm -rf -- "$tmp"
return 1
}
output_name=$(archive_stem "$file")
bzip2 -dc -- "$file" >"$tmp/$output_name"
;;
*.xz)
need xz || {
rm -rf -- "$tmp"
return 1
}
output_name=$(archive_stem "$file")
xz -dc -- "$file" >"$tmp/$output_name"
;;
*.zst)
need zstd || {
rm -rf -- "$tmp"
return 1
}
output_name=$(archive_stem "$file")
zstd -q -dc -- "$file" >"$tmp/$output_name"
;;
*.z)
need uncompress || {
rm -rf -- "$tmp"
return 1
}
output_name=$(archive_stem "$file")
uncompress -c "$file" >"$tmp/$output_name"
;;
*)
error "unknown format: '$file'"
rm -rf -- "$tmp"
return 2
;;
esac
if [[ $? -ne 0 ]]; then
error "failed to extract '$file'"
rm -rf -- "$tmp"
return 1
fi
mv -- "$tmp" "$destination"
printf 'Extracted: %s -> %s\n' \
"$file" "$destination"
}
while (($#)); do
case "$1" in
-d|--directory)
shift
[[ $# -gt 0 ]] || {
error "missing directory after --directory"
exit 2
}
ROOT=$1
;;
-l|--list)
LIST_ONLY=1
;;
-v|--verbose)
VERBOSE=1
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
error "unknown option: '$1'"
usage >&2
exit 2
;;
*)
break
;;
esac
shift
done
(($# > 0)) || {
usage >&2
exit 2
}
mkdir -p -- "$ROOT" || exit 1
if ((EUID == 0)); then
error \
"warning: avoid extracting untrusted archives as root"
fi
status=0
for file in "$@"; do
if [[ ! -f "$file" ]]; then
error \
"not found or not a regular file: '$file'"
status=1
continue
fi
if ((LIST_ONLY)); then
printf '\n==> %s <==\n' "$file"
list_archive "$file" || status=1
else
extract_archive "$file" || status=1
fi
done
exit "$status"
Code language: PHP (php)
The -l option allows users to inspect the contents before extracting:
extract --list backup.tar.zst
Code language: CSS (css)
It also accepts several archives and a common destination:
extract \
--directory "$HOME/extracted" \
backup.zip \
logs.tar.xz \
package.deb
Code language: JavaScript (javascript)
To display every processed file:
extract --verbose project.7z
Code language: CSS (css)
The most important difference compared with a basic case-based alias is that this version does not extract directly into the final destination. It first uses a hidden folder created with mktemp. If the command fails, the temporary folder is deleted and no incomplete result is left behind.
For TAR and ZIP archives, the script also checks internal filenames. It rejects absolute paths, Windows drive paths and .. components that might attempt to write outside the selected directory.
This validation does not make an archive from an unknown source trustworthy. The recommended approach remains extracting it without administrator privileges, inside an empty directory and, where the risk justifies it, within an isolated container or virtual machine.
DEB support uses dpkg-deb -R. This option extracts both the filesystem tree and the control information into DEBIAN/. Extracting a package is not the same as installing it: package-management tools should still be used for a proper installation.
For RPM files, the script prioritises rpm2archive. Current RPM documentation considers rpm2cpio obsolete because the CPIO format cannot hold individual files larger than 4 GB. When rpm2archive is unavailable, the script retains rpm2cpio as a fallback.
A script to create TAR, Gzip, XZ, Zstandard or ZIP archives
The second command receives a file or directory and creates a timestamped compressed copy. It uses tar.gz by default, but users can select another format, adjust the compression level and calculate a SHA-256 checksum.
Save it as compress:
#!/usr/bin/env bash
# compress — create verified compressed archives
# Usage: compress [options] <file-or-directory>
# Options:
# -f, --format FMT tar, gz, bz2, xz, zst or zip
# Default format: gz
# -o, --output FILE Output archive
# -l, --level N Compression level
# -c, --checksum Display SHA-256 after completion
# -v, --verbose Show files being added
# -h, --help Show help
set -Eeuo pipefail
FORMAT="gz"
OUTPUT=""
LEVEL=""
CHECKSUM=0
VERBOSE=0
TMPDIR_WORK=""
usage() {
sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'
}
error() {
printf 'compress: %s\n' "$*" >&2
}
need() {
command -v "$1" >/dev/null 2>&1 || {
error "required tool '$1' is not installed"
exit 127
}
}
cleanup() {
if [[ -n "$TMPDIR_WORK" &&
-d "$TMPDIR_WORK" ]]; then
rm -rf -- "$TMPDIR_WORK"
fi
}
trap cleanup EXIT INT TERM
validate_level() {
[[ -z "$LEVEL" ]] && return 0
[[ "$LEVEL" =~ ^[0-9]+$ ]] || {
error "compression level must be an integer"
exit 2
}
case "$FORMAT" in
gz|bz2|zip)
((LEVEL >= 1 && LEVEL <= 9)) || {
error \
"use a level between 1 and 9 for $FORMAT"
exit 2
}
;;
xz)
((LEVEL >= 0 && LEVEL <= 9)) || {
error "use a level between 0 and 9 for xz"
exit 2
}
;;
zst)
((LEVEL >= 1 && LEVEL <= 22)) || {
error \
"use a level between 1 and 22 for zst"
exit 2
}
;;
tar)
error \
"the tar format does not support compression levels"
exit 2
;;
esac
}
while (($#)); do
case "$1" in
-f|--format)
shift
[[ $# -gt 0 ]] || {
error "missing format"
exit 2
}
FORMAT=${1,,}
;;
-o|--output)
shift
[[ $# -gt 0 ]] || {
error "missing output filename"
exit 2
}
OUTPUT=$1
;;
-l|--level)
shift
[[ $# -gt 0 ]] || {
error "missing compression level"
exit 2
}
LEVEL=$1
;;
-c|--checksum)
CHECKSUM=1
;;
-v|--verbose)
VERBOSE=1
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
error "unknown option: '$1'"
usage >&2
exit 2
;;
*)
break
;;
esac
shift
done
(($# == 1)) || {
usage >&2
exit 2
}
TARGET=$1
[[ -e "$TARGET" ]] || {
error "not found: '$TARGET'"
exit 1
}
case "$FORMAT" in
tar)
EXT=".tar"
;;
gz)
EXT=".tar.gz"
;;
bz2)
EXT=".tar.bz2"
;;
xz)
EXT=".tar.xz"
;;
zst)
EXT=".tar.zst"
;;
zip)
EXT=".zip"
;;
*)
error \
"unknown format '$FORMAT'. " \
"Use: tar gz bz2 xz zst zip"
exit 2
;;
esac
validate_level
need realpath
need tar
SOURCE=$(realpath -e -- "$TARGET")
[[ "$SOURCE" != "/" ]] || {
error "refusing to compress the entire root filesystem '/'"
exit 2
}
PARENT=$(dirname -- "$SOURCE")
NAME=$(basename -- "$SOURCE")
STAMP=$(date +%Y%m%d_%H%M%S)
if [[ -z "$OUTPUT" ]]; then
OUTPUT="${NAME}_${STAMP}${EXT}"
fi
[[ ! -e "$OUTPUT" ]] || {
error "output file already exists: '$OUTPUT'"
exit 1
}
OUTPUT_DIR=$(dirname -- "$OUTPUT")
mkdir -p -- "$OUTPUT_DIR"
TMPDIR_WORK=$(
mktemp -d "${TMPDIR:-/tmp}/compress.XXXXXX"
)
TMPFILE="$TMPDIR_WORK/archive$EXT"
TAR_VERBOSE=()
((VERBOSE)) && TAR_VERBOSE=(-v)
case "$FORMAT" in
tar)
(
cd "$PARENT" &&
tar \
-c \
"${TAR_VERBOSE[@]}" \
-f "$TMPFILE" \
-- "$NAME"
)
;;
gz)
need gzip
GZIP_LEVEL=()
[[ -n "$LEVEL" ]] &&
GZIP_LEVEL=("-$LEVEL")
(
cd "$PARENT" &&
tar \
-c \
"${TAR_VERBOSE[@]}" \
-f - \
-- "$NAME"
) |
gzip \
-c \
"${GZIP_LEVEL[@]}" \
>"$TMPFILE"
;;
bz2)
need bzip2
BZ2_LEVEL=()
[[ -n "$LEVEL" ]] &&
BZ2_LEVEL=("-$LEVEL")
(
cd "$PARENT" &&
tar \
-c \
"${TAR_VERBOSE[@]}" \
-f - \
-- "$NAME"
) |
bzip2 \
-c \
"${BZ2_LEVEL[@]}" \
>"$TMPFILE"
;;
xz)
need xz
XZ_LEVEL=()
[[ -n "$LEVEL" ]] &&
XZ_LEVEL=("-$LEVEL")
(
cd "$PARENT" &&
tar \
-c \
"${TAR_VERBOSE[@]}" \
-f - \
-- "$NAME"
) |
xz \
-c \
-T0 \
"${XZ_LEVEL[@]}" \
>"$TMPFILE"
;;
zst)
need zstd
ZST_LEVEL=()
[[ -n "$LEVEL" ]] &&
ZST_LEVEL=("-$LEVEL")
(
cd "$PARENT" &&
tar \
-c \
"${TAR_VERBOSE[@]}" \
-f - \
-- "$NAME"
) |
zstd \
-q \
-c \
-T0 \
"${ZST_LEVEL[@]}" \
>"$TMPFILE"
;;
zip)
need zip
ZIP_LEVEL=()
[[ -n "$LEVEL" ]] &&
ZIP_LEVEL=("-$LEVEL")
if ((VERBOSE)); then
(
cd "$PARENT" &&
zip \
-r \
-y \
"${ZIP_LEVEL[@]}" \
"$TMPFILE" \
"$NAME"
)
else
(
cd "$PARENT" &&
zip \
-q \
-r \
-y \
"${ZIP_LEVEL[@]}" \
"$TMPFILE" \
"$NAME"
)
fi
;;
esac
# Confirm that the newly created archive can be read.
if [[ "$FORMAT" == "zip" ]]; then
need unzip
unzip -tq "$TMPFILE" >/dev/null
else
tar -tf "$TMPFILE" >/dev/null
fi
mv -- "$TMPFILE" "$OUTPUT"
SIZE=$(
du -h -- "$OUTPUT" |
awk '{print $1}'
)
printf 'Created: %s (%s)\n' \
"$OUTPUT" "$SIZE"
if ((CHECKSUM)); then
need sha256sum
sha256sum -- "$OUTPUT"
fi
Code language: PHP (php)
The simplest use creates a tar.gz archive:
compress ./my-project
The result will have a name similar to:
my-project_20260715_184530.tar.gz
Code language: CSS (css)
To use Zstandard and calculate a checksum:
compress \
--format zst \
--level 10 \
--checksum \
./backups
To create a ZIP file with a specific name:
compress \
--format zip \
--output delivery.zip \
./documentation
To create an XZ archive with stronger compression:
compress \
--format xz \
--level 9 \
./logs
The script first changes to the parent directory of the selected item. As a result, the archive contains my-project/ rather than an absolute path such as /home/user/projects/my-project/. This makes restoration easier on another machine and avoids unnecessarily storing the system’s complete directory structure.
It also rejects / as a target. A command entered by mistake should not begin compressing the entire server, including virtual filesystems, mounted volumes and directories that change while the archive is being created.
Before moving the result to the requested filename, compress attempts to read its index. For ZIP, it uses unzip -tq; for TAR-based formats, it runs tar -tf. This check does not guarantee that every byte can be recovered after a later storage failure, but it detects many empty, truncated or incomplete archives.
Which compression format should be used?
| Format | Practical advantage | Common use |
|---|---|---|
tar.gz | Broad compatibility and straightforward processing | Quick backups, source code and file distribution |
tar.bz2 | Compatible with older systems | Legacy archives already using Bzip2 |
tar.xz | Prioritises smaller output files | Distribution and long-term storage |
tar.zst | Strong balance between speed and compression | Backups, logs and large datasets |
zip | Easy to open on Linux, Windows and macOS | Sharing files with non-technical users |
tar | Groups files without compressing them | Already compressed data or intermediate processing |
Gzip compresses streams or individual files; it does not group directory trees by itself. That is why the script first creates a TAR stream and then sends it to Gzip, Bzip2, XZ or Zstandard. GNU Tar documents both this pipeline approach and automatic compressor selection based on the extension.
XZ and Zstandard can use several processor threads with the options included in the script. The chosen level should account for the fact that stronger compression normally requires more CPU time and memory. For a temporary snapshot before updating a service, the default level is usually more sensible than always selecting the maximum.
How to install both commands
After saving the files, give them execution permission:
chmod +x extract compress
They can then be launched from the current directory:
./extract backup.tar.gz
./compress ./project
To make them available as system-wide commands for authorised users:
sudo install \
-m 0755 \
extract \
/usr/local/bin/extract
sudo install \
-m 0755 \
compress \
/usr/local/bin/compress
From that point onward, users only need to type:
extract archive.zip
compress --format zst ./data
Not every Linux distribution includes the tools for all supported formats by default. Basic commands such as tar, gzip, bzip2 and xz are commonly installed, while zstd, 7z, unrar, zip, unzip, cpio and RPM utilities may require additional packages.
The script reports the missing executable directly:
extract: required tool '7z' is not installed
Code language: JavaScript (javascript)
This avoids assuming package names that differ between Debian, Ubuntu, Fedora, Rocky Linux, AlmaLinux, openSUSE and Arch Linux. Once the missing binary has been identified, the corresponding package can be located in the distribution’s repositories.
These scripts should not replace a proper backup strategy. A compressed copy stored on the same disk as the original data can disappear because of hardware failure, accidental deletion or an attack. Their purpose is to create delivery packages, quick snapshots, log bundles and copies before making a change.
Frequently asked questions
Does the extract script delete the original archive?
No. For .gz, .bz2, .xz and .zst files, it sends the decompressed output to a new file and preserves the original archive.
Can several archives be extracted with one command?
Yes. The command accepts all the archives provided and creates a separate directory for each one.
Why is every archive extracted into its own directory?
This reduces the risk of mixing contents or overwriting existing files. If the destination name already exists, the script creates another one with a timestamp and counter.
Which format is best for a quick backup?
tar.gz offers broad compatibility. tar.zst is a strong option when Zstandard is installed and fast recurring backups are needed, while ZIP is convenient when files must be shared with users on different operating systems.
