侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 184 篇文章
  • 累计收到 7 条评论

源目录末尾少一个斜杠,rsync 就多建一层目录

2026-10-13 / 0 评论 / 6 阅读

源目录末尾少一个斜杠,rsync 就多建一层目录

同一个源目录,命令里只改一处写法,目标里长出来的目录树就不一样。本机 /usr/bin/rsync 报的版本是 openrsync(protocol version 29),跟 GNU 那份不是同一个实现,下面每一条都在这台机器上跑过。

样例树搭在 /private/tmp/verify-rsync-slash:一个 a.txt、一个子目录 sub、两处叫 build 的目录、一个指向 a.txt 的符号链接,外加几条 .log。输出全部是 find 和 ls 的原文,退出码逐条打在旁边。

$ /usr/bin/rsync --version
openrsync: protocol version 29
rsync version 2.6.9 compatible

$ uname -sm
Darwin arm64

==== 1. 样例源树 ====
$ find src -print
src
src/link.txt
src/sub
src/sub/b.txt
src/sub/build
src/sub/build/junk2.txt
src/a.txt
src/build
src/build/junk1.txt

$ ls -l src
total 8
-rwxr----x  1 yxh  wheel    2 Jan  2  2020 a.txt
drwxr-xr-x  3 yxh  wheel   96 Oct 13 08:08 build
lrwxr-xr-x  1 yxh  wheel    5 Oct 13 08:08 link.txt -> a.txt
drwx------  4 yxh  wheel  128 Oct 13 08:08 sub

源目录里 link.txt 是指向 a.txt 的符号链接,a.txt 的权限位是 741,sub 是 700。

源目录后面那一下斜杠,目标里就多一层

$ rsync -r src/ dst_slash/
skipping non-regular file "link.txt"
退出码 0

$ find dst_slash -print
dst_slash
dst_slash/sub
dst_slash/sub/b.txt
dst_slash/sub/build
dst_slash/sub/build/junk2.txt
dst_slash/a.txt
dst_slash/build
dst_slash/build/junk1.txt

$ rsync -r src dst_plain/
skipping non-regular file "src/link.txt"
退出码 0

$ find dst_plain -print
dst_plain
dst_plain/src
dst_plain/src/sub
dst_plain/src/sub/b.txt
dst_plain/src/sub/build
dst_plain/src/sub/build/junk2.txt
dst_plain/src/a.txt
dst_plain/src/build
dst_plain/src/build/junk1.txt

两条命令的退出码都是 0,终端上唯一多出来的一行是 skipping non-regular file,那是符号链接没被复制,跟斜杠无关。只看两份列举的第一行就够了:dst_slash 下面是 a.txt,dst_plain 下面是 src。

斜杠的意思是把源目录里的内容当成要传的东西;不写斜杠,源目录本身也成了一个条目,于是目标里多出一层。

目标目录还不存在时,它按哪个名字建

$ rsync -r src/ new_a/
skipping non-regular file "link.txt"
退出码 0

$ find new_a -print
new_a
new_a/sub
new_a/sub/b.txt
new_a/sub/build
new_a/sub/build/junk2.txt
new_a/a.txt
new_a/build
new_a/build/junk1.txt

new_a 是新建出来的,第一行就是 a.txt,跟带斜杠那次的结果一样。

$ rsync -r src new_b
skipping non-regular file "src/link.txt"
退出码 0

$ find new_b -print
new_b
new_b/src
new_b/src/sub
new_b/src/sub/b.txt
new_b/src/sub/build
new_b/src/sub/build/junk2.txt
new_b/src/a.txt
new_b/src/build
new_b/src/build/junk1.txt

$ rsync -r src/ old/child/
skipping non-regular file "link.txt"
退出码 0

$ find old -print
old
old/child
old/child/sub
old/child/sub/b.txt
old/child/sub/build
old/child/sub/build/junk2.txt
old/child/a.txt
old/child/build
old/child/build/junk1.txt

不带斜杠时建出来的是 new_b/src,源目录的名字也被算成一个条目,整棵树往下沉了一层。父目录已存在、子目录还不存在的写法更常见:rsync -r src/ old/child/ 建的是 old/child,不多不少。

--delete 一加,目标里多出来的东西就没了

$ rsync -r s2/ d2/
退出码 0

$ find d2 -print(同步后)
d2
d2/t
d2/t/extra.txt
d2/t/onlydir
d2/t/keep.txt

$ ls -R d3(同步前)
t

d3/t:
extra.txt
keep.txt
onlydir

d3/t/onlydir:

$ rsync -r --delete s2/ d3/
退出码 0

$ find d3 -print(--delete 之后)
d3
d3/t
d3/t/keep.txt

d2 那次没加 --delete,同步完 extra.txt 和 onlydir 都还在,只有 keep.txt 跟着源目录走。d3 加了 --delete,同一个动作跑完只剩 keep.txt,目标里独有的文件和空目录一起被清掉。

两次的退出码都是 0,没有一句提示。路径写反一次,删的是别的目录,没有回收站。

要动它之前,先用 -av 加 --dry-run 跑一遍,准备删的东西会逐条打出来,文件一个不动。

$ rsync -av --dry-run --delete s2/ d4/
Transfer starting: 3 files
deleting t/onlydir/
deleting t/extra.txt

sent 128 bytes  received 20 bytes  134545 bytes/sec
total size is 2  speedup is 0.01
退出码 0
$ find d4 -print(干跑之后)
d4
d4/t
d4/t/extra.txt
d4/t/onlydir

去掉 --dry-run 再跑,同样那两行变成真的删除。

$ rsync -av --delete s2/ d4/
Transfer starting: 3 files
deleting t/onlydir/
deleting t/extra.txt

sent 128 bytes  received 20 bytes  134545 bytes/sec
total size is 2  speedup is 0.01
退出码 0
$ find d4 -print(真删之后)
d4
d4/t
d4/t/keep.txt

换成 -a 之后,权限和时间戳才跟过来

$ rsync -r src/ dst_r/
skipping non-regular file "link.txt"
退出码 0

$ rsync -a src/ dst_ar/
退出码 0

$ ls -l src
total 8
-rwxr----x  1 yxh  wheel    2 Jan  2  2020 a.txt
drwxr-xr-x  3 yxh  wheel   96 Oct 13 08:08 build
lrwxr-xr-x  1 yxh  wheel    5 Oct 13 08:08 link.txt -> a.txt
drwx------  4 yxh  wheel  128 Oct 13 08:08 sub
$ ls -l dst_r
total 8
-rwxr----x  1 yxh  wheel    2 Oct 13 08:08 a.txt
drwxr-xr-x  3 yxh  wheel   96 Oct 13 08:08 build
drwxr-xr-x  4 yxh  wheel  128 Oct 13 08:08 sub
$ ls -l dst_ar
total 8
-rwxr----x  1 yxh  wheel    2 Jan  2  2020 a.txt
drwxr-xr-x  3 yxh  wheel   96 Oct 13 08:08 build
lrwxr-xr-x  1 yxh  wheel    5 Oct 13 08:08 link.txt -> a.txt
drwx------  4 yxh  wheel  128 Oct 13 08:08 sub
$ ls -l dst_r/link.txt
ls: dst_r/link.txt: No such file or directory
$ ls -l dst_ar/link.txt
lrwxr-xr-x  1 yxh  wheel  5 Oct 13 08:08 dst_ar/link.txt -> a.txt

ls -l 里能直接看出分歧。dst_r 里没有 link.txt 这一条,源目录里那个符号链接只在终端留了一句 skipping non-regular file;dst_ar 里它是 lrwxr-xr-x,还指回 a.txt。

$ stat -f '%Sp %N' src/sub dst_r/sub dst_ar/sub
drwx------ src/sub
drwxr-xr-x dst_r/sub
drwx------ dst_ar/sub

$ stat -f '%Sm %N' -t '%Y-%m-%d %H:%M' src/a.txt dst_r/a.txt dst_ar/a.txt
2020-01-02 03:04 src/a.txt
2026-10-13 08:08 dst_r/a.txt
2020-01-02 03:04 dst_ar/a.txt

sub 的权限位也不一样:源目录是 drwx------,dst_r 里是 drwxr-xr-x,dst_ar 里跟着源目录走。mtime 那三行更直白,dst_ar 里的 a.txt 停在 2020-01-02 03:04,dst_r 里的换成了当前时间。

有一处和 GNU 的文档对不上:本机这份 rsync 在 -r 下把普通文件的权限位也保住了,a.txt 的 741 原样搬进了 dst_r。GNU rsync 上这条我没实测。

--exclude 匹配的是整条路径还是最后一段

$ find ex -print
ex
ex/sub
ex/sub/deep
ex/sub/keep.txt
ex/sub/build
ex/sub/build/junk2.txt
ex/sub/deep.log
ex/notes.log
ex/build
ex/build/junk1.txt
ex/w
ex/w/build

$ rsync -r --exclude=build ex/ ex_a/
退出码 0
$ find ex_a -print
ex_a
ex_a/sub
ex_a/sub/deep
ex_a/sub/keep.txt
ex_a/sub/deep.log
ex_a/notes.log
ex_a/w

$ rsync -r --exclude=build/ ex/ ex_b/
退出码 0
$ find ex_b -print
ex_b
ex_b/sub
ex_b/sub/deep
ex_b/sub/keep.txt
ex_b/sub/deep.log
ex_b/notes.log
ex_b/w
ex_b/w/build

样例树里放了两处同名但类型不同的东西:ex/w/build 是普通文件,ex/build 是目录。用它们试 --exclude=build,ex_a 里一个叫 build 的条目都没留下,连 ex_a/w 都成了空目录。末尾补一个斜杠之后,ex_b/w/build 这个普通文件留了下来,目录照旧被挡。

$ rsync -r --exclude=/build ex/ ex_c/
退出码 0
$ find ex_c -print
ex_c
ex_c/sub
ex_c/sub/deep
ex_c/sub/keep.txt
ex_c/sub/build
ex_c/sub/build/junk2.txt
ex_c/sub/deep.log
ex_c/notes.log
ex_c/w
ex_c/w/build

斜杠写在开头是另一层意思。--exclude=/build 只挡传输根目录下的 build,ex_c/sub/build 连里面的 junk2.txt 都跟着复制了过来。

$ rsync -r --exclude=sub/build ex/ ex_d/
退出码 0
$ find ex_d -print
ex_d
ex_d/sub
ex_d/sub/deep
ex_d/sub/keep.txt
ex_d/sub/deep.log
ex_d/notes.log
ex_d/build
ex_d/build/junk1.txt
ex_d/w
ex_d/w/build

$ rsync -r --exclude='*.log' ex/ ex_e/
退出码 0
$ find ex_e -print
ex_e
ex_e/sub
ex_e/sub/deep
ex_e/sub/keep.txt
ex_e/sub/build
ex_e/sub/build/junk2.txt
ex_e/build
ex_e/build/junk1.txt
ex_e/w
ex_e/w/build

--exclude=sub/build 的模式里带斜杠,匹配的是从传输根算起的整条路径,所以只挡掉 ex_d/sub/build,顶层那个 build 目录反而被复制过来。--exclude='*.log' 没有斜杠,按最后一段匹配,ex_e/notes.log 和 ex_e/sub/deep.log 一起消失。

两处口径合成一句话:模式里没有斜杠按最后一段匹配,有斜杠从传输根算起,末尾多一个斜杠只管目录。

可以直接抄进脚本的一组写法

几条合在一起,落在脚本里大概是这样。

# 目录内容镜像到目标,多余文件删掉,日志和 .git 不跟过去
rsync -a --delete \
  --exclude='.git/' \
  --exclude='*.log' \
  --exclude='build/' \
  src/ /srv/mirror/

# 目标还不存在时,先建好上一级再同步内容
mkdir -p /srv/mirror
rsync -a src/ /srv/mirror/

# 删之前先看一眼
rsync -a --dry-run --delete src/ /srv/mirror/

几条不能省的:源目录结尾的斜杠决定目标里有没有多一层;脚本里默认用 -a;--delete 只在确认目标就是镜像时才加,加之前先跑 --dry-run;--exclude 没有斜杠按最后一段匹配,有斜杠从传输根算起,末尾的斜杠只管目录。

    🤞 分享