乐趣区

关于程序员:linux-管道符踩坑指南

我很喜爱 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 命令进行搜寻。

但实际上是 catgrep命令是同时执行的,之所以能失去预期的后果,是因为 grep 'pattern' 会阻塞期待规范输出,而 cat 通过 Linux 管道向 grep 的规范输出写入数据。

执行上面这个命令能直观感触到 catgrep是在同时执行的,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,欢送点赞!

退出移动版