Inspect and extract ZIP files with unzip
Use unzip -l to inspect a ZIP, unzip -t to check its compressed data, and unzip -n to extract
without replacing existing files. For a directory of downloaded packages, you also need a batch
script that reports failures even when a later archive succeeds. This walkthrough gives you both a
small practice archive and a script that keeps each package in its own output directory.
Check your tools
Use Linux with Bash, Info-ZIP UnZip, and the usual cp, mkdir, mktemp, and rm utilities. You
also need Info-ZIP Zip to create the practice archive. These examples were tested with Bash 5.3.15,
UnZip 6.00, and Zip 3.0; use your distribution’s maintained packages. Check the implementations with:
bash --version && unzip -v && zip -v
The manual below is for Info-ZIP, so a different program named unzip may behave differently.
unzip -v also displays UNZIP and UNZIPOPT, environment variables that can add default options.
The individual commands below assume those variables are unset; the batch script clears them in
its own process.
Work with trusted, unencrypted ZIP files, such as packages from a publisher you have verified.
-t checks the archive’s stored CRC values; it does not authenticate its publisher or establish that
its contents are safe. An extraction directory is not a sandbox for user uploads. Untrusted archives
need an isolated worker and resource limits beyond this workflow.
Inspect a ZIP before extracting
Paste this into Bash to create unzip-demo beneath your current directory. If that name already
exists, setup stops without changing its contents. The parentheses keep your shell in its original
directory.
(
mkdir -- unzip-demo &&
cd -- unzip-demo &&
mkdir -- downloads &&
printf '{"mode":"development"}\n' > config.json &&
printf 'build complete\n' > build.log &&
zip -q ./downloads/package.zip config.json build.log
)
From the same starting directory, list the entries and then check their data:
unzip -l ./unzip-demo/downloads/package.zip &&
unzip -t ./unzip-demo/downloads/package.zip
The listing includes config.json and build.log; the test should report no errors. According to
the Info-ZIP manual, -l reads the
listing, while -t decompresses entries in memory and compares their CRCs. A successful listing
alone does not prove the data can be extracted.
Extract both entries into a separate directory:
unzip -n ./unzip-demo/downloads/package.zip -d ./unzip-demo/extracted
You should now have unzip-demo/extracted/config.json and unzip-demo/extracted/build.log.
-n skips an existing file without prompting. On a rerun, that means a locally edited
config.json stays edited even if the command returns zero. Use -o only when you intend to replace
existing files; it suppresses overwrite prompts by allowing replacement.
Extract only what you need
Pass a quoted member pattern to select JSON files:
unzip -n ./unzip-demo/downloads/package.zip '*.json' -d ./unzip-demo/config-only
This creates config-only/config.json without extracting build.log, and keeps existing files on
reruns. Quotes keep Bash from expanding *.json against your working directory; UnZip applies the
pattern to archive members. For exclusions, -x '*.log' omits matching log entries. To inspect just
the configuration without creating a file, use unzip -p ./unzip-demo/downloads/package.zip config.json.
Process a directory without hiding failures
For batch work, use a stricter output policy: an existing per-archive destination is a failure,
not a request to merge files. Save the following as extract-zips.sh in your starting directory,
then run it with bash as shown below. Do not source it into your shell.
The script processes top-level, nonhidden regular files ending in lowercase .zip; it skips
symlinks and directories. Keep the input files unchanged during the run and give the output root
to one invocation at a time. It needs space for the extracted files and a temporary copy of the
largest ZIP.
#!/usr/bin/env bash
if (( $# != 2 )) || [[ -z $1 || -z $2 ]]; then
printf 'Usage: bash extract-zips.sh INPUT_DIR OUTPUT_DIR\n' >&2
exit 2
fi
unset UNZIP UNZIPOPT
input_dir=$1
output_dir=$2
[[ $input_dir = /* ]] || input_dir="$PWD/$input_dir"
[[ $output_dir = /* ]] || output_dir="$PWD/$output_dir"
cd -P -- "$input_dir" || exit 1
input_dir=$PWD
mkdir -p -- "$output_dir" || exit 1
cd -P -- "$output_dir" || exit 1
output_dir=$PWD
scratch=$(mktemp -d -- "$output_dir/.unzip.XXXXXXXX") || exit 1
trap 'rm -rf -- "$scratch"' EXIT
shopt -s nullglob
status=0
count=0
for archive in "$input_dir"/*.zip; do
[[ -f "$archive" && ! -L "$archive" ]] || continue
count=$((count + 1))
name=${archive##*/}
destination="$output_dir/${name%.zip}"
if [[ -e "$destination" || -L "$destination" ]]; then
printf 'Destination already exists: %s\n' "$destination" >&2
status=1
continue
fi
printf 'Checking: %s\n' "$archive"
# UnZip expands archive-name wildcards even inside a quoted shell argument.
if ! cp -f -- "$archive" "$scratch/input.zip" ||
! unzip -tq -P '' "$scratch/input.zip" </dev/null; then
printf 'Cannot copy or validate: %s\n' "$archive" >&2
status=1
continue
fi
if ! mkdir -- "$destination"; then
status=1
continue
fi
if unzip -qo -P '' "$scratch/input.zip" -d "$destination" </dev/null; then
printf 'Extracted: %s\n' "$destination"
else
printf 'Extraction failed; inspect partial output: %s\n' "$destination" >&2
status=1
fi
done
if (( count == 0 )); then
printf 'No regular .zip files found in: %s\n' "$input_dir" >&2
status=1
fi
exit "$status"
Run the saved script against the practice archive:
bash ./extract-zips.sh ./unzip-demo/downloads ./unzip-demo/batch-output
A successful first run prints an Extracted: line and creates batch-output/package/config.json
and batch-output/package/build.log. A second run returns 1 and leaves that directory untouched.
Choose a new output root when you want a fresh extraction.
The temporary copy gives UnZip a literal input.zip name. This matters for names such as
release[1].zip: shell quoting alone does not disable UnZip’s own archive wildcard matching.
The source name still determines the destination. Paths are resolved once with cd -P and captured
from $PWD, so discovery and extraction use the same physical directory without losing trailing
newlines in directory names.
Each ZIP must pass -t before its destination is created. -o then applies only within that newly
created directory, and -P '' supplies an empty password so there is no interactive password
request. Password-protected packages need a separate workflow. The script keeps processing after a
failure but never resets status to zero. Bash’s
subshell and process rules
keep the saved script’s directory changes and options out of the calling shell.
Act on the result
The batch script returns 0 only when it found at least one eligible archive and every archive
completed without a warning or error. It returns 1 for an existing destination, no eligible input,
or a copy, validation, or extraction failure; 2 means the arguments are wrong. A later success
cannot hide an earlier failure.
Keep UnZip’s diagnostics when investigating a failure. A nonzero UnZip status can mean damaged data, unsupported compression or encryption, or a filesystem problem. Calling all of these “corruption” would send you looking in the wrong place.
A failed data check creates no per-archive destination. A failure during extraction can leave partial files; inspect those before retrying into a new output root. Previously completed outputs and the original ZIPs remain in place. The script removes only its private temporary copy.
For a build job, let the script’s nonzero status stop downstream processing. For a manual rerun, choose a new output root or inspect the existing files first. That decision belongs to your workflow, rather than an unattended overwrite prompt.
