unzip 命令详解与参数实测(Ubuntu 22.04)

unzip 全参数实测:creating/inflating 回显、-d/-l/-v/-t 基础参数、覆盖三态(交互 y/N/A 与 -o/-n)、-P 密码与 rc=82、单成员与通配符、-x/-j/-p/-z/-f/-u、损坏归档 rc=9 与 rc=2 排障、-O 字符集与中文文件名透传实测、man DIAGNOSTICS 返回码体系与加密解压工作流

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

unzip 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 Info-ZIP unzip 的解压与排障能力——creating/inflating 回显、-d/-l/-v/-t 基础参数、覆盖行为三态(交互 y/N/A 与 -o/-n)、-P 明文密码与交互密码输入(rc=82)、单成员与通配符解压、-x/-j 选择与平铺、-p/-z/-f/-u 进阶参数、损坏归档的两种形态(rc=9 截断与 rc=2 extra-field 损坏)、-O 字符集与中文文件名、以及 man 手册 DIAGNOSTICS 节的返回码体系,全部命令与回显已在 Ubuntu 22.04(UnZip 6.00)实测通过。

一、环境信息

本节列出本文所有实测所依赖的环境与测试数据,便于读者按同规格复现。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • unzip 版本:UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.(2.3 节实测)
  • 配套打包:Zip 3.0(July 5th 2008),by Info-ZIP(用于准备测试归档)
  • unzip 为系统预装命令,无需额外安装;本环境无 man 命令,但手册页文件 /usr/share/man/man1/unzip.1.gz(18,130 字节)存在,可用 zcat 查看(3.16 节实测)
  • 演示目录:/root/demo/unzip/
  • 测试归档:out.zip(2,064 字节,2026-10-15 18:37 打包,含 project/ 下 8 个条目:Makefile 113 B、config.ini 96 B、docs/readme.md 166 B、src/main.c 437 B、src/util.py 372 B,外加 3 个目录条目)
  • 演示密码:q1w2e3r4(仅用于加密归档解压演示,请勿在生产环境复用)

二、命令概览

2.1 语法与测试数据准备

unzip(Info-ZIP)的基本语法是 unzip [-Z] [-opts[modifiers]] file[.zip] [list] [-x xlist] [-d exdir],默认动作是把 list 中(除 xlist 排除的)文件解到 exdir 目录;file[.zip] 可以带通配符一次匹配多个归档;-Z 切换到 ZipInfo 模式(仅查看归档信息不解压)。与 zip 相对,unzip 只负责解包:归档怎么打进去的(条目路径、压缩方式、时间戳),它就按原样还原。

💡 技术拓展:解压命令按归档格式各管一摊,选错命令是新手最常见的报错来源——

命令管的格式典型用法
unzip.zipunzip out.zip -d dir
tar -xf.tar/.tar.gz/.tar.bz2/.tar.xztar -xf pkg.tar.xz
gunzip(gzip -d).gz(单文件)gunzip log.txt.gz
xz -d / bzip2 -d.xz / .bz2(单文件)xz -d data.xz
7zzip/7z 等多格式7z x pkg.7z(需安装 p7zip)

本文主角 unzip 只认 zip 家族格式;Linux 下最常见的 .tar.* 归档用 tar(另篇详解),两类命令不可混用。

准备测试归档(打包端的 adding: 过程一并保留,便于两端对照):

$ 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 28%)

$ unzip -l out.zip
Archive:  out.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-15 18:37   project/
        0  2026-10-15 18:37   project/src/
      372  2026-10-15 18:37   project/src/util.py
      437  2026-10-15 18:37   project/src/main.c
       96  2026-10-15 18:37   project/config.ini
      113  2026-10-15 18:37   project/Makefile
        0  2026-10-15 18:37   project/docs/
      166  2026-10-15 18:37   project/docs/readme.md
---------                     -------
     1184                     8 files

2.2 -h 帮助全量回显

unzip -h 输出参数速查(实测原样保留;更详细的内容用 -hh 或手册页):

UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.

Usage: unzip [-Z] [-opts[modifiers]] file[.zip] [list] [-x xlist] [-d exdir]
  Default action is to extract files in list, except those in xlist, to exdir;
  file[.zip] may be a wildcard.  -Z => ZipInfo mode ("unzip -Z" for usage).

  -p  extract files to pipe, no messages     -l  list files (short format)
  -f  freshen existing files, create none    -t  test compressed archive data
  -u  update files, create if necessary      -z  display archive comment only
  -v  list verbosely/show version info       -T  timestamp archive to latest
  -x  exclude files that follow (in xlist)   -d  extract files into exdir
modifiers:
  -n  never overwrite existing files         -q  quiet mode (-qq => quieter)
  -o  overwrite files WITHOUT prompting      -a  auto-convert any text files
  -j  junk paths (do not make directories)   -aa treat ALL files as text
  -U  use escapes for all non-ASCII Unicode  -UU ignore any Unicode fields
  -C  match filenames case-insensitively     -L  make (some) names lowercase
  -X  restore UID/GID info                   -V  retain VMS version numbers
  -K  keep setuid/setgid/tacky permissions   -M  pipe through "more" pager
  -O CHARSET  specify a character encoding for DOS, Windows and OS/2 archives
  -I CHARSET  specify a character encoding for UNIX and other archives

See "unzip -hh" or unzip.txt for more help.  Examples:
  unzip data1 -x joe   => extract all files except joe from zipfile data1.zip
  unzip -p foo | more  => send contents of foo.zip via pipe into program more
  unzip -fo foo ReadMe => quietly replace existing ReadMe if archive file newer

帮助末尾的示例已经点出两个惯用法:-x 排除成员、-fo 「归档副本较新时安静替换」——后者在 3.13 节有对应的实测解读。

2.3 版本确认

确认实测环境的 unzip 版本:

$ unzip -v | head -2
UnZip 6.00 of 20 April 2009, by Debian. Original by Info-ZIP.

三、参数逐项实测

3.1 基本解压与 creating/inflating 回显

进入一个空目录直接 unzip 归档名,zip 里的目录条目对应 creating: 行、文件条目对应 inflating: 行,按打包时的顺序逐条还原:

$ rm -rf ex-basic && mkdir ex-basic

$ cd ex-basic

$ unzip ../out.zip
Archive:  ../out.zip
   creating: project/
   creating: project/src/
  inflating: project/src/util.py     
  inflating: project/src/main.c      
  inflating: project/config.ini      
  inflating: project/Makefile        
   creating: project/docs/
  inflating: project/docs/readme.md  

$ find . | sort
.
./project
./project/Makefile
./project/config.ini
./project/docs
./project/docs/readme.md
./project/src
./project/src/main.c
./project/src/util.py

$ cd /root/demo/unzip

解读:目录层级完整重建,5 个文件 + 3 个目录与 2.1 节清单一致;inflating: 行尾的对齐空格是 unzip 回显原样保留。

💡 要点:默认解压到当前目录——归档若没有统一的根目录条目,文件会「撒一地」,解包前先 unzip -l 看一眼结构,或始终用 -d 指定目标目录(3.2 节)。

3.2 -d 指定解压目标目录

-d exdir 把内容解到指定目录(不存在则自动创建),避免污染当前目录:

$ unzip out.zip -d ex-target
Archive:  out.zip
   creating: ex-target/project/
   creating: ex-target/project/src/
  inflating: ex-target/project/src/util.py  
  inflating: ex-target/project/src/main.c  
  inflating: ex-target/project/config.ini  
  inflating: ex-target/project/Makefile  
   creating: ex-target/project/docs/
  inflating: ex-target/project/docs/readme.md  

$ find ex-target -type f | sort
ex-target/project/Makefile
ex-target/project/config.ini
ex-target/project/docs/readme.md
ex-target/project/src/main.c
ex-target/project/src/util.py

解读:回显中的路径带上了 ex-target/ 前缀,说明 -d 相当于「先切到该目录再解压」;目标目录及子目录按需自动 creating。

💡 要点:-d 是脚本化解压的标配参数——目标目录可预测,配合 -o/-n 可精确控制覆盖行为(3.6 节)。

3.3 -l 短格式列表

-l 只列清单不解压,四列分别为原始长度、日期、时间、条目名,末行汇总:

$ unzip -l out.zip
Archive:  out.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-10-15 18:37   project/
        0  2026-10-15 18:37   project/src/
      372  2026-10-15 18:37   project/src/util.py
      437  2026-10-15 18:37   project/src/main.c
       96  2026-10-15 18:37   project/config.ini
      113  2026-10-15 18:37   project/Makefile
        0  2026-10-15 18:37   project/docs/
      166  2026-10-15 18:37   project/docs/readme.md
---------                     -------
     1184                     8 files

解读:Length 是解压后的原始大小(目录条目为 0),汇总行 1184 8 files 是全部条目原始大小与条目数合计。

💡 要点:-l 是解包前的「安全检查」——先看根路径结构(有没有统一前缀)、再看覆盖风险(哪些文件会落进当前目录)。

3.4 -v 详细列表(含压缩方式与 CRC-32)

-v 在 -l 基础上增加 Method(压缩方式)、Size(压缩后大小)、Cmpr(压缩率)与 CRC-32 校验值:

$ unzip -v out.zip
Archive:  out.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2026-10-15 18:37 00000000  project/
       0  Stored        0   0% 2026-10-15 18:37 00000000  project/src/
     372  Defl:N      220  41% 2026-10-15 18:37 d47d2742  project/src/util.py
     437  Defl:N      255  42% 2026-10-15 18:37 db9dbaf1  project/src/main.c
      96  Defl:N       85  12% 2026-10-15 18:37 40dd630d  project/config.ini
     113  Defl:N       86  24% 2026-10-15 18:37 c7735841  project/Makefile
       0  Stored        0   0% 2026-10-15 18:37 00000000  project/docs/
     166  Defl:N      120  28% 2026-10-15 18:37 bafa674e  project/docs/readme.md
--------          -------  ---                            -------
    1184              766  35%                            8 files

解读:Stored 表示该条目未压缩(目录与 -0 打包的文件),Defl:N 表示 normal 级 deflate;汇总行给出压缩后总大小 766 B、整体压缩率 35%。CRC-32 列可用于跨端比对条目内容是否一致。

💡 要点:排查「这个文件在归档里到底压到多小」「两条目是否同内容」时用 -v;Defl:X/Defl:F 等后缀代表打包端使用的 deflate 级别。

3.5 -t 测试归档完整性

-t 逐条目解压校验但不落盘,是判断归档是否完好的标准手段:

$ unzip -t out.zip; echo rc=$?
Archive:  out.zip
    testing: project/                 OK
    testing: project/src/             OK
    testing: project/src/util.py      OK
    testing: project/src/main.c       OK
    testing: project/config.ini       OK
    testing: project/Makefile         OK
    testing: project/docs/            OK
    testing: project/docs/readme.md   OK
No errors detected in compressed data of out.zip.
rc=0

解读:每个条目一行 testing: ... OK,全部通过后输出 No errors detected,退出码 0。损坏归档的表现见 4.1 节(rc=2/rc=9)。

💡 要点:下载或传输完成的 zip 先 unzip -t 再解压;-t 与解压的校验逻辑一致,能提前发现 CRC 与格式损坏。

3.6 覆盖行为三态:交互 y/N/A 与 -o/-n

目标目录已有同名文件时,默认行为是逐文件询问 [y]es, [n]o, [A]ll, [N]one, [r]ename。先把 ex-target 里的文件时间戳刷新,再分别用交互与非交互方式实测:

$ sleep 2

$ find ex-target/project -type f -exec touch {} +

$ echo N | timeout 10 unzip out.zip -d ex-target; echo rc=$?
Archive:  out.zip
replace ex-target/project/src/util.py? [y]es, [n]o, [A]ll, [N]one, [r]ename: rc=0

$ echo A | timeout 10 unzip out.zip -d ex-target; echo rc=$?
Archive:  out.zip
replace ex-target/project/src/util.py? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/src/util.py  
  inflating: ex-target/project/src/main.c  
  inflating: ex-target/project/config.ini  
  inflating: ex-target/project/Makefile  
  inflating: ex-target/project/docs/readme.md  
rc=0

$ find ex-target/project -type f -exec touch {} +

$ printf "y\ny\ny\ny\ny\ny\n" | timeout 10 unzip out.zip -d ex-target; echo rc=$?
Archive:  out.zip
replace ex-target/project/src/util.py? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/src/util.py  
replace ex-target/project/src/main.c? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/src/main.c  
replace ex-target/project/config.ini? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/config.ini  
replace ex-target/project/Makefile? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/Makefile  
replace ex-target/project/docs/readme.md? [y]es, [n]o, [A]ll, [N]one, [r]ename:   inflating: ex-target/project/docs/readme.md  
rc=0

$ unzip -n out.zip -d ex-target; echo rc=$?
Archive:  out.zip
rc=0

$ unzip -o out.zip -d ex-target; echo rc=$?
Archive:  out.zip
  inflating: ex-target/project/src/util.py  
  inflating: ex-target/project/src/main.c  
  inflating: ex-target/project/config.ini  
  inflating: ex-target/project/Makefile  
  inflating: ex-target/project/docs/readme.md  
rc=0

解读:N(None)答一次即全部跳过且不再提示,rc=0;A(All)答一次即全部覆盖;y 则逐文件询问(5 个文件问了 5 次);-n 从不覆盖、全程静默,rc=0;-o 全部覆盖、从不提示,rc=0。

💡 要点:脚本里永远显式给 -o 或 -n,不要依赖交互提示(管道喂 EOF 会得到 rc=1,见 3.13 节);-n 适合「补缺」,-o 适合「强制还原」。

3.7 -P 密码解压与 rc=82

-P 在命令行直接携带密码。密码正确时正常解压;错误时该条目被跳过并返回 82:

$ unzip -P q1w2e3r4 secret.zip -d ex-pw
Archive:  secret.zip
  inflating: ex-pw/project/config.ini  

$ unzip -P wrongpass secret.zip -d ex-pw2; echo rc=$?
Archive:  secret.zip
   skipping: project/config.ini      incorrect password
rc=82

解读:正确密码一路 inflating:;错误密码回显 skipping: ... incorrect password,由于全部条目都因密码失败,退出码为 82(按手册定义,若至少一个文件成功则降为 rc=1,见 3.16 节摘录)。

💡 要点:-P 的密码会留在 shell 历史与进程列表里,同 zip 的 -P 风险一致;交互式输入密码的方式见 4.2 节工作流。

3.8 解压单个文件与通配符

在归档名后追加成员列表即可部分解压,成员名要写归档内路径,支持通配符:

$ unzip out.zip project/config.ini -d ex-single
Archive:  out.zip
  inflating: ex-single/project/config.ini  

$ unzip out.zip "project/src/*" -d ex-wild
Archive:  out.zip
   creating: ex-wild/project/src/
  inflating: ex-wild/project/src/util.py  
  inflating: ex-wild/project/src/main.c  

$ find ex-single ex-wild -type f | sort
ex-single/project/config.ini
ex-wild/project/src/main.c
ex-wild/project/src/util.py

解读:单成员解压只还原 config.ini;通配符 "project/src/*" 命中 src 下两个文件并自动创建 ex-wild/project/src/ 层级。通配符必须加引号防止 shell 先行展开。

💡 要点:大归档里只要个别文件时,unzip 归档 成员 -d 目录 比「全解再删」快得多;拿不准成员名时先 unzip -l 复制粘贴。

3.9 -x 解压时排除

-x 后跟要排除的成员列表,其余照常解压:

$ unzip out.zip -x project/Makefile project/docs/readme.md -d ex-excl
Archive:  out.zip
   creating: ex-excl/project/
   creating: ex-excl/project/src/
  inflating: ex-excl/project/src/util.py  
  inflating: ex-excl/project/src/main.c  
  inflating: ex-excl/project/config.ini  
   creating: ex-excl/project/docs/

$ find ex-excl -type f | sort
ex-excl/project/config.ini
ex-excl/project/src/main.c
ex-excl/project/src/util.py

解读:Makefile 与 readme.md 被排除,但 ex-excl/project/docs/ 目录仍然被创建——目录条目与文件条目是相互独立的,排除文件不影响目录条目的还原。

💡 要点:-x 写在归档名之后、-d 之前;与 3.8 的「点名解压」相反,适合「除了某几个都要」的场景。

3.10 -j 撇开路径还原(junk paths)

-j 解压时丢弃成员的路径前缀,文件直接落在目标目录下:

$ unzip -j out.zip "project/src/*" -d ex-flat
Archive:  out.zip
  inflating: ex-flat/util.py         
  inflating: ex-flat/main.c          

$ find ex-flat -type f | sort
ex-flat/main.c
ex-flat/util.py

解读:与 3.8 相同的通配符,加上 -j 后 project/src/ 层级被剥掉,文件平铺在 ex-flat/ 下。

💡 要点:-j 常用于「只要归档里的几个文件内容,不关心结构」的场景;不同目录的同名成员平铺后会互相覆盖,用之前先 -l 确认无重名。

3.11 -p 解压到管道

-p 把成员内容直接写到标准输出,不产生任何过程消息,适合与管道下游命令衔接:

$ unzip -p out.zip project/config.ini; echo rc=$?
[server]
host = 127.0.0.1
port = 8080
timeout = 30

[log]
level = info
file = /var/log/demo.log
rc=0

$ unzip -p out.zip project/Makefile | wc -l
10

解读:-p 输出的就是文件原始内容(无 Archive: 头、无 inflating: 行),wc -l 统计 Makefile 为 10 行。成员名必须写归档内完整路径。

💡 要点:unzip -p 归档 成员 | grep 关键字 是「不解压看内容」的利器;也可以用来跨归档流式搬运单个文件内容。

3.12 -z 显示归档注释

-z 只显示归档注释。先给 out.zip 写入注释(打包端的 zip -z 提示符一并保留),再用 unzip -z 读取:

$ printf "demo archive comment\n.\n" | zip -z out.zip; echo rc=$?
enter new zip file comment (end with .):
rc=0

$ unzip -z out.zip; echo rc=$?
Archive:  out.zip
demo archive comment
rc=0

解读:unzip -z 输出 Archive: 行后仅打印注释本身。注释写入后,带注释归档的解压类操作还会先回显一行注释(3.13/3.14 节的实测回显中可见 demo archive comment)。

💡 要点:用 zip -z 写入备份时间、生成命令等审计信息后,unzip -z 一条命令即可读取,无需解包。

3.13 -f 仅解压已存在且较旧的成员(freshen)

-f 只处理「磁盘上已存在、且归档内副本更新」的成员。把 ex-f 中两个文件的时间戳改旧后实测——注意它对较旧文件仍会发起覆盖询问,管道 EOF 直接把选择当作 [N]one:

$ rm -rf ex-f && unzip -q out.zip -d ex-f

$ touch -d "2026-01-01 00:00:00" ex-f/project/config.ini ex-f/project/Makefile

$ unzip -f out.zip -d ex-f; echo rc=$?
Archive:  out.zip
demo archive comment
replace ex-f/project/config.ini? [y]es, [n]o, [A]ll, [N]one, [r]ename:  NULL
(EOF or read error, treating as "[N]one" ...)
rc=1

$ ls -l ex-f/project/config.ini ex-f/project/Makefile ex-f/project/src/main.c
-rw-r--r-- 1 root root 113 Jan  1  2026 ex-f/project/Makefile
-rw-r--r-- 1 root root  96 Jan  1  2026 ex-f/project/config.ini
-rw-r--r-- 1 root root 437 Oct 15 18:37 ex-f/project/src/main.c

解读:三个时间信号——较新的 main.c(Oct 15 18:37)被 -f 静默跳过(无任何回显);较旧的 config.ini(Jan 1 2026)被选中准备更新,但覆盖已存在文件仍触发 replace ... ? 询问;管道 EOF(无输入)时 unzip 打印 NULL (EOF or read error, treating as "[N]one" ...) 并以 rc=1 退出,ls 证实两个文件都保持 2026-01-01 未被改写。

💡 要点:-f 并不隐含「安静覆盖」——帮助示例原文 unzip -fo foo ReadMe => quietly replace existing ReadMe if archive file newer,即「时间筛选靠 -f、安静覆盖靠 -o」,两者要组合使用;EOF 中止(rc=1)与显式 N(rc=0)语义不同,脚本喂空输入会踩坑。

3.14 -u 更新旧成员并重建缺失成员

-u 在 -f 的行为之上,还会重建磁盘上缺失的成员。删掉 ex-f 里的 Makefile 后实测:

$ rm ex-f/project/Makefile

$ unzip -u out.zip -d ex-f; echo rc=$?
Archive:  out.zip
demo archive comment
replace ex-f/project/config.ini? [y]es, [n]o, [A]ll, [N]one, [r]ename:  NULL
(EOF or read error, treating as "[N]one" ...)
  inflating: ex-f/project/Makefile   
rc=1

$ ls -l ex-f/project/Makefile
-rw-r--r-- 1 root root 113 Oct 15 18:37 ex-f/project/Makefile

解读:缺失的 Makefile 无需询问、直接 inflating: 还原(时间戳恢复为归档内副本的 Oct 15 18:37);较旧的 config.ini 与 3.13 节一样进入询问并被 EOF 中止。rc=1 来自被 [N]one 跳过的 config.ini(warning 级),Makefile 的重建本身是成功的。

💡 要点:「-u = -f + 补缺」,增量同步场景用它;要全程无提示请组合 -o(如 -uo),否则较旧成员仍会询问。

3.15 -O 指定字符集与中文文件名

-O 为 DOS/Windows/OS2 来源的归档指定文件名字符集,-I 对应 UNIX 来源。实测环境 locale 为 C(LANG 为空),分别用 UTF-8 locale 与 C locale 准备含中文文件名的归档再解压观察:

$ locale | head -2
LANG=
LANGUAGE=

$ echo "这是中文文件名测试文件的内容。" > 中文说明.txt

$ LC_ALL=C.UTF-8 zip chs-utf8.zip 中文说明.txt
  adding: 中文说明.txt (deflated 2%)

$ LC_ALL=C.UTF-8 unzip -l chs-utf8.zip
Archive:  chs-utf8.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       46  2026-10-15 18:38   中文说明.txt
---------                     -------
       46                     1 file

$ LC_ALL=C zip chs-c.zip 中文说明.txt
  adding: 中文说明.txt (deflated 2%)

$ LC_ALL=C.UTF-8 unzip -l chs-c.zip; echo rc=$?
Archive:  chs-c.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       46  2026-10-15 18:38   中文说明.txt
---------                     -------
       46                     1 file
rc=0

$ LC_ALL=C.UTF-8 unzip -O UTF-8 -l chs-c.zip; echo rc=$?
Archive:  chs-c.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       46  2026-10-15 18:38   中文说明.txt
---------                     -------
       46                     1 file
rc=0

$ LC_ALL=C.UTF-8 unzip chs-c.zip -d ex-moji && ls ex-moji
Archive:  chs-c.zip
  inflating: ex-moji/中文说明.txt  
中文说明.txt

$ LC_ALL=C.UTF-8 unzip -O UTF-8 chs-c.zip -d ex-moji-ok && ls ex-moji-ok
Archive:  chs-c.zip
  inflating: ex-moji-ok/中文说明.txt  
中文说明.txt

$ rm -rf ex-moji ex-moji-ok

$ LC_ALL=C.UTF-8 unzip -O cp437 -l chs-c.zip; echo rc=$?
Archive:  chs-c.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       46  2026-10-15 18:38   中文说明.txt
---------                     -------
       46                     1 file
rc=0

$ LC_ALL=C.UTF-8 unzip -O cp437 chs-c.zip -d ex-moji437 && ls ex-moji437
Archive:  chs-c.zip
  inflating: ex-moji437/中文说明.txt  
中文说明.txt

$ rm -rf ex-moji437

解读:EFS(UTF-8)标志只在 UTF-8 locale 打包时写入;C locale 打包的无标志归档在本环境解压时,无论默认、-O UTF-8 还是强制 -O cp437,列清单与解包出的文件名都完整正常(全程 rc=0)。也就是说本环境的 Debian UnZip 6.00 对 cp437 下无法有效映射的 UTF-8 字节序列回退为原字节透传,没有复现出乱码。

💡 要点:乱码的典型成因是「打包端无 EFS 标志 + 解压端按本地代码页强行转码」,本环境的透传行为属于宽容实现,不能指望所有解压器都如此;跨平台分发时打包端保持 UTF-8 locale,解压端如遇乱码用 -O 显式指定来源字符集逐一尝试。

3.16 返回码体系(man 手册 DIAGNOSTICS 节)

unzip 的退出码在手册 DIAGNOSTICS 节有权威定义。本环境无 man 命令,用 zcat 直接读手册页文件(troff 源码原样保留):

$ zcat /usr/share/man/man1/unzip.1.gz | sed -n "/^.SH DIAGNOSTICS/,/^.SH BUGS/p" | head -50
.SH DIAGNOSTICS
The exit status (or error level) approximates the exit codes defined by PKWARE
and takes on the following values, except under VMS:
.RS
.IP 0
normal; no errors or warnings detected.
.IP 1
one or more warning errors were encountered, but processing completed
successfully anyway.  This includes zipfiles where one or more files
was skipped due to unsupported compression method or encryption with an
unknown password.
.IP 2
a generic error in the zipfile format was detected.  Processing may have
completed successfully anyway; some broken zipfiles created by other
archivers have simple work-arounds.
.IP 3
a severe error in the zipfile format was detected.  Processing probably
failed immediately.
.IP 4
\fIunzip\fP was unable to allocate memory for one or more buffers during
program initialization.
.IP 5
\fIunzip\fP was unable to allocate memory or unable to obtain a tty to read
the decryption password(s).
.IP 6
\fIunzip\fP was unable to allocate memory during decompression to disk.
.IP 7
\fIunzip\fP was unable to allocate memory during in-memory decompression.
.IP 8
[currently not used]
.IP 9
the specified zipfiles were not found.
.IP 10
invalid options were specified on the command line.
.IP 11
no matching files were found.
.IP 50
the disk is (or was) full during extraction.
.IP 51
the end of the ZIP archive was encountered prematurely.
.IP 80
the user aborted \fIunzip\fP prematurely with control-C (or similar)
.IP 81
testing or extraction of one or more files failed due to unsupported
compression methods or unsupported decryption.
.IP 82
no files were found due to bad decryption password(s).  (If even one file is
successfully processed, however, the exit status is 1.)
.RE
.PP

解读:本文实测命中的返回码对照——0(正常,3.5 节)、1(有跳过/警告但整体完成,3.13/3.14 节的 EOF 中止)、2(格式类错误,4.1 节 extra-field 损坏)、82(密码全败,3.7 节)、9(归档未找到,4.1 节截断件对 -t 亦如此)。

💡 要点:rc=1 是最容易被脚本忽略的返回码——「完成但跳过了东西」;对归档完整性敏感的流程应把 1 也当作异常处理。

四、实战示例

4.1 损坏 zip 的排障路径

传输中断与磁盘错误是 zip 损坏的两大来源,两类损坏在 unzip 里的表现不同。构造「截断件」(只保留前 200 字节,中央目录丢失)与「字节翻转件」(用 dd 把偏移 200 处 4 字节改写为 0)分别实测:

$ head -c 200 out.zip > broken.zip

$ unzip broken.zip; echo rc=$?
Archive:  broken.zip
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of broken.zip or
        broken.zip.zip, and cannot find broken.zip.ZIP, period.
rc=9

$ unzip -t broken.zip; echo rc=$?
Archive:  broken.zip
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
unzip:  cannot find zipfile directory in one of broken.zip or
        broken.zip.zip, and cannot find broken.zip.ZIP, period.
rc=9

$ cp out.zip crcbad.zip

$ printf "\x00\x00\x00\x00" | dd of=crcbad.zip bs=1 seek=200 conv=notrunc 2>/dev/null

$ unzip -t crcbad.zip; echo rc=$?
Archive:  crcbad.zip
    testing: project/                 OK
    testing: project/src/             OK
    testing: project/src/util.py     bad extra-field entry:
      EF block length (4 bytes) exceeds remaining EF data (3 bytes)
    testing: project/src/main.c       OK
    testing: project/config.ini       OK
    testing: project/Makefile         OK
    testing: project/docs/            OK
    testing: project/docs/readme.md   OK
At least one error was detected in crcbad.zip.
rc=2

解读:截断件连中央目录都找不到,报 End-of-central-directory signature not found,普通解压与 -t 都是 rc=9,此时文件头还在,可尝试 zip -FF 类修复工具抢救局部数据;字节翻转件的中央目录完好,-t 能精确指出坏在哪个条目的哪个字段(bad extra-field entry: EF block length (4 bytes) exceeds remaining EF data (3 bytes)),其余条目照常 OK,整体 rc=2。

💡 要点:先 unzip -t 再下结论——rc=9 说明「尾部/中央目录没了」(多为截断),rc=2 说明「某个条目结构坏了但整体框架还在」(可尝试部分解压未损坏条目);排障时保留原始损坏件,所有试验在副本上进行。

4.2 加密归档的端到端工作流

加密 zip 的两端都可以脚本化:打包端 zip -e 用 script 喂两次密码(输入 + 确认),解压端 unzip 交互提示只需喂一次:

$ { sleep 2; echo q1w2e3r4; sleep 2; echo q1w2e3r4; } | timeout 15 script -qec "zip -e secret.zip project/config.ini" /dev/null
Enter password: 
Verify password: 
  adding: project/config.ini (deflated 11%)

$ { sleep 2; echo q1w2e3r4; } | timeout 15 script -qec "unzip secret.zip -d ex-pw3" /dev/null; echo rc=$?
Archive:  secret.zip
[secret.zip] project/config.ini password: 
  inflating: ex-pw3/project/config.ini  
rc=0

解读:解压端的交互提示是 [secret.zip] project/config.ini password:,没有二次确认,喂一次即可,rc=0。配合 3.7 节的 -P(明文、rc=82 演示)与 3.16 节的返回码定义,加密链路的三种结局(成功 0 / 部分成功 1 / 密码全败 82)都有实测依据。

💡 要点:两端喂入次数不同——zip 要两次(Enter + Verify),unzip 只要一次;script -qec "命令" /dev/null 是无终端环境下处理交互提示的通用方案。

4.3 覆盖行为决策速查

综合 3.6/3.13/3.14 节的实测,按场景选择覆盖策略:

场景用法实测行为退出码
首次解压(无同名)unzip 归档 -d 目录creating/inflating0
强制全部覆盖-o全部 inflating,无提示0
一个都不覆盖-n全部跳过,无提示0
逐个人工决策默认(交互)每文件询问 y/n/A/N/r0(EOF 中止为 1)
只更新较旧成员-f较新静默跳过;较旧覆盖前仍询问0/1
更新旧成员 + 补缺-u缺失成员直接 inflating0/1
脚本安静增量同步-uo / -fo时间筛选 + 无提示覆盖0

解读:交互提示只在「要覆盖已存在文件」时出现;-f/-u 的时间筛选不消除覆盖询问,安静化必须叠加 -o。

💡 要点:记住两个组合——「还原现场」用 -o,「增量同步」用 -uo;管道喂答案的方案遇到 EOF 会得到 rc=1,宁可显式给 -o/-n。

4.4 部分解压姿势速查

综合 3.8/3.9/3.10/3.11 节的实测,按「要什么」选姿势:

需求命令形态实测结果节
还原单个成员unzip out.zip project/config.ini -d 目录单文件带路径还原3.8
按通配符还原unzip out.zip "project/src/*" -d 目录命中成员 + 层级重建3.8
排除部分成员unzip out.zip -x 成员... -d 目录其余全解,目录条目仍创建3.9
平铺还原unzip -j out.zip "project/src/*" -d 目录丢弃层级直接落目标目录3.10
内容直取不落盘unzip -p out.zip 成员 | 下游命令纯内容进管道,无过程消息3.11

解读:成员名一律写归档内路径(先 unzip -l 确认);通配符加引号防 shell 展开;-j 前确认无同名冲突。

💡 要点:大归档的局部消费优先「-p 看内容、点名/通配符解文件」,避免全量解压;确实要全量时再套 -d + 覆盖策略(4.3 节)。

五、注意事项与常见问题

Q1:管道里执行 unzip 提示 NULL (EOF or read error, treating as "[N]one" ...) 且退出码 1? 这是交互覆盖询问遇到 EOF 的回退行为:unzip 把「读不到输入」当作 [N]one 全部跳过,并以 rc=1 退出(3.13/3.14 节实测)。它与显式回答 N(rc=0)不同。脚本场景请直接使用 -o(全覆盖)或 -n(全不覆盖)。

Q2:-f/-u 不是「安静更新」吗,为什么还会询问? -f/-u 只负责按时间筛选哪些成员需要处理(较新成员静默跳过、缺失成员由 -u 重建),一旦选中成员需要覆盖磁盘上的现有文件,仍走交互询问流程。安静化要组合 -o,帮助示例原文 unzip -fo foo ReadMe => quietly replace existing ReadMe if archive file newer 即此惯用法。

Q3:rc=82 和 rc=1 都是「没解出来」,怎么区分? rc=82 表示所有文件都因密码错误未解出(3.7 节实测 skipping: ... incorrect password);若至少一个文件成功、其余被跳过,则按手册定义降为 rc=1(3.16 节摘录)。rc=1 还涵盖「部分条目被跳过」的其他 warning 场景,处理脚本时把 1 与 0 分开对待。

Q4:中文文件名解出来是乱码怎么办? 先确认打包端:UTF-8 locale 打包会写 EFS 标志(3.15 节实测两端正常)。C locale 打包的无标志归档在本环境实测为原字节透传(含强制 -O cp437 亦正常),但严格按代码页转码的解压器(部分 Windows 工具)仍可能乱码;此时在解压端用 -O 逐一指定来源字符集(cp437/GBK 等)尝试还原。

Q5:环境里没有 man 命令,怎么看 unzip 手册? 手册页文件通常仍在:zcat /usr/share/man/man1/unzip.1.gz | less(3.16 节即用 zcat | sed -n 精确提取 DIAGNOSTICS 节)。需要渲染版可临时安装 man-db,或直接查 man7.org 在线版。

六、参考链接