Archives and compression
zip
Package and compress files into a .zip archive, the format every operating system opens natively.
zip [OPTION...] ARCHIVE[.zip] [FILE...]
zip archive.zip files... creates (or adds to) a zip archive. Unlike a .tar.gz, a zip
compresses each file separately and keeps a central index, so a single member can be listed or
extracted without reading the rest, and Windows and macOS open it with a double-click. That
makes zip the format for sending files to people; for backups and Unix-to-Unix transfers,
tar keeps more metadata (owners, symlinks, special files).
Two habits matter. First, zip does not descend into directories unless you pass -r: zip
out.zip dir stores just the empty directory entry. Second, an existing archive is updated,
not replaced: zip adds new names and replaces matching ones, but never removes members on its
own. Delete the old zip first if you want a fresh one, or use -FS to sync it with the disk.
Paths are stored as given, so run zip from the directory you want the archive to be relative
to. Extract with unzip.
Examples
Zip a few files
zip notes.zip notes.txt todo.txt
adding: notes.txt (deflated 23%) adding: todo.txt (deflated 13%)
adding: lines show each file and how much deflate compression saved. The .zip suffix is
added automatically if you leave it off.
Zip a directory with -r
zip -r src.zip src
adding: src/ (stored 0%) adding: src/Makefile (deflated 14%) adding: src/app.py (deflated 35%) adding: src/util.sh (deflated 14%) adding: src/main.c (deflated 5%) adding: src/lib/ (stored 0%) adding: src/lib/helpers.py (deflated 7%)
-r recurses. Directories are stored as empty entries (stored 0%) so they are recreated
on extraction.
The empty-directory surprise
zip src.zip src
unzip -l src.zip
adding: src/ (stored 0%)
Archive: src.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-26 01:56 src/
--------- -------
0 1 file Without -r, zip adds only the directory entry itself. The archive contains one empty
directory and nothing else.
Leave files out with -x
zip -rq data.zip data -x '*.json' '*.conf'
unzip -l data.zip
Archive: data.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-25 01:33 data/
65 2026-10-25 17:38 data/hosts.txt
27 2026-10-26 09:05 data/numbers.txt
20 2026-10-26 18:06 data/windows.txt
59 2026-10-26 08:21 data/inventory.tsv
78 2026-10-26 20:12 data/poem.txt
405 2026-10-26 08:37 data/sales.csv
436 2026-10-27 08:26 data/users.csv
66 2026-10-25 17:04 data/fruits.txt
--------- -------
1156 9 files -q hides the adding: lines. -x takes one or more patterns, matched against the stored
paths; quote them so the shell does not expand them first. Both JSON files and the
.conf file were skipped.
Store files without their directories
zip -j flat.zip src/lib/helpers.py src/main.c
unzip -l flat.zip
adding: helpers.py (deflated 7%)
adding: main.c (deflated 5%)
Archive: flat.zip
Length Date Time Name
--------- ---------- ----- ----
88 2026-10-28 07:32 helpers.py
83 2026-10-28 15:30 main.c
--------- -------
171 2 files -j ("junk paths") stores only the base names, so the archive is flat. Two files with the
same name in different directories would collide.
Add a file to an existing archive
Setup run before this example
zip -q notes.zip notes.txtzip notes.zip todo.txt
unzip -l notes.zip
adding: todo.txt (deflated 13%)
Archive: notes.zip
Length Date Time Name
--------- ---------- ----- ----
190 2026-10-25 05:30 notes.txt
62 2026-10-24 21:28 todo.txt
--------- -------
252 2 files Zipping into an existing archive adds to it; notes.txt from before is still there.
Refresh only changed files with -u
Setup run before this example
zip -qr src.zip src
sleep 2; echo '/* v2 */' >> src/main.czip -ru src.zip src
updating: src/main.c (deflated 4%)
-u replaces members whose file on disk is newer than the copy in the archive and adds
new files. Only main.c changed, so only it is updating:.
Remove a member from an archive
Setup run before this example
zip -q notes.zip notes.txt todo.txtzip -d notes.zip todo.txt
unzip -l notes.zip
deleting: todo.txt
Archive: notes.zip
Length Date Time Name
--------- ---------- ----- ----
190 2026-10-25 05:30 notes.txt
--------- -------
190 1 file -d deletes the named members from the archive in place; the files on disk are not
touched.
Zip a list of files from find
find . -name '*.py' | zip -q python.zip -@
unzip -l python.zip
Archive: python.zip
Length Date Time Name
--------- ---------- ----- ----
88 2026-10-28 07:32 src/lib/helpers.py
363 2026-10-28 09:43 src/app.py
--------- -------
451 2 files -@ reads the file names to add from standard input, one per line, so find can choose
them. zip drops the leading ./ from each name.
Test an archive
Setup run before this example
zip -qr src.zip srczip -T src.zip
test of src.zip OK
-T runs a full integrity test of the archive (zip calls unzip -t for this) and reports
OK or the damaged members.
A name that does not exist
zip out.zip nope.txt
zip warning: name not matched: nope.txt zip error: Nothing to do! (out.zip)
zip warns about each unmatched name. When nothing at all could be added it stops with
Nothing to do! and exit status 12, and no archive is created.
Common pitfalls
zip out.zip dirwithout-rstores an empty directory. Always usezip -rfor directories.Re-running
zip -r out.zip dirafter deleting files fromdirleaves the deleted files in the archive. Removeout.zipfirst, or usezip -r -FS out.zip dirto make the archive match the disk.zip does not keep Unix owners, and symlinks are followed (their targets stored) unless you add
-y. Usetarwhen that metadata matters.zip -eencryption is the legacy ZipCrypto scheme, which is weak. For real protection, encrypt with a dedicated tool.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
Linux distributions and macOS both ship Info-ZIP
zip3.0, so options behave the same. macOS Finder's "Compress" adds a__MACOSX/folder with resource forks; command-line zip does not.
Options worth knowing
A selection, described in our own words. For the full list, see zip(1) or zip --help.
| Option | What it does |
|---|---|
-r, --recurse-paths | Recurse into directories. |
-q, --quiet | Do not print the adding: lines. |
-v, --verbose | Print more detail. |
-j, --junk-paths | Store only file names, without directory paths. |
-x, --exclude PATTERN | Leave out files matching the patterns that follow. |
-i, --include PATTERN | Add only files matching the patterns that follow. |
-u, --update | Replace members only if the file on disk is newer; add new files. |
-f, --freshen | Replace members that are newer on disk; do not add new files. |
-FS, --filesync | Make the archive match the disk, deleting members whose files are gone. |
-d, --delete | Delete the named members from the archive. |
-m, --move | Delete the original files after adding them. |
-@, --names-stdin | Read the names of files to add from standard input. |
-T, --test | Test the new archive's integrity, and undo the change if the test fails. |
-e, --encrypt | Encrypt with a password (prompted; weak legacy encryption). |
-y, --symlinks | Store symbolic links as links instead of the files they point to. |
-0 | Store without compression. |
-9 | Compress as much as possible (default level is 6). |
-sf, --show-files | List the files that would be operated on, then exit. |