今天遇到的一个小问题:foo.txt 是一个常规文件,link-1 是一个符号连接(有些地方又叫软连接,symbol link),指向的是 foo.txt,我们使用 sed -i"" 's/foo/bar/g'link-1 的内容进行替换,替换完成之后,发现 link-1 从一个符号连接变成了一个常规的文件。

实验流程如下,我们对 link-1 进行原地替换,-i"",执行之后,foo.txt 的内容没有改变,但是 link-1 从符号连接变成了常规文件:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

vagrant@foobarhost:~/test$ cat foo.txt

foo

1112

hello

world

vagrant@foobarhost:~/test$ ln -s foo.txt link-1

vagrant@foobarhost:~/test$ ls -l

total 4

-rw-r--r-- 1 vagrant vagrant 21 Oct 27 07:44 foo.txt

lrwxrwxrwx 1 vagrant vagrant  7 Oct 27 07:47 link-1 -> foo.txt

vagrant@foobarhost:~/test$ sed -i"" 's/foo/bar/g' link-1

vagrant@foobarhost:~/test$ ls -l

total 8

-rw-r--r-- 1 vagrant vagrant 21 Oct 27 07:44 foo.txt

-rw-r--r-- 1 vagrant vagrant 21 Oct 27 07:47 link-1

vagrant@foobarhost:~/test$ cat foo.txt

foo

1112

hello

world

vagrant@foobarhost:~/test$ cat link-1

bar

1112

hello

world

乍一看,不合理,sed 只是用来替换一个文件,怎么能改变文件的属性呢?

但是仔细一想,其实挺合理:

  • sed 不可能同时打开一个文件,一边读一边写,因为一写的话,文件会被 truncate。比如执行一下 cat foo.txt > foo.txt,文件会被直接清空;
  • sed 也不可能将文件读入内存,处理,然后写入原文件。因为 sed 最基本的设计就是一个“行处理器”,要一行一行 streaming 处理,读入一部分,处理,然后写入一部分,用很少的内存就够了;
  • 所以要实现原地替换的话,就需要有一个临时文件,sed 先把结果写入到这个文件,最后将文件 rename 到原来的地方;

可以用 strace 来验证我们的推测:

1

strace sed -i"" 's/foo/bar/g' link-1 > strace.log 2>&1

可以看到,sed 就是打开了一个临时文件,然后读-处理-写,最后进行 rename(2)

使用 strace 跟踪 sed 的结果

那有没有不改变文件性质的方法呢?如果解析出来符号连接指向的目标就好了。

很多 Unix 命令,比如 cp, ls 都有设置 follow 符号连接还是不 follow 的选项,sed 应该也有。看了下 man sed,发现果然有。

       --follow-symlinks

              follow symlinks when processing in place

再用 strace 追踪了一下过程,发现 rename 的时候,就会指向符号连接的 target 而不是符号连接本身了。

strace 追踪 sed 执行过程