
打包脚本出问题时,第一反应通常是通配符写错了。位置错了更常见:--exclude 站在源目录后面,bsdtar 把它当成一个待添加的成员,直接报 Cannot stat,同时排除规则一条都没生效。本机是 macOS 自带的 bsdtar 3.5.3,十二处容易站错位置的写法各跑一遍,输出如下。
tar 版本: bsdtar 3.5.3 - libarchive 3.7.4 zlib/1.2.12 liblzma/5.4.3 bz2lib/1.0.8
系统: Darwin
== 1. --exclude 写在源目录后面
tar: --exclude=*.log: Cannot stat: No such file or directory
tar: Error exit delayed from previous errors.
包内成员: src/ src/run.log src/b.txt src/logs/ src/a.txt src/logs/run.log
== 2. --exclude 写在源目录前面
包内成员: src/ src/b.txt src/logs/ src/a.txt
== 3. --exclude 想连子目录里的 log 一起排掉
包内成员: src/ src/b.txt src/logs/ src/a.txt
换 src/logs/* 之后: src/ src/run.log src/b.txt src/a.txt
== 4. 打包时给绝对路径
tar: Removing leading '/' from member names
包内前三个成员: var/folders/vz/zf2_sm250pgd4qb_p847p1240000gn/T/tmp.LqTiZwJksE/src/ var/folders/vz/zf2_sm250pgd4qb_p847p1240000gn/T/tmp.LqTiZwJksE/src/run.log var/folders/vz/zf2_sm250pgd4qb_p847p1240000gn/T/tmp.LqTiZwJksE/src/b.txt
== 5. -C 的位置(想只打 src 里那一层,不想要 src/ 这层目录)
tar -cf out.tar -C src . → ./ ./run.log ./b.txt ./logs/ ./a.txt
tar -cf out.tar src -C src → src/ src/run.log src/b.txt src/logs/ src/a.txt src/logs/run.log
== 6. 带路径的打包 vs 不带
进目录打 . → ./ ./run.log ./b.txt ./logs/ ./a.txt
进目录只打两个文件 → a.txt b.txt
== 7. 选项里的 f 后面必须紧跟包名
tar -czf out.tar.gz src → 成功, 成员数 6
tar: out/bad.tar.gz: Cannot stat: No such file or directory
tar: Error exit delayed from previous errors.
== 8. 往 .tar.gz 里追加文件
tar: Cannot append to compressed archive.
== 9. 解包时的 -C 与 --strip-components
直接解到 out/x1: a.txt b.txt logs run.log
--strip-components=1 之后: a.txt b.txt logs
== 10. 解包时目标位置已有同名文件
默认行为, a.txt 现在是: 订单 A
加 -k 之后, a.txt 现在是: 旧内容
逐条对一遍
-
--exclude写在源目录后面,排除规则整条失效。命令是tar -cf out.tar src --exclude='*.log',结果是报tar: --exclude=*.log: Cannot stat: No such file or directory,然后包里照样有src/run.log与src/logs/run.log。它一边报错一边把包打完了,只看退出码的脚本会以为成功。 -
把选项挪到源目录前面,同一份目录立刻排干净:
tar -cf out.tar --exclude='*.log' src,成员只剩四个,两层里的 log 都不见了。这是全部十条里最值得先记住的一条。 -
排除子目录时,通配要写全路径。
--exclude='src/logs/*'的成员列表里连src/logs/这个目录本身都没有了;换句话说它连空目录一起排掉。--exclude='*.log'保留目录、只排文件。要不要那个空目录,决定选哪种写法。 -
绝对路径会被削掉开头的斜杠。
tar -cf out.tar /var/folders/.../src打印Removing leading '/' from member names,包内成员变成长长的相对路径。想控制包里的层级结构,用-C而不是靠绝对路径。 -
-C站在成员后面等于没写。tar -cf out.tar -C src .得到./ ./a.txt ./b.txt这一层;写成tar -cf out.tar src -C src之后,包内成员跟不带-C时一模一样,连src/logs/run.log都还在里面。参数是按出现顺序生效的,位置一变语义就变。 -
包内结构由打包时那串路径决定,解包时改不了。同一个目录,进目录打
.得到带./前缀的成员,进目录只打两个文件名就得到平铺的两层文件。这一步想清楚,后面就不用--strip-components打补丁。 -
-f后面紧跟的那一位就是包名,选项字母别乱堆。tar -czf out.tar.gz src正常;写成tar -cfz out/bad.tar.gz src,-f吃掉了z,out/bad.tar.gz被当成成员去 stat,报同样的Cannot stat。错误信息指向的是「文件不存在」,跟真实原因隔着好几层。 -
压缩包不能追加。
tar -rf out.tar.gz 新文件直接报Cannot append to compressed archive.,因为 gzip 流无法就地插入。需要反复加文件的场景,先把.tar留着,最后再压一次。 -
--strip-components只在解包时起作用,而且按成员路径层级数。解src/那一层用--strip-components=1,配合-C指定目标目录,就不用先cd再解。 -
解包时目标位置已有同名文件,默认直接覆盖。实测
a.txt从「旧内容」变成包里的内容;加-k之后旧文件保留,内容仍是「旧内容」。备份恢复脚本里这个默认值值得显式写出来,别指望它保守。 -
上面这些错误都走「延迟到最后」这条路。bsdtar 报
Error exit delayed from previous errors.,包已经写出来了,退出码非 0 但文件在磁盘上,看起来半成功。脚本里不能只看返回值,也顺手看一眼 stderr。 -
打完包最好回头对一次成员列表。
tar -tf out.tar打出来的是解包后的真实路径,比打包命令更能说明问题;把期望的文件名和排除掉的模式各 grep 一次,比对着命令参数推理可靠。
落到脚本上
把顺序固定成一种形状,上面十二条里有十条都不会遇到:先写选项,再写 -C,最后才是要打包的目录或文件。
# 选项在最前,-C 紧挨着目标目录的含义
tar -czf "$OUT" --exclude='*.log' --exclude='.DS_Store' -C "$SRC" .
若只让我留一条,就是「要排除的先写在前面」:它同时解决第 1、2 两条,而这两条是上面这趟输出里直接撞上的,也最容易被安静地放过。整份输出里没有一条报错会让 tar 提前退出,Cannot stat 只是「延迟到最后的错误」,包已经写出来了,所以这种错误必须自己去 grep 输出,不能只看返回值。