提高工作效率、作業系統最佳化、自動化等是每個IT從業人員所追求的目標。在Linux作業系統中,能夠熟練使用重定向和管道命令列工具是必須掌握的技能之一。本文將透過實例詳解重定向和管道工具的使用方法及原理。
我很喜歡Linux 系統,尤其是Linux 的一些設計很漂亮,例如可以將一些複雜的問題分解成若干小問題,透過管道符和重定向機制靈活地用現成的工具解決,寫成shell 腳本就很高效。
本文就分享我在實務上使用重定向和管道符遇到的一些坑,搞清楚一些底層原理,寫腳本的效率能提升不少。
> 和 >> 重定向符的坑
先說第一個問題,執行以下指令會發生什麼事?
$ cat file.txt > file.txt
讀取再寫入同一個文件,感覺什麼事都不會發生對吧?
實際上,上述指令執行的結果是清空file.txt檔案中的內容。
PS:有的 Linux 發行版可能會直接報錯,可以執行catfile.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,核心思路和我们使用临时文件时类似的,这个「海绵」就好比一个临时文件,就可以避免同时打开同一个文件进行读写的问题。
在Linux操作系统中,重定向和管道是非常有用的命令行工具,可以让我们更好地掌握系统的运行状态和信息。掌握相关技能能够帮助我们更好地进行系统优化和自动化工作,从而更好地提高工作效率。相信通过本文的介绍,读者对重定向和管道的原理和使用方法都有了更为深入的了解。
以上是從入門到精通,學會Linux重定向和管道工具加速你的工作流程!的詳細內容。更多資訊請關注PHP中文網其他相關文章!