Zsh 開發指南(第十三篇 管道和重定向)

陌辭寒發表於2019-02-18

導讀

到目前為止,我們已經大致瞭解了 zsh 的語法特性,可以寫一些功能不復雜的指令碼了。但 shell 指令碼主要的應用場景並不是閉門造車寫獨立的程式,而是和外部環境互動。所以要寫出實用的指令碼,要了解 zsh 如何和外部環境互動。這裡的外部環境包括其他程式、檔案系統、網路等等。本篇主要講管道和重定向,這是和其他程式、檔案系統等互動的基礎。

本文中的命令主要是為了演示管道的用法,在實際指令碼中通常不需要使用這些命令,因為可以用 zsh 程式碼直接實現。另外本系列文章不詳細講任何外部命令的用法,因為相關文件或者書籍特別多。如果看不懂本文的某些內容,可以暫時跳過,基本不影響其餘部分的理解。

管道

管道是類 Unix 系統中的一個比較基礎也特別重要的概念,它用於將一個程式的輸出作為另一個程式的輸入,進而兩個程式的資料可以互通。如果只是使用管道,還是非常簡單易懂的,並不需要了解管道的實現細節。

管道的基本用法:

% ls
git  tmp
# wc -l 功能是計算輸入內容的行數
% ls | wc -l
2複製程式碼

| 即管道,在鍵盤上是主鍵盤區右側 對應的上檔鍵字元。如果只輸入 wc -l,wc 會等待使用者輸入,這時可以輸入字串,然後回車繼續輸入,直到按 ctrl + d 結束輸入。然後 wc 會統計使用者一共輸入了多少行,然後輸出行數。

# 敲 wc -l 回車後,依次按 a 回車 b 回車 ctrl + d
% wc -l
a
b
2複製程式碼

但如果前邊有個管道符號,ls | wc -l,那麼 wc 就不等待使用者輸入了,而是直接將 ls 的結果作為輸入讀取過來,然後統計行數,輸出結果。

關於管道的更多細節

我們再執行一個簡單的例子:

% cat | wc -l

# 檢視 cat 程式開啟的 fd
% ls -l /proc/$(pidof cat)/fd
total 0
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:15 0 -> /dev/pts/1
l-wx------ 1 goreliu goreliu 0 2017-08-30 21:15 1 -> pipe:[2803]
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:15 2 -> /dev/pts/1

# 檢視 wc 程式開啟的 fd
% ls -l /proc/$(pidof wc)/fd
total 0
lr-x------ 1 goreliu goreliu 0 2017-08-30 21:16 0 -> pipe:[2803]
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:16 1 -> /dev/pts/1
lrwx------ 1 goreliu goreliu 0 2017-08-30 21:16 2 -> /dev/pts/1複製程式碼

cat 命令的效果是等待使用者輸入,等使用者輸入一行,它就把這行再輸出來,直到使用者按 ctrl – d。所以 cat | wc -l 也會等待使用者輸入。

我們看下 fd 的指向,/dev/ps1/1 是指向偽終端裝置檔案的,程式就是通過這個來讀取使用者的輸入和輸出自己的內容。0 是標準輸入(即使用者輸入端),1 是標準輸出(即正常情況的輸出端),2 是錯誤輸出(即異常情況的輸出端)。但是 cat 的輸出端指向了 一個管道,並且 wc 的 輸入端指向了一個相同的管道,這代表兩個程式的輸入輸出端是通過管道連線的。這種管道是匿名管道,即只在核心中存在,是沒有對應的檔案路徑的。

重定向

重定向,指的便是 fd 的重定向,管道也是重定向的一種方法。但用得更多的是將程式的 fd 重定向到檔案。

一個最簡單的例子是輸出內容到檔案。

% echo abce > test.txt
% cat test.txt
abce複製程式碼

因為這個用法太常見了,大家可能習以為常了。我們依然來看下更多的細節。

% cat > test.txt

# 在另一個 zsh 中執行
% ls -l /proc/$(pidof cat)/fd
total 0
lrwx------ 1 goreliu goreliu 0 Aug 30 21:43 0 -> /dev/pts/1
l-wx------ 1 goreliu goreliu 0 Aug 30 21:43 1 -> /tmp/test.txt
lrwx------ 1 goreliu goreliu 0 Aug 30 21:43 2 -> /dev/pts/1複製程式碼

可以看到標準輸出已經指向 test.txt 檔案了。

除了標準輸出可以重定向,標準輸入(fd 0),錯誤輸出(fd 2)也都可以。

% touch 0.txt 1.txt 2.txt
% sleep 1000 <0.txt >1.txt 2>2.txt

# 在另一個 zsh 中執行
% ls -l /proc/$(pidof sleep)/fd
total 0
lr-x------ 1 goreliu goreliu 0 Aug 30 21:46 0 -> /tmp/0.txt
l-wx------ 1 goreliu goreliu 0 Aug 30 21:46 1 -> /tmp/1.txt
l-wx------ 1 goreliu goreliu 0 Aug 30 21:46 2 -> /tmp/2.txt複製程式碼

<0.txt 是重定向標準輸入,2>2.txt 是重定向錯誤輸出,>1.txt(即 1>1.txt)是重定向到標準輸出。然後我們看到 3 個檔案已經各就各位,全部被重定向了。但因為 sleep 並不去讀寫任何東西,重定向它的輸入輸出沒有什麼意義。

更多重定向的用法

一個 fd 只能重定向到一個檔案,一一對應。但在 zsh 中,我們可以把一個 fd 對應到多個檔案。

% cat >0.txt >1.txt >2.txt複製程式碼

輸入完成後,3 個檔案的內容都更新了,這是怎麼回事呢?

其實是 zsh 程式做了中介。

% pstree -p | grep cat
        `-tmux: server(1172)-+-zsh(1173)---cat(1307)---zsh(1308)

% ls -l /proc/1307/fd
total 0
lrwx------ 1 goreliu goreliu 0 Aug 30 21:57 0 -> /dev/pts/1
l-wx------ 1 goreliu goreliu 0 Aug 30 21:57 1 -> pipe:[2975]
lrwx------ 1 goreliu goreliu 0 Aug 30 21:57 2 -> /dev/pts/1

% ls -l /proc/1308/fd
total 0
l-wx------ 1 goreliu goreliu 0 Aug 30 21:58 12 -> /tmp/0.txt
l-wx------ 1 goreliu goreliu 0 Aug 30 21:58 13 -> /tmp/1.txt
lr-x------ 1 goreliu goreliu 0 Aug 30 21:58 14 -> pipe:[2975]
l-wx------ 1 goreliu goreliu 0 Aug 30 21:58 15 -> /tmp/2.txt複製程式碼

可以看到 cat 的標準輸出是重定向到管道了,管道對面是 zsh 程式,然後 zsh 開啟了那三個檔案。實際將內容寫入檔案的是 zsh,而不是 cat。但不管是誰寫入的,這個用法很方便。

標準輸入、錯誤輸出也可以重定向多個檔案。

% echo good >0.txt >1.txt >2.txt

% cat <0.txt <1.txt <2.txt
good
good
good複製程式碼

給 cat 的標準輸出重定向 3 個檔案,它將 3 個檔案的內容全部讀取了出來。

除了能同時重定向 fd 到多個檔案外,還可以同時重定向到管道和檔案。

# 敲完 a b c 後 ctrl -d 退出
% cat >0.txt >1.txt | wc -l
a
b
c
3

% cat 0.txt 1.txt
a
b
c
a
b
c複製程式碼

可以看到輸入的內容寫入了檔案,並且通過管道傳給了 wc -l,不用說,這又是 zsh 在做背後工作,將資料分發給了檔案和管道。所以在 zsh 中是不需要使用 tee 命令的。

命名管道

除了匿名管道,我們還可以使用命名管道,這樣更容易控制。命名管道所使用的檔案即 FIFO(First Input First Output,先入先出)檔案。

# mkfifo 用來建立 FIFO 檔案
% mkfifo fifo
% ls -l
prw-r--r-- 1 goreliu goreliu 0 2017-08-30 21:29 fifo|

# cat 寫入 fifo
% cat > fifo

# 開啟另一個 zsh,執行 wc -l 讀取 fifo
% wc -l < fifo複製程式碼

然後在 cat 那邊輸入一些內容,按 ctrl – d 退出,wc 這邊就會統計輸入的行數。

在輸入完成之前,我們也可以看一下 cat 和 wc 兩個程式的 fd 指向哪裡:

% ls -l /proc/$(pidof cat)/fd
total 0
lrwx------ 1 goreliu goreliu 0 Aug 30 21:35 0 -> /dev/pts/2
l-wx------ 1 goreliu goreliu 0 Aug 30 21:35 1 -> /tmp/fifo
lrwx------ 1 goreliu goreliu 0 Aug 30 21:35 2 -> /dev/pts/2

% ls -l /proc/$(pidof wc)/fd
total 0
lr-x------ 1 goreliu goreliu 0 Aug 30 21:34 0 -> /tmp/fifo
lrwx------ 1 goreliu goreliu 0 Aug 30 21:34 1 -> /dev/pts/1
lrwx------ 1 goreliu goreliu 0 Aug 30 21:34 2 -> /dev/pts/1複製程式碼

可以看到之前的匿名管道已經變成了我們剛剛建立的 fifo 檔案,其他的並無不同。

exec 命令的用法

說起重定向,就不得不提 exec 命令。exec 命令主要用於啟動新程式替換當前程式以及對 fd 做一些操作。

用 exec 啟動新程式:

% exec cat複製程式碼

看上去效果和直接執行 cat 差不多。但如果執行 ctrl + d 退出 cat,終端模擬器就關閉了,因為在執行 exec cat 的時候,zsh 程式將已經被 cat 取代了,回不去了。

但在指令碼中很少直接這樣使用 exec,更多情況是用它來操作 fd:

# 將當前 zsh 的錯誤輸出重定向到 test.txt
% exec 2>test.txt
# 隨意敲入一個不存在的命令,錯誤提示不出現了
% fdsafds
# 錯誤提示被重定向到 test.txt 裡
% cat test.txt
zsh: command not found: fdsafds複製程式碼

更多用法:

用法 功能
n>filename 重定向 fd n 的輸出到 filename 檔案
n<filename 重定向 fd n 的輸入為 filename 檔案
n<>filename 同時重定向 fd n 的輸入輸出為 filename 檔案
n>&m 重定向 fd n 的輸出到 fd m
n<&m 重定向 fd n 的輸入為 fd m
n>&- 關閉 fd n 的輸出
n<&- 關閉 fd n 的輸入

更多例子:

# 把錯誤輸出關閉,這樣錯誤內容就不再顯示
% exec 2>&-
% fsdafdsa

% exec 3>test.txt
% echo good >&3
% exec 3>&-
# 關閉後無法再輸出
% echo good >&3
zsh: 3: bad file descriptor

% exec 3>test.txt
# 將 fd 4 的輸出重定向到 fd 3
% exec 4>&3
% echo abcd >&4
# 輸出內容到 fd 4,test.txt 內容更新了
% cat test.txt
abcd複製程式碼

通常情況我們用 exec 主要為了重定向輸出和關閉輸出,比較少操作輸入。

總結

本文講了管道和重定向的基本概念和各種用法。Zsh 中的重定向還是非常靈活好用的,之後的文章會詳細講在實際場景中怎樣使用。

參考

adelphos.blog.51cto.com/2363901/160…

全系列文章地址:github.com/goreliu/zsh…

付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活定價,歡迎諮詢,微信 ly50247。

相關文章