zip 命令详解与参数实测(Ubuntu 22.04)
zip 全参数实测:adding/updating/deleting 回显与 rc=12 语义、-0/-1/-9 级别字节级差异、-e 伪终端加密与 -P 明文风险、-u/-d/-m 归档维护、-s 分卷合并还原、-i/-x/-@/-j 四种文件选择对比、-T/-z/-o/-y 辅助参数、stdin 压缩、中文文件名 EFS 编码及与 gzip 横向对比
zip 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 Info-ZIP zip 的压缩与归档能力——
adding/updating/deleting三类过程回显、-0/-1/-9压缩级别的字节级差异、-e交互式加密(含无终端报错与伪终端方案)与-P明文密码、-u/-d/-m三种归档维护动作与 rc=12 语义、-s分卷压缩与合并还原、-i/-x/-@/-j四种文件选择方式、-T完整性测试、-z归档注释、-o时间戳对齐、-y软链接处理、标准输入压缩、中文文件名编码(EFS 标志)以及与 gzip 的横向对比,全部命令与回显已在 Ubuntu 22.04(Zip 3.0)实测通过。
一、环境信息
本节列出本文所有实测所依赖的环境与测试数据,便于读者按同规格复现。
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- zip 版本:Zip 3.0(July 5th 2008),by Info-ZIP(2.3 节实测)
- 解压配套:UnZip 6.00 of 20 April 2009, by Debian(
unzip -v实测),本文仅用其-l/-z做清单与注释核对;zip 与 unzip 均为系统预装命令,无需额外安装 - 演示目录:
/root/demo/zip/ - 测试数据:
project/目录(src/main.c435 B、src/util.py372 B、docs/readme.md164 B、config.ini96 B、Makefile113 B,内容为短小的源码与配置,复现时任意同量级文本均可,压缩百分比会随内容略有差异)、access.log(2,628,894 字节日志类高重复文本)、rand.bin(2,500,000 字节/dev/urandom随机数据,不可压数据代表) - 演示密码:
q1w2e3r4(仅用于-e/-P加密演示,请勿在生产环境复用) - 进度说明:3.6 节会向
project/config.ini追加一行(96 B → 122 B),此后章节涉及该文件的字节数均以 122 B 为准
二、命令概览
2.1 语法与测试数据准备
zip(Info-ZIP)的基本语法是 zip [-options] [-b path] [-t mmddyyyy] [-n suffixes] [zipfile list] [-xi list],默认动作是 add or replace——把 list 中的文件加入 zipfile,同名条目直接替换。它同时完成「归档」(多文件打包成单一 .zip)与「压缩」(逐条目 deflate),这一点与 gzip 这类单文件压缩器有本质区别。list 可以是文件、目录(需配合 -r 递归),也可以是特殊名 -(压缩标准输入);zipfile 与 list 全部省略时,zip 把标准输入压缩后写到标准输出(3.18 节实测)。
💡 技术拓展:「打包 + 压缩」的工具链不止 zip 一种,常见格式横向对比——
格式/命令 原理与特点 适用场景 zip 归档+压缩一体,逐条目 deflate,支持随机读取单条目与注释 跨平台传文件包(Windows 原生可开) tar 仅打包不压缩,Unix 权限/属主语义保留最完整 系统内归档,外接压缩器组合使用 tar.gz tar 打包 + gzip(deflate)压缩 Linux 下最通用的分发形态 tar.bz2 tar + bzip2(BWT),压缩率更高、更慢 冷数据归档 tar.xz tar + xz(LZMA2),压缩率最高、开销最大 源码发布等追求最小体积的场景 本文主角 zip 是其中唯一「归档与压缩一体 + 逐条目压缩 + Windows 原生可开」的格式;同一数据与 gzip 的实测对比见 4.4 节。
先准备演示数据。project/ 模拟一个典型源码工程(五个文件内容为任意短小的配置与源码,字节数见环境信息),access.log 与 rand.bin 分别代表「高重复文本」与「随机不可压数据」两类典型负载:
rm -rf /root/demo/zip
mkdir -p /root/demo/zip/project/src /root/demo/zip/project/docs
cd /root/demo/zip
seq 1 30000 | awk '{print "2026-10-15 18:32:00 INFO demo worker processed request id=" $1 " status=OK latency=12ms"}' > access.log
head -c 2500000 /dev/urandom > rand.bin
验证两份大文件的清单与大小:
$ ls -lh access.log rand.bin
-rw-r--r-- 1 root root 2.6M Oct 15 18:32 access.log
-rw-r--r-- 1 root root 2.4M Oct 15 18:32 rand.bin
2.2 -h 帮助全量回显
zip -h 输出第一页帮助(实测原样保留,可作为参数速查表;更完整的第二页用 -h2 查看):
Copyright (c) 1990-2008 Info-ZIP - Type 'zip "-L"' for software license.
Zip 3.0 (July 5th 2008). Usage:
zip [-options] [-b path] [-t mmddyyyy] [-n suffixes] [zipfile list] [-xi list]
The default action is to add or replace zipfile entries from list, which
can include the special name - to compress standard input.
If zipfile and list are omitted, zip compresses stdin to stdout.
-f freshen: only changed files -u update: only changed or new files
-d delete entries in zipfile -m move into zipfile (delete OS files)
-r recurse into directories -j junk (don't record) directory names
-0 store only -l convert LF to CR LF (-ll CR LF to LF)
-1 compress faster -9 compress better
-q quiet operation -v verbose operation/print version info
-c add one-line comments -z add zipfile comment
-@ read names from stdin -o make zipfile as old as latest entry
-x exclude the following names -i include only the following names
-F fix zipfile (-FF try harder) -D do not add directory entries
-A adjust self-extracting exe -J junk zipfile prefix (unzipsfx)
-T test zipfile integrity -X eXclude eXtra file attributes
-y store symbolic links as the link instead of the referenced file
-e encrypt -n don't compress these suffixes
-h2 show more help
帮助首行就点明了默认行为:The default action is to add or replace zipfile entries——zip 对已有归档的缺省操作是追加与替换,-u/-f/-d/-m 四个维护类参数都建立在这个语义之上。
2.3 版本确认
确认实测环境的 zip 版本(zip -v 不加管道时还会输出编译选项与目标系统信息,这里截取版本关键行):
$ zip -v | head -3
Copyright (c) 1990-2008 Info-ZIP - Type 'zip "-L"' for software license.
This is Zip 3.0 (July 5th 2008), by Info-ZIP.
Currently maintained by E. Gordon. Please send bug reports to
三、参数逐项实测
3.1 默认压缩:adding 回显与条目追加
不加任何参数时,zip 把命令行里的文件逐个加入归档,每个文件输出一行 adding: 回显。对同一个归档连续执行两次,第二次不会覆盖第一次的结果,而是继续追加:
$ zip out.zip project/config.ini
adding: project/config.ini (deflated 11%)
$ zip out.zip project/Makefile
adding: project/Makefile (deflated 24%)
$ unzip -l out.zip
Archive: out.zip
Length Date Time Name
--------- ---------- ----- ----
96 2026-10-15 18:32 project/config.ini
113 2026-10-15 18:32 project/Makefile
--------- -------
209 2 files
解读:两个文件进入同一个 out.zip,条目名就是命令行书写的相对路径 project/config.ini、project/Makefile——归档内路径与书写路径一致,解压时会按这个路径重建目录结构。(deflated 11%) 表示该条目经 deflate 压缩后缩小了 11%。末行的 209 2 files 是所有条目原始大小与条目数的合计。
💡 要点:条目名 = 命令行书写路径。想控制解压后的目录层级,就在打包时控制书写路径(配合
-j的反向操作见 3.10 节)。
3.2 -r 递归压缩目录
zip 默认不深入目录(目录本身会被跳过),-r(recurse)才会递归打包目录下全部内容,并额外写入目录条目:
$ rm -f out.zip
$ zip -r out.zip project
adding: project/ (stored 0%)
adding: project/src/ (stored 0%)
adding: project/src/util.py (deflated 41%)
adding: project/src/main.c (deflated 42%)
adding: project/config.ini (deflated 11%)
adding: project/Makefile (deflated 24%)
adding: project/docs/ (stored 0%)
adding: project/docs/readme.md (deflated 27%)
$ unzip -l out.zip
Archive: out.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-15 18:32 project/
0 2026-10-15 18:32 project/src/
372 2026-10-15 18:32 project/src/util.py
435 2026-10-15 18:32 project/src/main.c
96 2026-10-15 18:32 project/config.ini
113 2026-10-15 18:32 project/Makefile
0 2026-10-15 18:32 project/docs/
164 2026-10-15 18:32 project/docs/readme.md
--------- -------
1180 8 files
解读:8 个条目 = 5 个文件 + 3 个目录条目。目录条目长度为 0、压缩方式为 stored 0%,它们的作用是让解压端即使遇到空目录也能重建结构。打包顺序是先目录后文件,与目录遍历顺序一致。
💡 要点:目录条目占归档空间极小但有用——不想要它们可以用
-D(do not add directory entries);判断一个 zip「为什么比文件加起来还大」,先看看里面有多少个 stored 的目录条目。
3.3 -0/-1/-9 压缩级别与存储模式
-1 到 -9 调节 deflate 的压缩强度(默认 -6),-0 则完全不压缩、只存储(store)。用 2.6 MB 的日志文本做横向对比:
$ zip -9 big9.zip access.log
adding: access.log (deflated 97%)
$ zip big.zip access.log
adding: access.log (deflated 97%)
$ zip -1 big1.zip access.log
adding: access.log (deflated 97%)
$ zip -0 big0.zip access.log
adding: access.log (stored 0%)
$ ls -l access.log big1.zip big.zip big9.zip big0.zip
-rw-r--r-- 1 root root 2628894 Oct 15 18:32 access.log
-rw-r--r-- 1 root root 84691 Oct 15 18:32 big.zip
-rw-r--r-- 1 root root 2629064 Oct 15 18:32 big0.zip
-rw-r--r-- 1 root root 88530 Oct 15 18:32 big1.zip
-rw-r--r-- 1 root root 82922 Oct 15 18:32 big9.zip
解读:回显里的 (deflated 97%) 三档完全相同——这个百分比是取整后的展示值,真实差异必须看字节数:-9(82,922 B)< 默认 -6(84,691 B)< -1(88,530 B)< -0(2,629,064 B)。-0 的产物比原文件还多 170 字节,这就是 zip 格式本身的头部与中央目录开销。
💡 要点:级别差异在日志类文本上只有 ±5% 左右,但对 CPU 耗时差异明显;批量压缩大量数据时用默认
-6即可,-9只在存储成本敏感时值得。-0的价值在「已压缩数据再打包」(视频、图片、*.gz),避免无意义的二次压缩耗时。
3.4 -e 交互式加密(无终端报错与伪终端方案)
-e 为归档加密码保护。在没有终端的标准输入/输出环境里直接执行会失败——zip 要求从终端读取密码:
$ zip -e secret.zip project/config.ini; echo rc=$?
zip error: Invalid command arguments (stderr is not a tty)
rc=16
$ { sleep 2; echo q1w2e3r4; sleep 2; echo q1w2e3r4; } | timeout 15 script -qec "zip -e secret.zip project/config.ini" /dev/null; echo rc=$?
Enter password:
Verify password:
adding: project/config.ini (deflated 11%)
rc=0
$ unzip -l secret.zip
Archive: secret.zip
Length Date Time Name
--------- ---------- ----- ----
96 2026-10-15 18:32 project/config.ini
--------- -------
96 1 file
解读:无终端时报 zip error: Invalid command arguments (stderr is not a tty),退出码 16;走伪终端后依次出现 Enter password: 与 Verify password: 两个提示,两次输入一致才完成加密。加密不改变条目元数据,unzip -l 仍可正常列出清单(文件名不加密)。
💡 要点:
-e的密码必须来自终端,自动化脚本里要么用script/expect伪造终端(两次喂入之间留足延迟),要么改用-P(3.5 节)。注意 zip 传统加密(PKZIP 对称流密码)强度有限,不能当作强安全边界。
3.5 -P 明文密码(不安全,仅演示)
-P 直接在命令行携带密码,效果与 -e 相同:
$ zip -P q1w2e3r4 pwinfile.zip project/config.ini
adding: project/config.ini (deflated 11%)
$ ls -l secret.zip pwinfile.zip
-rw-r--r-- 1 root root 299 Oct 15 18:32 pwinfile.zip
-rw-r--r-- 1 root root 299 Oct 15 18:32 secret.zip
解读:-P 与 -e 生成的归档大小完全一致(同为 299 字节),说明两者底层用的是同一套加密机制,区别只在密码的输入方式。
💡 要点:
-P的密码会留在 shell 历史文件和进程列表(ps -ef可见)里,只在受控环境使用;需要脚本化加密时优先考虑密钥文件 + 伪终端喂入的方式,并严格控制演示密码的复用范围。
3.6 -u 更新已有条目
-u(update)只处理「归档里没有的」或「比归档内副本更新的」文件。对内容未变化的文件执行 -u,zip 什么都不做并返回退出码 12;追加一行并等 2 秒后重试,命中更新;再喂一个新文件,直接加入:
$ zip -u out.zip project/docs/readme.md; echo rc=$?
rc=12
$ echo "# appended by zip -u test" >> project/config.ini
$ sleep 2
$ zip -u out.zip project/config.ini
updating: project/config.ini (deflated 13%)
$ cp project/docs/readme.md project/extra.md
$ zip -u out.zip project/extra.md
adding: project/extra.md (deflated 27%)
解读:三种回显对应三种情形——无变化时静默返回 rc=12(nothing to do,不是错误但退出码非 0);文件内容变化且 mtime 更新时回显 updating:;归档里不存在的新文件回显 adding:。注意 sleep 2:zip 的更新判定粒度包含 mtime,同一秒内的修改不会被识别为「更新」。此后 out.zip 中的 config.ini 从 96 B 变为 122 B,后续章节的字节数以此为基准。
💡 要点:
rc=12会破坏set -e脚本与&&链,需要显式判断;「改完立刻 zip -u」是常见坑,实测至少隔 2 秒才被识别为 changed。
3.7 -d 从归档删除条目
-d(delete)从已有归档中删除条目,条目名必须写归档内的完整路径:
$ zip -d out.zip project/extra.md
deleting: project/extra.md
$ unzip -l out.zip
Archive: out.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-15 18:32 project/
0 2026-10-15 18:32 project/src/
372 2026-10-15 18:32 project/src/util.py
435 2026-10-15 18:32 project/src/main.c
122 2026-10-15 18:32 project/config.ini
113 2026-10-15 18:32 project/Makefile
0 2026-10-15 18:32 project/docs/
164 2026-10-15 18:32 project/docs/readme.md
--------- -------
1206 8 files
解读:deleting: 确认删除动作;config.ini 显示 122 B,正是 3.6 节更新后的新副本,说明 -u 与 -d 都直接作用于归档内条目。删除支持通配符(4.2 节实战用 "project/src/*" 一次删除 3 个条目),通配符务必加引号防止 shell 先行展开。
💡 要点:
-d只对条目名精确匹配或通配符命中生效,拿不准路径时先unzip -l查清单,操作后再复核一遍——模式没命中时不会删除任何条目,也不会报错。
3.8 -m 压缩后删除原文件
-m(move)在加入归档后删除磁盘上的原文件,相当于「转存」:
$ cp project/config.ini cfg-move.ini
$ zip -m cfgmove.zip cfg-move.ini
adding: cfg-move.ini (deflated 13%)
$ ls -l cfg-move.ini; echo rc=$?
ls: cannot access 'cfg-move.ini': No such file or directory
rc=2
解读:adding: 之后 cfg-move.ini 从磁盘消失(ls 报 No such file or directory),内容已转入 cfgmove.zip。
💡 要点:
-m等价于「压缩 +rm原文件」,原文件一旦删除无法从命令行层面撤销,只能从归档还原;用于日志轮转、临时产物转存时,先确认归档写入成功(退出码 0)再依赖它。
3.9 -q 静默与 -v 详细输出
-q 关闭过程回显(脚本场景降噪),-v 输出更细的压缩统计:
$ zip -q quiet.zip project/config.ini
$ ls -l quiet.zip; echo rc=$?
-rw-r--r-- 1 root root 292 Oct 15 18:32 quiet.zip
rc=0
$ zip -v verbose.zip project/config.ini
adding: project/config.ini (in=122) (out=106) (deflated 13%)
total bytes=122, compressed=106 -> 13% savings
解读:-q 执行后无任何回显但退出码 0、归档正常生成;-v 在 adding 行追加了 (in=122) (out=106) 的字节数统计,并汇总一行 total bytes=122, compressed=106 -> 13% savings。此处的 in=122 也再次印证 3.6 节之后 config.ini 已是 122 B。
💡 要点:
-q是定时任务与流水线的标配;-v适合排查「这个目录压缩后应该多大」类问题,比(deflated 13%)的取整百分比精确到字节。
3.10 -j 撇开目录路径(junk paths)
-j 把文件加入归档时剥掉路径前缀,只保留文件名本身:
$ zip -j junk.zip project/src/main.c project/src/util.py
adding: main.c (deflated 42%)
adding: util.py (deflated 41%)
$ unzip -l junk.zip
Archive: junk.zip
Length Date Time Name
--------- ---------- ----- ----
435 2026-10-15 18:32 main.c
372 2026-10-15 18:32 util.py
--------- -------
807 2 files
解读:条目名从 project/src/main.c 变成了 main.c,归档内没有任何目录层级,解压后所有文件平铺在目标目录。
💡 要点:
-j适合「一堆文件丢给同事」的临时包;但它会把不同目录下的同名文件压成同名条目互相覆盖,目录结构有意义时严禁使用。
3.11 -x 排除文件
-x(exclude)在打包时排除匹配模式的文件,通常与 -r 组合、写在命令行末尾:
$ rm -f out.zip
$ zip -r out.zip project -x "project/docs/*"
adding: project/ (stored 0%)
adding: project/src/ (stored 0%)
adding: project/src/util.py (deflated 41%)
adding: project/src/main.c (deflated 42%)
adding: project/config.ini (deflated 13%)
adding: project/Makefile (deflated 24%)
$ unzip -l out.zip
Archive: out.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-15 18:32 project/
0 2026-10-15 18:32 project/src/
372 2026-10-15 18:32 project/src/util.py
435 2026-10-15 18:32 project/src/main.c
122 2026-10-15 18:32 project/config.ini
113 2026-10-15 18:32 project/Makefile
--------- -------
1042 6 files
解读:project/docs/* 命中的不只是 docs 下的文件——project/docs/ 这个目录条目也被一并排除(清单里已无 docs 条目),剩下 4 文件 + 2 目录共 6 个条目。config.ini 显示 deflated 13% 与 122 B,与 3.6 节后的状态一致。
💡 要点:
-x的模式按归档内路径匹配,所以要以「打包时书写的根路径」开头;模式加引号防 shell 展开;-x与-i(3.12 节)是黑名单与白名单的关系。
3.12 -i 仅包含指定模式
-i(include)反向限定只打包匹配模式的文件,同样配合 -r 使用:
$ zip -r inc.zip project -i "project/src/*"
adding: project/src/ (stored 0%)
adding: project/src/util.py (deflated 41%)
adding: project/src/main.c (deflated 42%)
$ unzip -l inc.zip
Archive: inc.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-15 18:57 project/src/
372 2026-10-15 18:57 project/src/util.py
435 2026-10-15 18:57 project/src/main.c
--------- -------
807 3 files
解读:只有 src 下的内容进了归档(2 文件 + 1 目录条目),config.ini、Makefile、docs/ 全部被白名单挡在外面。
💡 要点:
-i与-x可以叠加,构成「先白名单再剔除」的两级过滤;相比-x黑名单,-i白名单在「只要某类文件」的场景下更不容易漏排除新增的无关文件。
3.13 -@ 从标准输入读取文件清单
-@ 让 zip 从标准输入逐行读取要打包的文件名,可与 find 等命令直接串联:
$ find project -type f | zip -@ sel.zip
adding: project/src/util.py (deflated 41%)
adding: project/src/main.c (deflated 42%)
adding: project/config.ini (deflated 13%)
adding: project/Makefile (deflated 24%)
adding: project/docs/readme.md (deflated 27%)
$ unzip -l sel.zip
Archive: sel.zip
Length Date Time Name
--------- ---------- ----- ----
372 2026-10-15 18:32 project/src/util.py
435 2026-10-15 18:32 project/src/main.c
122 2026-10-15 18:32 project/config.ini
113 2026-10-15 18:32 project/Makefile
164 2026-10-15 18:32 project/docs/readme.md
--------- -------
1206 5 files
解读:find project -type f 的 5 行输出逐个成为归档条目,条目路径与 find 输出一致;与 -r 最大的不同是没有目录条目(清单里没有任何 stored 0% 的目录行)。
💡 要点:
-@是「精确控制打包内容」的最强方式——任何能用命令生成文件名列表的场景(find -mtime -1、git ls-files、grep -l)都能接进来;缺点是丢失目录条目,空目录结构不会随包保留。
3.14 -T 完整性测试
-T 对归档做完整性测试,是打包后自检的快捷方式:
$ zip -T inc.zip; echo rc=$?
test of inc.zip OK
rc=0
$ zip -T nosuch.zip; echo rc=$?
zip error: Nothing to do! (nosuch.zip)
rc=12
解读:测试通过输出 test of inc.zip OK、退出码 0;归档不存在时报 Nothing to do!、退出码 12。-T 的内部动作等价于逐条目校验数据可读可解。
💡 要点:重要归档建议「打包后立刻
zip -T」;注意rc=12同时被-u无变化(3.6 节)和-T目标不存在复用,脚本判断时不要只写rc != 0,要区分 12 的两种来源。
3.15 -z 添加归档注释
-z 为整个归档添加一段注释。注释从标准输入读取,以单独一行 . 结束:
$ printf "demo project nightly backup\n.\n" | zip -z inc.zip; echo rc=$?
enter new zip file comment (end with .):
rc=0
$ unzip -z inc.zip; echo rc=$?
Archive: inc.zip
demo project nightly backup
rc=0
解读:交互提示符为 enter new zip file comment (end with .):,管道输入时提示符仍会打印;unzip -z 只显示注释本身。注释随归档持久存储,解压端的常规解压操作也会把注释显示出来。
💡 要点:归档注释适合写入备份时间、数据版本、生成命令等审计信息,
unzip -z一条命令即可读取,比打开归档翻清单高效得多。
3.16 -o 归档时间戳对齐最新条目
-o 把归档文件自身的修改时间设为「归档内最新条目的时间」。先把演示文件的时间戳统一改到 09:00,再重建归档观察:
$ find project -exec touch -d "2026-10-15 09:00:00" {} +
$ zip -r -q inc.zip project -i "project/src/*"
$ ls -l inc.zip
-rw-r--r-- 1 root root 1004 Oct 15 18:57 inc.zip
$ zip -o inc.zip project/src/main.c
updating: project/src/main.c (deflated 42%)
$ ls -l inc.zip
-rw-r--r-- 1 root root 1004 Oct 15 09:00 inc.zip
解读:第一次打包后归档时间是执行时刻(Oct 15 18:57);zip -o inc.zip project/src/main.c 重新写入该条目后,归档自身的时间被拉回到条目时间 Oct 15 09:00,字节数保持 1004 不变。
💡 要点:分发「内容固定版本」的归档时,
-o能让文件管理器里显示的时间与内容一致,便于对版本;-o需要至少一个文件参数来触发重新写入,单纯zip -o inc.zip不会改动时间。
3.17 -y 软链接的两种存储方式
默认情况下 zip 跟随软链接,把链接指向的目标文件内容存入归档;-y 则把链接本身作为条目存储:
$ ln -s project/src/util.py link-util.py
$ zip nofollow.zip link-util.py
adding: link-util.py (deflated 41%)
$ unzip -l nofollow.zip
Archive: nofollow.zip
Length Date Time Name
--------- ---------- ----- ----
372 2026-10-15 09:00 link-util.py
--------- -------
372 1 file
$ zip -y linky.zip link-util.py
adding: link-util.py (stored 0%)
$ unzip -l linky.zip
Archive: linky.zip
Length Date Time Name
--------- ---------- ----- ----
19 2026-10-15 18:57 link-util.py
--------- -------
19 1 file
解读:不带 -y 的归档里条目长 372 B(目标文件大小,deflated 41%)、条目时间是目标文件的 09:00;-y 的归档条目只有 19 B、stored 0%——19 恰好是目标路径字符串 project/src/util.py 的长度,条目时间是链接自身的创建时间 18:57。
💡 要点:打包含软链接的目录树时务必想清楚:默认行为会把链接「实体化」(目标内容多份冗余,链接关系丢失);
-y保留链接关系,但要求解压端也理解链接条目。
3.18 标准输入压缩(zipfile 与列表均省略)
帮助原文写明 If zipfile and list are omitted, zip compresses stdin to stdout——把命令输出通过管道交给 zip、再用重定向落盘,就得到一个流式压缩的归档:
$ cat project/config.ini | zip > cfg-stdin.zip; echo rc=$?
adding: - (deflated 11%)
rc=0
$ unzip -l cfg-stdin.zip
Archive: cfg-stdin.zip
Length Date Time Name
--------- ---------- ----- ----
96 2026-10-15 18:57 -
--------- -------
96 1 file
解读:条目名固定为 -,内容来自标准输入;zip 输出的压缩流写入 cfg-stdin.zip。
💡 要点:这种形态适合「内存/管道中转」场景(如把命令输出压成 zip 直接送入远端存储),代价是条目名丢失——解压端需要自行重命名;一般场景还是用「归档名 + 文件列表」的标准形态。
3.19 中文文件名与编码(EFS 标志)
实测环境的 locale 为 C(LANG 为空)。分别用 UTF-8 locale 与 C locale 打包同一个中文文件名,观察归档的编码行为:
$ locale | head -2
LANG=
LANGUAGE=
$ echo "这是一个用于测试 zip 中文文件名的说明文件。" > 中文说明.txt
$ LC_ALL=C.UTF-8 zip chs-utf8.zip 中文说明.txt
adding: 中文说明.txt (stored 0%)
$ LC_ALL=C.UTF-8 unzip -l chs-utf8.zip
Archive: chs-utf8.zip
Length Date Time Name
--------- ---------- ----- ----
63 2026-10-15 18:32 中文说明.txt
--------- -------
63 1 file
$ LC_ALL=C zip chs-c.zip 中文说明.txt
adding: 中文说明.txt (stored 0%)
$ LC_ALL=C.UTF-8 unzip -l chs-c.zip; echo rc=$?
Archive: chs-c.zip
Length Date Time Name
--------- ---------- ----- ----
63 2026-10-15 18:32 中文说明.txt
--------- -------
63 1 file
rc=0
解读:LC_ALL=C.UTF-8 下 zip 会给非 ASCII 文件名的条目置上 EFS 标志(声明「此文件名是 UTF-8」);LC_ALL=C 下 zip 不写 EFS 标志,文件名按原始字节存储。本环境的 UnZip 6.00 对无 EFS 标志的条目按原始字节透传显示:显式 unzip -O UTF-8 -l 的输出与上一条完全一致,解包到子目录后 ls 也能看到完整的中文文件名,两种归档全程 rc=0。
💡 要点:中文文件名乱码的根源是「打包端没写 EFS 标志 + 解压端按本地代码页(如 cp437)强行解码」的组合;在 Ubuntu 到 Ubuntu 的链路上本实测未出现乱码,跨 Windows 等按代码页解释文件名的解压器时风险最高。打包端保持 UTF-8 locale,解压端用
-O显式指定编码,是最稳的组合。
四、实战示例
4.1 大文件分卷压缩、合并与还原
-s 按指定大小切分归档,适合绕开单文件大小限制的传输通道。切分产物是 .z01、.z02 等前置卷加一个 .zip 末卷(末卷存放中央目录):
$ zip -s 1m split.zip rand.bin
adding: rand.bin (deflated 0%)
$ ls -l split.zip split.z01 split.z02
-rw-r--r-- 1 root root 1048576 Oct 15 18:32 split.z01
-rw-r--r-- 1 root root 1048576 Oct 15 18:32 split.z02
-rw-r--r-- 1 root root 403403 Oct 15 18:32 split.zip
$ zip -s 0 split.zip --out unsplit.zip
copying: rand.bin
$ ls -l unsplit.zip rand.bin
-rw-r--r-- 1 root root 2500000 Oct 15 18:32 rand.bin
-rw-r--r-- 1 root root 2097248 Oct 15 18:32 unsplit.zip
$ unzip -l split.zip; echo rc=$?
Archive: split.zip
warning [split.zip]: zipfile claims to be last disk of a multi-part archive;
attempting to process anyway, assuming all parts have been concatenated
together in order. Expect "errors" and warnings...true multi-part support
doesn't exist yet (coming soon).
Length Date Time Name
--------- ---------- ----- ----
2500000 2026-10-15 18:32 rand.bin
--------- -------
2500000 1 file
rc=1
解读:rand.bin 是随机数据压不动,回显 deflated 0%;2.5 MB 被切成两个 1 MiB 前置卷加一个 403,403 B 的末卷。还原端先传输全部分卷,再用 zip -s 0 split.zip --out unsplit.zip 把分卷合并为普通单卷归档,回显 copying: rand.bin,产物 2,097,248 B(原文件压缩后大小加头部开销)。若跳过合并直接对末卷 unzip -l,unzip 会打出 multi-part 警告并按「分卷已按序拼接」的假设尽力处理,退出码 1。
💡 要点:分卷单位支持 k/m/g;解压前必须先合并(
zip -s 0 分卷 --out 单卷),unzip 目前对多卷归档只是「尽力而为」;所有分卷必须齐全且按序,缺卷时合并产物会损坏。
4.2 每日备份工作流:打包 → 增量 → 校验 → 瘦身
把 3.2/3.6/3.7/3.14 节的能力组合成一条可脚本化的备份流水线(承 3.16 节,演示文件时间戳已统一为 09:00):
$ rm -f backup.zip
$ zip -r -q backup.zip project -x "project/docs/*"
$ echo "# daily change" >> project/config.ini
$ sleep 2
$ zip -u backup.zip project/config.ini
updating: project/config.ini (deflated 14%)
$ zip -T backup.zip; echo rc=$?
test of backup.zip OK
rc=0
$ zip -d backup.zip "project/src/*"; echo rc=$?
deleting: project/src/
deleting: project/src/util.py
deleting: project/src/main.c
rc=0
$ unzip -l backup.zip
Archive: backup.zip
Length Date Time Name
--------- ---------- ----- ----
0 2026-10-15 09:00 project/
111 2026-10-15 18:57 project/config.ini
113 2026-10-15 09:00 project/Makefile
--------- -------
224 3 files
解读:第一步 -r -q 静默全量打包(排除 docs);变更发生后 -u 只重写变化条目(updating:,96 B → 111 B);-T 校验归档完好;-d 用带引号的通配符 "project/src/*" 一次删除 3 个条目(含 project/src/ 目录条目本身),实现「瘦身」。
💡 要点:
-u前的sleep 2不能省(同秒修改不触发更新,见 3.6 节);-d的通配符匹配会连同目录条目一起删除,解压端就不会再重建空的src/目录。
4.3 四种文件选择方式的对比
-r、-r -x、-r -i、-@、-j 都能改变归档的条目构成,选型取决于「要结构还是要内容」。汇总本文实测数据:
| 方式 | 实测命令 | 条目构成 | 目录条目 | 适用场景 |
|---|---|---|---|---|
| 全量递归(3.2 节) | zip -r out.zip project | 8 条 = 5 文件 + 3 目录 | 有 | 目录结构需完整保留 |
| 排除模式(3.11 节) | zip -r out.zip project -x "project/docs/*" | 6 条 = 4 文件 + 2 目录 | 有 | 黑名单式剔除 |
| 白名单模式(3.12 节) | zip -r inc.zip project -i "project/src/*" | 3 条 = 2 文件 + 1 目录 | 有 | 只要某一类文件 |
| 标准输入清单(3.13 节) | find project -type f | zip -@ sel.zip | 5 条纯文件 | 无 | 精确控制内容 |
| 平铺模式(3.10 节) | zip -j junk.zip project/src/main.c project/src/util.py | 2 条无路径 | 无 | 临时传文件给他人 |
解读:目录条目的有无直接影响解压端的目录重建:-r 系列会重建空目录,-@ 与 -j 不会;-j 还会抹平层级,同名文件会互相覆盖。
💡 要点:需要「结构 + 精确内容」时,用
-r -x/-r -i;需要「内容 + 动态条件」时,用find/git ls-files接-@;两者都不需要目录条目时再考虑-j。
4.4 zip 与 gzip 的横向对比
单文件场景下 zip 与 gzip 各有开销结构,用 122 B 的 config.ini 实测(承 3.6 节后的文件状态):
$ gzip -k project/config.ini
$ ls -l project/config.ini project/config.ini.gz
-rw-r--r-- 1 root root 122 Oct 15 18:32 project/config.ini
-rw-r--r-- 1 root root 135 Oct 15 18:32 project/config.ini.gz
$ zip cfg-only.zip project/config.ini
adding: project/config.ini (deflated 13%)
$ ls -l cfg-only.zip project/config.ini.gz
-rw-r--r-- 1 root root 292 Oct 15 18:32 cfg-only.zip
-rw-r--r-- 1 root root 135 Oct 15 18:32 project/config.ini.gz
$ unzip -l cfg-only.zip
Archive: cfg-only.zip
Length Date Time Name
--------- ---------- ----- ----
122 2026-10-15 18:32 project/config.ini
--------- -------
122 1 file
$ rm -f project/config.ini.gz
解读:同为 deflate 算法,gzip 产物 135 B、zip 产物 292 B——小文件上 zip 的每条目本地头 + 中央目录开销(约 157 B)占比极高。负载越大差距被稀释:access.log 上 zip 默认级别为 84,691 B(3.3 节),gzip 同级数据也是同一量级。
💡 要点:单文件流式压缩选 gzip(开销最小、管道生态成熟);多文件归档选 zip(归档能力 + 随机解压单条目 + 加密/注释/分卷等格式能力),两者的取舍是「格式功能 vs 每文件固定开销」。
五、注意事项与常见问题
Q1:脚本里执行 zip -e 报 "zip error: Invalid command arguments (stderr is not a tty)" 怎么办?
-e 必须从终端读密码(3.4 节,rc=16)。伪终端方案:{ sleep 2; echo 密码; sleep 2; echo 密码; } | timeout 15 script -qec "zip -e 目标.zip 文件" /dev/null,两次输入间留足延迟。纯自动化场景改用 -P,但要接受密码进入 shell 历史与进程列表的代价。
Q2:zip -u 返回退出码 12 是失败了吗?
不是。rc=12 表示 nothing to do——没有文件需要加入或更新(3.6 节实测),归档本身完好。但它会让 set -e 脚本与 && 链中断,需要显式处理。另外 zip 的更新判定含 mtime 比对,同秒内的修改不会触发 updating:,实测间隔 2 秒以上才可靠。
Q3:分卷 zip 怎么解压?直接 unzip 报 warning 正常吗?
正常且预期内。unzip 目前对多卷归档支持不完整,直接对末卷操作会提示 multi-part 警告并以 rc=1 继续(4.1 节)。标准流程是先传输全部分卷(.z01、.z02、.zip),再 zip -s 0 split.zip --out unsplit.zip 合并成单卷后解压。
Q4:中文文件名在归档里会乱码吗?
取决于打包端是否写入 EFS(UTF-8)标志与解压端的解码策略。3.19 节实测:UTF-8 locale 打包写 EFS 标志;C locale 打包不写但 Ubuntu 的 unzip 按字节透传仍显示正常。乱码高发于「C locale 打包 + 按 cp437 等代码页解码的解压器」组合,跨平台分发时打包端保持 UTF-8 locale,解压端用 -O 显式指定字符集。
Q5:软链接打包后到底存了什么?
默认存目标文件的内容(条目大小等于目标大小、条目时间是目标文件的),-y 才存链接本身(条目大小等于目标路径字符串长度,3.17 节实测 19 B)。含软链接的目录树打包前先想清楚要哪一种;默认行为会把同一个目标的多个链接实体化成多份冗余内容。
六、参考链接
- Info-ZIP 官方站点(zip/unzip 项目主页):http://www.info-zip.org/
- zip(1) 手册页(man7.org):https://man7.org/linux/man-pages/man1/zip.1.html
- PKWARE APPNOTE:.ZIP 文件格式规范(EFS 标志、分卷与加密机制的权威定义):https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
- unzip(1) 手册页(man7.org,解压端行为参考):https://man7.org/linux/man-pages/man1/unzip.1.html
- Ubuntu jammy zip 软件包页:https://packages.ubuntu.com/jammy/zip