How to Create a Bash File Unarchiving Tool

How to Create a Bash File Unarchiving Tool

Anyone who’s spent time at a terminal has run into the same small annoyance: a .tar.gz here, a .zip there, a .7z from a colleague, and each one demands a slightly different command with slightly different flags. Remembering tar -xzf versus unzip versus 7z x isn’t hard individually, but it adds friction. The fix is a single Bash script that detects the archive type and extracts it correctly — a universal unarchiving tool.

This article builds that tool step by step, explains the mechanics of archive detection, and covers the practical considerations that turn a toy script into something reliable enough for daily use.

Why a Universal Unarchiving Script Is Worth Building

Most people reach for whatever tool they remember first, then look up the syntax when it doesn’t work. A wrapper script solves this permanently:

  • One command (unpack file.ext) regardless of archive format.
  • Consistent behavior across .zip, .tar, .tar.gz, .tar.bz2, .tar.xz, .rar, and .7z.
  • Easy to extend when a new format shows up.
  • Useful as a building block in larger automation — download-and-extract pipelines, batch processing, CI artifact handling.

Step 1: Detecting Archive Type

The naive approach is to check the file extension with a case statement. It’s simple and works for the overwhelming majority of real-world files:

#!/usr/bin/env bash
set -euo pipefail

FILE="$1"

if [[ ! -f "$FILE" ]]; then
    echo "Error: '$FILE' not found."
    exit 1
fi

case "$FILE" in
    *.tar.gz|*.tgz)   tar -xzf "$FILE" ;;
    *.tar.bz2|*.tbz2) tar -xjf "$FILE" ;;
    *.tar.xz)         tar -xJf "$FILE" ;;
    *.tar)            tar -xf  "$FILE" ;;
    *.zip)            unzip "$FILE" ;;
    *.rar)            unrar x "$FILE" ;;
    *.7z)             7z x "$FILE" ;;
    *.gz)             gunzip "$FILE" ;;
    *.bz2)            bunzip2 "$FILE" ;;
    *)
        echo "Error: unsupported archive format for '$FILE'"
        exit 1
        ;;
esac

echo "Extracted: $FILE"

How This Works

  • case "$FILE" in ... esac performs pattern matching on the filename. Each pattern (*.tar.gz, *.zip, etc.) is checked in order, and the first match wins.
  • tar -xzf extracts (x), through gzip decompression (z), from the given file (f). The flag order matters less than people think, but keeping f last, right before the filename, is a safe convention.
  • -xjf uses bzip2 decompression (j), and -xJf uses xz decompression (J — capital, since lowercase j is already taken by bzip2).
  • unzip, unrar x, and 7z x are the respective native tools for their formats; there’s no unified Unix API for these, so we shell out to each one.

Save this as unpack.sh, make it executable, and test it:

chmod +x unpack.sh
./unpack.sh project-backup.tar.gz

Output:

Extracted: project-backup.tar.gz

Step 2: Detecting by Content, Not Just Extension

Extensions can lie — a renamed file, a download with no extension, or a mislabeled upload. The file command inspects the actual byte signature (magic numbers) to identify format regardless of name:

detect_type() {
    file --brief --mime-type "$1"
}

TYPE=$(detect_type "$FILE")

case "$TYPE" in
    application/gzip)        tar -xzf "$FILE" 2>/dev/null || gunzip -k "$FILE" ;;
    application/x-bzip2)     tar -xjf "$FILE" 2>/dev/null || bunzip2 -k "$FILE" ;;
    application/x-xz)        tar -xJf "$FILE" 2>/dev/null || unxz -k "$FILE" ;;
    application/x-tar)       tar -xf "$FILE" ;;
    application/zip)         unzip "$FILE" ;;
    application/x-rar)       unrar x "$FILE" ;;
    application/x-7z-compressed) 7z x "$FILE" ;;
    *)
        echo "Unknown archive type: $TYPE"
        exit 1
        ;;
esac

Here, tar -xzf is attempted first for gzip files because a .gz-compressed file could either be a plain gzipped file or a gzipped tarball; falling back to plain gunzip handles the non-tar case. This dual-detection approach (extension first, MIME type as a fallback or verification) is considerably more robust.

Step 3: Combining Extension and MIME Detection

The most reliable script uses extension matching as the fast path, and falls back to file-based detection when the extension is missing or ambiguous:

#!/usr/bin/env bash
set -euo pipefail

FILE="$1"
DEST="${2:-.}"

[[ ! -f "$FILE" ]] && { echo "File not found: $FILE"; exit 1; }
mkdir -p "$DEST"

extract_by_extension() {
    case "$1" in
        *.tar.gz|*.tgz)   tar -xzf "$1" -C "$2" ;;
        *.tar.bz2|*.tbz2) tar -xjf "$1" -C "$2" ;;
        *.tar.xz)         tar -xJf "$1" -C "$2" ;;
        *.tar)            tar -xf  "$1" -C "$2" ;;
        *.zip)            unzip -q "$1" -d "$2" ;;
        *.rar)            unrar x -inul "$1" "$2" ;;
        *.7z)             7z x -o"$2" "$1" > /dev/null ;;
        *.gz)             gunzip -k "$1" ;;
        *.bz2)            bunzip2 -k "$1" ;;
        *) return 1 ;;
    esac
}

if extract_by_extension "$FILE" "$DEST"; then
    echo "Extracted '$FILE' to '$DEST' (by extension)"
    exit 0
fi

TYPE=$(file --brief --mime-type "$FILE")
echo "Extension unrecognized, detected MIME type: $TYPE"

case "$TYPE" in
    application/zip)             unzip -q "$FILE" -d "$DEST" ;;
    application/x-tar)           tar -xf "$FILE" -C "$DEST" ;;
    application/gzip)            tar -xzf "$FILE" -C "$DEST" 2>/dev/null || gunzip -k "$FILE" ;;
    application/x-7z-compressed) 7z x -o"$DEST" "$FILE" > /dev/null ;;
    *)
        echo "Unsupported or unknown archive type: $TYPE"
        exit 1
        ;;
esac

echo "Extracted '$FILE' to '$DEST'"

The -C "$DEST" flag tells tar to change to the destination directory before extracting, avoiding a separate cd and keeping the current working directory untouched — an important detail for scripts run in automation pipelines where the caller’s working directory shouldn’t shift unexpectedly.

Step 4: Handling Nested and Multi-Part Archives

Some archives are compressed twice (.tar.gz is really “tar, then gzip”) and some are split into multiple parts (.tar.gz.001, .rar multi-volume sets). A more advanced version can loop:

extract_recursive() {
    local file="$1"
    local dest="$2"
    extract_by_extension "$file" "$dest"

    # If exactly one file was extracted and it's still an archive, recurse
    local contents
    contents=$(find "$dest" -maxdepth 1 -type f)
    local count
    count=$(echo "$contents" | wc -l)

    if [[ "$count" -eq 1 ]] && file --brief "$contents" | grep -qiE 'archive|compressed'; then
        echo "Nested archive detected: $contents"
        extract_recursive "$contents" "$dest"
    fi
}

This checks whether the extraction left behind a single file that itself looks like an archive, and if so, extracts again — useful for double-compressed downloads.

Real-World Use Cases

  • Download-and-extract pipelines: Pair with curl or wget to fetch and immediately unpack release artifacts in a build script.
  • Log rotation cleanup: Batch-extract .gz logs before running analysis tools that expect plain text.
  • Dataset preparation: Machine learning workflows often distribute datasets as .tar.gz or .zip; a universal extractor simplifies setup scripts shared across a team.
  • Incoming file processing: A watched “inbox” directory that automatically unpacks whatever lands in it, regardless of format.

Automation Example: Batch Unpacking a Directory

#!/usr/bin/env bash
set -euo pipefail

for archive in ./downloads/*; do
    [[ -f "$archive" ]] || continue
    ./unpack.sh "$archive" "./extracted/$(basename "$archive" | sed 's/\.[^.]*$//')"
done

This loops through every file in ./downloads, extracting each into its own subdirectory under ./extracted, named after the archive (extension stripped).

Best Practices

  • Always extract into a dedicated destination directory rather than the current directory, to avoid clutter and accidental overwrites (zip bomb protection also benefits from this — see security below).
  • Use quiet flags (-q for unzip, > /dev/null for 7z) in scripts to keep automated output clean, but keep verbose output available via a --verbose flag for manual debugging.
  • Check tool availability before use with command -v unzip &>/dev/null || { echo "unzip not installed"; exit 1; }.
  • Prefer -k (keep) flags on gunzip/bunzip2 so the original compressed file isn’t deleted, which is safer for repeatable scripts.

Security Considerations

  • Zip bombs and archive bombs: A small archive can expand to an enormous size and exhaust disk space. Check the uncompressed size before extracting fully-untrusted archives: unzip -l file.zip or tar -tzf file.tar.gz list contents without extracting, and their reported sizes can be summed and checked against a threshold first.
  • Path traversal: Malicious archives can contain entries like ../../etc/passwd. Modern tar and unzip versions block this by default, but it’s worth verifying the installed version handles it, especially on older systems.
  • Never extract untrusted archives as root. Run extraction as an unprivileged user in a sandboxed or disposable directory when the source isn’t trusted.

Optimization Tips

  • For very large archives, extracting directly with tar is faster than combining separate decompression and extraction steps, since tar‘s built-in flags (-z, -j, -J) stream decompression rather than writing an intermediate decompressed file to disk.
  • Use pigz (parallel gzip) as a drop-in decompression accelerator on multi-core systems: tar --use-compress-program=pigz -xf file.tar.gz.

Troubleshooting

  • “tar: Unrecognized archive format”: The file extension likely doesn’t match the actual compression; try the MIME-type detection fallback.
  • “unzip: command not found”: Install it via the system package manager (apt install unzip, dnf install unzip, etc.) — this script assumes the relevant binary is present.
  • Extraction succeeds but files are missing: Check for path traversal protection messages in the tool’s output; some tools silently skip suspicious entries.

Common Mistakes

  • Forgetting -C "$DEST" and extracting into the current working directory unintentionally.
  • Not handling missing tools (unrar, 7z) gracefully, causing a confusing “command not found” instead of a clear error message.
  • Assuming file extension always matches actual content, which breaks silently on renamed or mislabeled files.

FAQs

Can this handle password-protected archives? Yes, with modification — pass -P password to unzip or prompt interactively; avoid hardcoding passwords in scripts.

What about .tar.zst (Zstandard)? Add a case for *.tar.zst using tar --zstd -xf, assuming zstd is installed.

Does this work on macOS? Mostly yes; tar, gzip, and unzip ship by default, though 7z and unrar need to be installed separately via Homebrew.

Summary

A universal unarchiving tool eliminates one of the most repetitive small tasks at the command line. By combining extension-based fast detection with MIME-type fallback via the file command, the script handles the vast majority of real-world archives reliably, while staying simple enough to extend for new formats as they come up.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash File Archiving Tool

How to Create a Bash File Archiving Tool

Next Post
How to Create a Bash File Version Control System

How to Create a Bash File Version Control System

Related Posts