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 横向对比

Linux命令Linux系统UbuntuUbuntu 22.04命令行压缩归档2026/10/16

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.c 435 B、src/util.py 372 B、docs/readme.md 164 B、config.ini 96 B、Makefile 113 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.gztar 打包 + gzip(deflate)压缩Linux 下最通用的分发形态
tar.bz2tar + bzip2(BWT),压缩率更高、更慢冷数据归档
tar.xztar + 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 project8 条 = 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.zip5 条纯文件无精确控制内容
平铺模式(3.10 节)zip -j junk.zip project/src/main.c project/src/util.py2 条无路径无临时传文件给他人

解读:目录条目的有无直接影响解压端的目录重建:-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)。含软链接的目录树打包前先想清楚要哪一种;默认行为会把同一个目标的多个链接实体化成多份冗余内容。

六、参考链接