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

  • extract recognises 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.
  • compress supports 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?

FormatPractical advantageCommon use
tar.gzBroad compatibility and straightforward processingQuick backups, source code and file distribution
tar.bz2Compatible with older systemsLegacy archives already using Bzip2
tar.xzPrioritises smaller output filesDistribution and long-term storage
tar.zstStrong balance between speed and compressionBackups, logs and large datasets
zipEasy to open on Linux, Windows and macOSSharing files with non-technical users
tarGroups files without compressing themAlready 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.

Scroll to Top