今天遇到的一个小问题: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)
:
那有没有不改变文件性质的方法呢?如果解析出来符号连接指向的目标就好了。
很多 Unix 命令,比如 cp
, ls
都有设置 follow 符号连接还是不 follow 的选项,sed
应该也有。看了下 man sed
,发现果然有。
1 2 3 |
--follow-symlinks follow symlinks when processing in place |
再用 strace
追踪了一下过程,发现 rename
的时候,就会指向符号连接的 target 而不是符号连接本身了。
您好,也就是说:
1、sed 替换的底层逻辑是把更新后的内容写入一个临时文件里面,然后再重命名这个临时文件为原文件
2、这样就会使得如果没有添加‘–follow-symlinks’选项的话,对软链接文件进行 sed 操作就会使得软链文件变成了一个常规文件(其实是那个临时文件重命名导致的)
我这么理解对吗?
完全正确!
谢谢!
不敢原地替换的,万一中间不小心输错了,原文件不就没了吗
是的,但是操作的目的就是修改原本的配置文件。测试的时候不会用
-i""
,测试没问题了,批量执行的时候才会真正用。-i.bak 会备份原文件成 *.bak
其实还有个 -c 选项,–follow-symlinks 针对软链接是适用的,对硬链接是不适用的,-c 对软硬链接都适用。按照 man 手册说法,如果不涉及硬链接,一般 –follow-symlinks 够了,性能较好。
谢谢!学习了。