# 練習 8:更多的重定向和過濾:`head`,`tail`,`awk`,`grep`,`sed`
> 原文:[Exercise 8. Bash: more on redirection and filtering: head, tail, awk, grep, sed](https://archive.fo/JH46V)
> 譯者:[飛龍](https://github.com/wizardforcel)
> 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
現在你試過了 Linux,我會介紹一下 Unix 的方式。注意看。
> 這就是 Unix 的哲學:寫一些程序,只做一件事,并且把它做好。編寫程序,使其一起工作。編寫程序來處理文本流,因為這是一個通用接口。
實際上這意味著為了熟練使用 Linux,你需要知道如何從一個程序中獲取輸出,并將其提供給另一個程序,通常會在此過程中修改它。通常,你可以通過使用管道,將多個程序合并在一起,它允許將一個程序的輸出連接到另一個程序。像這樣:

這里發生的事情真的很簡單。幾乎每個 Linux 程序在啟動時打開這三個文件:
`stdin` - 標準輸入。這是程序讀取東西的地方。
`stdout` - 標準輸出。這是程序寫出東西的地方。
`stderr` - 標準錯誤。這是程序報錯的地方。
這就是它的讀取方式:
```
啟動程序 1
開始從鍵盤讀取數據
開始向顯示器寫出錯誤
啟動程序 2
開始從程序 1 讀取輸入
開始向顯示器寫出錯誤
啟動程序 3
開始從程序 2 讀取輸入
開始向顯示器寫出錯誤
開始向顯示器寫出數據
```
還有另一種方式來描繪發生的事情,如果你喜歡 South Park 風格的幽默,但要小心:看到的是不會是不可見的。
讓我們考慮以下管道,它接受`ls -al`的輸出,僅打印文件名和文件修改時間:
```
ls -al | tr -s ' ' | cut -d ' ' -f 8,9
```
這是所發生事情的概述:
```
啟動 ls -al
獲取當前目錄中的文件列表
向顯示器寫出錯誤
向管道寫出輸出
啟動 tr -s ' '
通過管道從 ls -al 讀取輸入
兩個字段之間只保留一個空格
向顯示器寫出錯誤
向管道寫出輸出
啟動 cut -d ' ' -f 8,9
通過管道從 tr -s ' ' 讀取輸入
只保留字段 8 和 9,扔掉其它東西
向顯示器寫出錯誤
向顯示器寫出輸出
```
更詳細地說,這是每一步發生的事情:
第一步:`ls -al`,我們獲取了目錄列表,每一列都叫做字段。
```
user1@vm1:~$ ls -al
total 52
drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 .
drwxr-xr-x 3 root root 4096 Jun 6 21:49 ..
-rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout
-rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc
-rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt
-rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst
-rw-r--r-- 1 user1 user1 634 Jun 15 20:03 ls.out
-rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile
-rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak
-rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1
-rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo
```
第二步:`ls -al | tr -s ' '`,我們在兩個字段之間只保留,因為`cut`不能將多個空格理解為一種方式,來分離多個字段。
```
user1@vm1:~$ ls -al | tr -s ' '
total 52
drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 .
drwxr-xr-x 3 root root 4096 Jun 6 21:49 ..
-rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout
-rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc
-rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt
-rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst
-rw-r--r-- 1 user1 user1 634 Jun 15 20:03 ls.out
-rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile
-rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak
-rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1
-rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo
```
第三步:我們只保留字段 8 和 9,它們是我們想要的。
```
user1@vm1:~$ ls -al | tr -s ' ' | cut -d ' ' -f 8,9
14:16 .
21:49 ..
19:34 .bash_history
21:48 .bash_logout
12:24 .bashrc
14:16 hello.txt
16:26 .lesshst
20:03 ls.out
12:25 .profile
12:19 .profile.bak
13:12 .profile.bak1
14:16 .viminfo
```
現在你學到了,如何從一個程序獲取輸入,并將其傳給另一個程序,并且如何轉換它。
## 這樣做
```
1: ls -al | head -n 5
2: ls -al | tail -n 5
3: ls -al | awk '{print $8, $9}'
4: ls -al | awk '{print $9, $8}'
5: ls -al | awk '{printf "%-20.20s %s\n",$9, $8}'
6: ls -al | grep bash
7: ls -al > ls.out
8: cat ls.out
9: cat ls.out | sed 's/bash/I replace this!!!/g'
```
### 你會看到什么
```
user1@vm1:~$ ls -al | head -n 5
total 52
drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 .
drwxr-xr-x 3 root root 4096 Jun 6 21:49 ..
-rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout
user1@vm1:~$ ls -al | tail -n 5
-rw-r--r-- 1 user1 user1 636 Jun 18 17:52 ls.out
-rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile
-rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak
-rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1
-rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo
user1@vm1:~$ ls -al | awk '{print $8, $9}'
14:16 .
21:49 ..
19:34 .bash_history
21:48 .bash_logout
12:24 .bashrc
14:16 hello.txt
16:26 .lesshst
17:52 ls.out
12:25 .profile
12:19 .profile.bak
13:12 .profile.bak1
14:16 .viminfo
user1@vm1:~$ ls -al | awk '{print $9, $8}'
. 14:16
.. 21:49
.bash_history 19:34
.bash_logout 21:48
.bashrc 12:24
hello.txt 14:16
.lesshst 16:26
ls.out 17:52
.profile 12:25
.profile.bak 12:19
.profile.bak1 13:12
.viminfo 14:16
user1@vm1:~$ ls -al | awk '{printf "%-20.20s %s\n",$9, $8}'
. 14:16
.. 21:49
.bash_history 19:34
.bash_logout 21:48
.bashrc 12:24
hello.txt 14:16
.lesshst 16:26
ls.out 17:52
.profile 12:25
.profile.bak 12:19
.profile.bak1 13:12
.viminfo 14:16
user1@vm1:~$ ls -al | grep bash
-rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout
-rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc
user1@vm1:~$ ls -al > ls.out
user1@vm1:~$ cat ls.out
total 48
drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 .
drwxr-xr-x 3 root root 4096 Jun 6 21:49 ..
-rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout
-rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc
-rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt
-rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst
-rw-r--r-- 1 user1 user1 0 Jun 18 17:53 ls.out
-rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile
-rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak
-rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1
-rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo
user1@vm1:~$ cat ls.out | sed 's/bash/I replace this!!!/g'
total 48
drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 .
drwxr-xr-x 3 root root 4096 Jun 6 21:49 ..
-rw------- 1 user1 user1 4865 Jun 15 19:34 .I replace this!!!_history
-rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .I replace this!!!_logout
-rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .I replace this!!!rc
-rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt
-rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst
-rw-r--r-- 1 user1 user1 0 Jun 18 17:53 ls.out
-rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile
-rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak
-rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1
-rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo
```
## 解釋
+ 只打印目錄列表中的前 5 個條目。
+ 只打印目錄列表中的后 5 個條目。
+ 只打印修改時間和文件名。注意我如何使用`awk`,這比`cut`更聰明。這里的區別就是,`cut`只能將單個符號(我們這里是空格)理解為一種方式,來分離字段(字段分隔符),`awk`將任意數量的空格和 TAB 看做文件分隔符,所以沒有必要使用`tr`來消除不必要的空格。
+ 按此順序打印文件名和修改時間。這又是`cat`不能做的事情。
+ 工整地打印文件名和修改時間。注意現在輸出如何變得更清晰。
+ 僅打印目錄列表中包含`bash`的行。
+ 將目錄列表的輸出寫入文件`ls.out`。
+ 打印出`ls.out`。`cat`是最簡單的可用程序,允許你打印出一個文件,沒有更多了。盡管如此簡單,但在構建復雜管道時非常有用。
+ 打印出`ls.out`,將所有的`bash`條目替換為`I replace this!!!`。`sed`是一個強大的流編輯器,它非常非常非常有用。
## 附加題
+ 打開`head`,`tail`,`awk`, `grep`和`sed`的手冊頁。不要害怕,只要記住手冊頁面總是在那里。有了一些實踐,你將能夠實際了解他們。
+ 查找`grep`選項,能夠打印它找到的那行之前,或之后的一行。
+ 使用 Google 搜索`awk printf`命令,嘗試了解它如何工作。
+ 閱讀 [The Useless Use of Cat Award](https://archive.fo/9Zcyu)。嘗試那里的一些例子。
- 笨辦法學 Linux 中文版
- 練習 0:起步
- 練習 1:文本編輯器,vim
- 練習 2:文本瀏覽器,少即是多
- 練習 3:Bash:Shell、.profile、.bashrc、.bash_history
- 練習 4:Bash:處理文件,pwd,ls,cp,mv,rm,touch
- 練習 5:Bash:環境變量,env,set,export
- 練習 6:Bash:語言設置,LANG,locale,dpkg-reconfigure locales
- 練習 7:Bash:重定向,stdin,stdout,stderr,<,>,>>,|,tee,pv
- 練習 8:更多的重定向和過濾:head,tail,awk,grep,sed
- 練習 9:Bash:任務控制,jobs,fg
- 練習 10:Bash:程序退出代碼(返回狀態)
- 練習 11:總結
- 練習 12:文檔:man,info
- 練習 13:文檔:Google
- 練習 14:包管理:Debian 包管理工具aptitude
- 練習 15:系統啟動:運行級別,/etc/init.d,rcconf,update-rc.d
- 練習 16:處理進程,ps,kill
- 練習 17:任務調度:cron,at
- 練習 18:日志:/var/log,rsyslog,logger
- 練習 19:文件系統:掛載,mount,/etc/fstab
- 練習 20:文件系統:修改和創建文件系統,tune2fs,mkfs
- 練習 21:文件系統:修改根目錄,chroot
- 練習 22:文件系統:移動數據,tar,dd
- 練習 23:文件系統:權限,chown,chmod,umask
- 練習 24:接口配置,ifconfig,netstat,iproute2,ss,route
- 練習 25:網絡:配置文件,/etc/network/interfaces
- 練習 26:網絡:封包過濾配置,iptables
- 練習 27:安全 Shell,ssh,sshd,scp
- 練習 28:性能:獲取性能情況,uptime,free,top
- 練習 29:內核:內核消息,dmesg
- 練習 30:打磨、洗練、重復:總復習
- 下一步做什么
- Debian 手動安裝