# Linux (七) - 管線命令 (pipe)

在執行指令時有時候會需要下多個指令才能獲取我們想要的資料,很多時候都是因為要先看到前一個指令的輸出再將輸出內容代到下一個指令中。但是這樣就要分很多次執行效率並不高,此外如果想要用 `Shell Script` 執行一連串的指令的話這樣是行不通的。因此這時我們就需要用到 Linux 所支援的管線命令,可以將前一個指令的輸出代入到下一個指令中。
<!-- more -->
## 使用方式
管線命令的使用方式是透過 `|` 這個符號來連結前一個命令的輸出和下一個命令的輸入。如下 :
```bash=
[command1] | [command2] | [command3] | ...
```
所以 `command2` 會處理 `command1` 所傳進來的資料,而 `command3` 會再處理 `command2` 傳進來的資料,以此類推。
👁🗨 這裡要注意的是在 `|` 後面所接的指令要是可以接收 `stdin` 的才行,例如 : less、more。此外管線命令只能接收前一個命令傳入的 `stdout`,會忽略 `stderr`,也就是說只能接收正確的訊息。
`stdin`、`stdout`、`stderr` 稱為標準串流,請參考 [維基百科](https://zh.wikipedia.org/wiki/%E6%A8%99%E6%BA%96%E4%B8%B2%E6%B5%81) 介紹。
**範例**
下面這個範例可以看到由 `ls -l /etc` 和 `less` 兩個指令藉由 `|` 組成,也就是說 `less` 指令會接收 `ls -l /etc` 所輸出的內容再進行處理。所以原本 `ls -l /etc` 單獨執行的話會輸出非常多的檔案詳細資料,但是透過 `less` 去承接 `ls -l /etc` 輸出的內容再顯示就可以前後翻輸出的內容。
```bash=
$ ls -l /etc | less
total 1168
drwxr-xr-x. 3 root root 101 Aug 27 00:55 abrt
-rw-r--r--. 1 root root 16 Aug 27 01:00 adjtime
-rw-r--r--. 1 root root 1529 Apr 1 2020 aliases
-rw-r--r--. 1 root root 12288 Sep 21 02:26 aliases.db
drwxr-xr-x. 2 root root 261 Aug 27 01:03 alternatives
-rw-------. 1 root root 541 Aug 8 2019 anacrontab
-rw-r--r--. 1 root root 55 Aug 8 2019 asound.conf
-rw-r--r--. 1 root root 1 Oct 30 2018 at.deny
drwxr-x---. 3 root root 43 Aug 27 00:56 audisp
drwxr-x---. 3 root root 83 Sep 21 02:25 audit
drwxr-xr-x. 2 root root 148 Oct 13 06:10 bash_completion.d
-rw-r--r--. 1 root root 2853 Apr 1 2020 bashrc
drwxr-xr-x. 2 root root 6 Aug 6 17:30 binfmt.d
-rw-r--r--. 1 root root 37 Apr 7 2020 centos-release
-rw-r--r--. 1 root root 51 Apr 7 2020 centos-release-upstream
drwxr-xr-x. 2 root root 6 Aug 4 2017 chkconfig.d
-rw-r--r--. 1 root root 1142 Aug 27 01:06 chrony.conf
-rw-r-----. 1 root chrony 481 Aug 8 2019 chrony.keys
drwxr-xr-x. 2 root root 26 Aug 27 01:02 cifs-utils
:
```
## 常用搭配指令
### cut
cut 指令可以用於逐行擷取部分欄位或字元。
```bash=
cut [option] [file]
```
option :
* -c : 指定字元 (character),例如 : 2-5 代表第 2 到 第 5 個字元;2-5,7 代表第 2 到第 5 個字元加上第 7 個字元。
* -d : 指定欄位的分隔符號 (delima),可以用於 CSV 等等的檔案取得特定欄位時的分隔。
* -f : 指定欄位 (field),可以搭配 `-d` 來分隔出欄位再指定想要的欄位。指定格式和 `-c` 相同。
* `--complement` : 排除指定的內容,會將 `cut` 指定的內容刪除,留下剩餘的內容。
**範例**
首先先建立一個測試用的 CSV 檔,如下 :
```bash=
$ cat > test.csv
id,name,sex,score
1,tony,male,67
2,simon,male,66
3,mary,female,77
4,lucy,female,89
5,frank,male,98
```
接著對 `test.csv` 進行 `cut` 擷取字元,下面三個分別是擷取字元、擷取欄位及排除指定的內容。
```bash=
$ cut -c 2,5-7 test.csv
dame
,ny,
,mon
,ry,
,cy,
,ank
$ cut -d , -f 1,3-4 test.csv
id,sex,score
1,male,67
2,male,66
3,female,77
4,female,89
5,male,98
$ cut -d , -f 1,3-4 test.csv --complement
name
tony
simon
mary
lucy
frank
```
上面用的範例是直接讀取檔案,這裡我們就換用 `管線命令` 的方式來取得。先使用 `cat` 指令將檔案輸出,接著 `cut` 指令再將輸出進行擷取處理,所以可以看到下面的結果和上面的輸出結果是相同的。
```bash=
$ cat test.csv | cut -c 2,5-7
dame
,ny,
,mon
,ry,
,cy,
,ank
$ cat test.csv | cut -d , -f 1,3-4
id,sex,score
1,male,67
2,male,66
3,female,77
4,female,89
5,male,98
$ cat test.csv | cut -d , -f 1,3-4 --complement
name
tony
simon
mary
lucy
frank
```
### grep
grep 可以從檔案或串流中取得想要尋找的資料並顯示出來,尋找方式可以是關鍵字或是正規表示法 (regular expression)。而找到的的資料會是整行都輸出。
```bash=
grep [option] [match pattern] [file|dir]...
```
option :
* -i、`--ignore-case` : 不分大小寫。
* -n、`--line-number` : 標示與搜尋要求匹配的行號。
* -v、`--invert-match` : 反向匹配,即排除要求的內容,留下不包含要求的內容。
* -r、`--recursive` : 遞迴搜尋指定目錄與其子目錄下的所有檔案。
* -A、`--after-context=NUM` : 指定多輸出匹配資料的後幾行。
* -B、`--before-context=NUM` : 指定多輸出匹配資料的前幾行。
* -C、`--context=NUM` : 指定多輸出匹配資料的前後幾行。
match pattern 可以輸入想要搜尋的條件,可以是關鍵字或是正規表示法。
file|dir 可以是檔案或是目錄,一般情況是用檔案,如果想要搜尋目錄需要搭配 `-r` 使用。
**範例**
我們利用剛剛 `cut` 指令建好的 `test.csv` 來測試,可以看到直接指定關鍵字 `mary` 會將跟 `mary` 有關的資料整行都輸出。
```bash=
$ grep mary test.csv
3,mary,female,77
```
`grep` 指令也可以搭配萬用字元 `*` 來一次搜尋多個檔案,這裡我們將 `test.csv` 複製一個新的 `test2.csv`,接著用萬用字元在多個檔案中搜尋 `mary`,結果如下 :
```bash=
$ cp test.csv test2.csv
$ grep mary *.csv
test.csv:3,mary,female,77
test2.csv:3,mary,female,77
```
接著將 `test.csv` 新增一筆有大寫的資料,我們來測試忽略大小寫的功能。可以看到最後結果會找到包含大小寫的資料。
```bash=
$ cat >> test.csv
6,Mary,female,85
$ grep -i mary test.csv
3,mary,female,77
6,Mary,female,85
```
如果想要輸出行號的話可以加上 `-n`,則就會在輸出的第一行顯示行號,如下 :
```bash=
$ grep -in mary test.csv
4:3,mary,female,77
7:6,Mary,female,85
```
當搜尋的條件是不想出現的就可以加上 `-v` 就會排除指定的條件並輸出剩餘的內容。如下 :
```bash=
$ grep -v mary test.csv
id,name,sex,score
1,tony,male,67
2,simon,male,66
4,lucy,female,89
5,frank,male,98
6,Mary,female,85
```
有些情況可能需要找到某個目錄下的所有檔案中的某個關鍵字,這時就可以用 `-r` 並指定目錄就可以遞迴搜尋這個目錄下的所有檔案。下面這個範例我們先建立兩層測試用的目錄,並將 `test.csv` 複製進去。接著在下 `-r` 搭配目錄去搜尋,可以發現最後找到了兩個不同目錄下的 `test.csv` 裡面都有指定的條件。
```bash=
$ mkdir dir1
$ cp test.csv dir1/test.csv
$ mkdir dir1/dir2
$ cp test.csv dir1/dir2/test.csv
$ grep -r mary dir1
dir1/dir2/test.csv:3,mary,female,77
dir1/test.csv:3,mary,female,77
```
有時候只輸出找到的那一行可能會不太確定是不是真的要找的內容,這時就可以使用 `-ABC` 這三個選項來指定要多輸出前後幾行的資料。如下 :
```bash=
# 多輸出後一行
$ grep -A 1 mary test.csv
3,mary,female,77
4,lucy,female,89
# 多輸出前一行
$ grep -B 1 mary test.csv
2,simon,male,66
3,mary,female,77
# 多輸出前後各一行
$ grep -C 1 mary test.csv
2,simon,male,66
3,mary,female,77
4,lucy,female,89
```
前面有介紹到也可以使用正規表示法來指定搜尋條件,下面這個範例就用正規表示法指定搜尋開頭是 `5` 的資料,如下 :
```bash=
$ grep "^5" test.csv
5,frank,male,98
```
最後我們使用 `管線命令` 來操作 `grep`,可以看到 `grep` 命令將 `ls` 命令的輸出進行處理後再將結果輸出,如下 :
```bash=
$ ls | grep "^t"
test.csv
test2.csv
```
## Summary
本篇介紹了如何使用管線命令將多個命令串在一起,他的方便性是可以直接將前一個命令的輸出交給下一個命令作為輸入,節省自己慢慢一個命令一個命令下還有找到下一個命令要輸入的內容。
還有一些可以使用管線命令的指令,例如 : sort(資料排序)、uniq (顯示不重複資料)、col(過濾反向換行)、sed(內容替換)、join(合併兩個檔案的相同資料)。
## 參考
[1] [管線命令 (pipe)](https://dywang.csie.cyut.edu.tw/moodle23/dywang/linuxProgram/node15.html)
[2] [指令下達行為與基礎檔案管理](http://linux.vbird.org/linux_basic_train/unit02.php)
[3] [Linux 之 Bash -- 管線命令](https://www.twblogs.net/a/5d469b25bd9eee5327fb71fa)
[4] [Linux 的 cut 擷取部份字元、欄位指令教學與常用範例整理](https://blog.gtwang.org/linux/linux-cut-command-tutorial-and-examples/)
[5] [Linux 匹配文字 grep 指令用法教學與範例](https://blog.gtwang.org/linux/linux-grep-command-tutorial-examples/)
[6] [grep命令](https://man.linuxde.net/grep)
[7] [正規表示法與文件格式化處理 | 鳥哥](http://linux.vbird.org/linux_basic/0330regularex.php#basicre)
###### tags: `Linux`