共计 3126 个字符,预计需要花费 8 分钟才能阅读完成。
我很喜爱 Linux 零碎,尤其是 Linux 的一些设计很漂亮,比方能够将一些简单的问题分解成若干小问题,通过管道符和重定向机制灵便地用现成的工具解决,写成 shell 脚本就很高效。
前文写过好几篇 Linux 相干的文章:
- Linux 过程 / 管道 / 重定向 / 文件描述符
本文就分享一下我在实践中应用重定向和管道符遇到的一些坑,搞明确一些底层原理,写脚本的效率能晋升不少。
\> 和 >> 重定向符的坑
先说第一个问题,执行如下命令会产生什么?
$ cat file.txt > file.txt
读取再写入同一个文件,感觉什么也不会产生对吧?
实际上,上述命令运行的后果是清空 file.txt
文件中的内容。
PS:有的 Linux 发行版可能会间接报错,能够执行 cat < file.txt > file.txt
绕开这个检测。
前文 Linux 过程和文件描述符 说过,程序自身没有必要关怀本人的规范输出 / 输入指向哪里,是 shell 通过管道符和重定向符号批改了程序的规范输出 / 输入的地位。
所以执行 cat file.txt > file.txt
这个命令时,shell 会先关上 file.txt
,因为重定向符号是>
,所以文件中的内容会被清空,而后 shell 将cat
命令的规范输入设置为 file.txt
,这时候cat
命令才开始执行。
也就是如下过程:
1、shell 关上 file.txt
并清空其内容。
2、shell 将 cat
命令的规范输入指向 file.txt
文件。
3、shell 执行 cat
命令,读了一个空文件。
4、cat
命令将空字符串写入规范输入(file.txt
文件)。
所以,最初的后果就是 file.txt
变成了空文件。
咱们晓得,>
会清空指标文件,>>
会在指标文件尾部追加内容,那么如果将重定向符 >
改成 >>
会怎么呢?
$ echo hello world > file.txt # 文件中只有一行内容
$ cat file.txt >> file.txt # 这个命令会死循环
file.txt
中首先被写入一行内容,执行 cat file.txt >> file.txt
后预期的后果应该是两行内容。
然而很遗憾,运行后果并不合乎预期,而是会死循环不断向 file.txt
中写入 hello world,文件很快就会变得很大,只能用 Control+C 进行命令。
这就有意思了,为什么会死循环呢?其实稍加剖析就能够想到起因:
首先要回顾 cat
命令的行为,如果只执行 cat
命令,就会从命令行读取键盘输入的内容,每次按下回车,cat
命令就会回显输出,也就是说,cat
命令是逐行读取数据而后输入数据的。
那么,cat file.txt >> file.txt
命令的执行过程如下:
1、关上file.txt
,筹备在文件尾部追加内容。
2、将 cat
命令的规范输入指向 file.txt
文件。
3、cat
命令读取 file.txt
中的一行内容并写入规范输入(追加到 file.txt
文件中)。
4、因为刚写入了一行数据,cat
命令发现 file.txt
中还有能够读取的内容,就会反复步骤 3。
以上过程,就好比一边遍历列表,一遍往列表里追加元素一样,永远遍历不完,所以导致咱们的命令死循环。
\> 重定向符和 | 管道符配合
咱们常常会遇到这样的需要:截取文件的前 XX 行,其余的都删除。
在 Linux 中,head
命令能够实现截取文件前几行的性能:
$ cat file.txt # file.txt 中有五行内容
1
2
3
4
5
$ head -n 2 file.txt # head 命令读取前两行
1
2
$ cat file.txt | head -n 2 # head 也能够读取规范输出
1
2
如果咱们想保留文件的前 2 行,其余的都删除,可能会用如下命令:
$ head -n 2 file.txt > file.txt
然而这就犯了前文说的谬误,最初 file.txt
会被清空,不能实现咱们的需要。
那咱们是这样写命令是否能够避坑呢:
$ cat file.txt | head -n 2 > file.txt
论断是不行,文件内容仍然会被清空。
What?是不是管道漏了,把数据全漏掉了?
前文 Linux 过程和文件描述符 也说过管道符的实现原理,实质上就是将两个命令的规范输出和输入连接起来,让前一个命令的规范输入作为下一个命令的规范输出。
然而,如果你认为这样写命令能够失去预期的后果,那可能是因为你认为管道符连贯的命令是串行执行的,这是一个常见的谬误,实际上 管道符连贯的多个命令是并行执行的。
你可能认为,shell 会先执行 cat file.txt
命令,失常读取 file.txt
中的所有内容,而后把这些内容通过管道传递给 head -n 2 > file.txt
命令。
尽管这时候 file.txt
中的内容会被清空,然而 head
并没有从文件中读取数据,而是从管道读取数据,所以应该能够向 file.txt
正确写入两行数据。
但实际上,上述了解是谬误的,shell 会并行执行管道符连贯的命令,比如说执行如下命令:
$ sleep 5 | sleep 5
shell 会同时启动两个 sleep
过程,所以执行后果是睡眠 5 秒,而不是 10 秒。
这是有点违反直觉的,比方这种常见的命令:
$ cat filename | grep 'pattern'
直觉如同是先执行 cat
命令一次性读取了 filename
中所有的内容,而后传递给 grep
命令进行搜寻。
但实际上是 cat
和grep
命令是同时执行的,之所以能失去预期的后果,是因为 grep 'pattern'
会阻塞期待规范输出,而 cat
通过 Linux 管道向 grep
的规范输出写入数据。
执行上面这个命令能直观感触到 cat
和grep
是在同时执行的,grep
在实时处理咱们用键盘输入的数据:
$ cat | grep 'pattern'
说了这么多,再回顾一开始的问题:
$ cat file.txt | head -n 2 > file.txt
cat
命令和 head
会并行执行,谁先谁后不确定,执行后果也就不确定。
如果 head
命令先于 cat
执行,那么 file.txt
就会被先清空,cat
也就读取不到任何内容;反之,如果 cat
先把文件的内容读取进去,那么能够失去预期的后果。
不过,通过我的试验(将这种并发状况反复 1w 次)发现,file.txt
被清空这种谬误状况呈现的概率远大于预期后果呈现的概率,这个临时还不分明是为什么,应该和 Linux 内核实现过程和管道的逻辑无关。
解决方案
说了这么多管道符和重定向符的特点,如何能力防止这个文件被清空的坑呢?
最靠谱的方法就是不要同时对同一个文件进行读写,而是通过临时文件的形式做一个直达。
比如说只保留 file.txt
文件中的头两行,能够这样写代码:
# 先把数据写入临时文件,而后笼罩原始文件
$ cat file.txt | head -n 2 > temp.txt && mv temp.txt file.txt
这是最简略,最牢靠,十拿九稳的办法。
你如果嫌这段命令太长,也能够通过 apt/brew/yum
等包管理工具装置 moreutils
包,就会多出一个 sponge
命令,像这样应用:
# 先把数据传给 sponge,而后由 sponge 写入原始文件
$ cat file.txt | head -n 2 | sponge file.txt
sponge
这个单词的意思是海绵,挺形象的,它会先把输出的数据「排汇」起来,最初再写入file.txt
,外围思路和咱们应用临时文件时相似的,这个「海绵」就好比一个临时文件,就能够防止同时关上同一个文件进行读写的问题。
以上就是重定向和管道符的一些坑,心愿能帮到你。
查看更多优质算法文章 点击这里,手把手带你刷力扣,致力于把算法讲清楚!我的 算法教程 曾经取得 90k star,欢送点赞!