DomBro Studio

高级bash

2017/12/12

目录

1. 数据流重定向

重定向,英文 redirct。这个词程序员经常能遇到,大概意思就是给一个东西重新定位。而数据流重定向,大概就是将数据传导到其他地方去。

1.1 what

想知道啥叫数据流重定向,就要先了解一下命令执行结果。默认情况下,我们执行一个命令无论正确与否,命令执行结果(如果有的话)信息都会被打印到屏幕上。

1.1.1 标准输出 & 标准错误输出

上面说到,默认情况下一个命令的执行结果都会被输出到屏幕上。输出的结果无非就是两种情况:命令回传的正确信息和命令回传的错误信息。这两种情况就分别对应 标准输出(standard output,简称stdout) 和 标准错误输出(standard error output,简称stderr)

1
2
3
4
5
dombro@ubuntu:~$ cat catfile <== 使用 cat 将文件内容显示
hello <== 文件内容被打印出来,stdout
just test for stin
dombro@ubuntu:~$ cat cat11
cat: cat11: No such file or directory <== 没有这个文件 stderr

也就是说当你命令执行成功时输出的信息就是 stdout ,反之命令执行失败输出的信息就是 stderr 。不管正确与错误的数据都输出到屏幕上时很混乱的,如何将这两者分开就是数据流重定向的功能

1.1.2 数据流重定向功能

数据流重定向可以将 stdout 和 stderr 分别传送到其他文件和设备中而不只是将其输出到屏幕上。而传送所需要的特殊字符如下

1.标准输入(stdin) : 代码为 0,使用 < 或 <<

2.标准输出(stdout) : 代码为 1,使用 > 或 >>

3.标准错误输出(stderr) : 代码为 2,使用 2> 或 2>>

光说不练假把式,通过几个范例练习一下

  • 范例一:将系统根目录下(/) 各个文件信息记录到 ~/rootfile 中
1
2
3
4
5
6
dombro@ubuntu:~$ ll / > ./rootfile <== 神奇的是这是屏幕什么都不会输出
dombro@ubuntu:~$ cat ./rootfile <== 使用 cat 查看一下,果然信息都写入了rootfile 中
total 112
drwxr-xr-x 25 root root 4096 Nov 5 09:47 ./
drwxr-xr-x 25 root root 4096 Nov 5 09:47 ../
...下面省略

范例一中 ~/rootfile 的创建方式是:

1.该文件若不存在系统自动将它创建

2.若文件已经存在,系统会先将这个文件清空,在将数据写入

3.综合上两点,若以 > 输出到一个已存在的文件中,那这个文件就会被覆盖掉

如果想以累加的方式重定向,不删除旧数据,咋办?将 > 改成 >> 就好了。上面说的标准输出代码为1,所以在 > 和 >> 前面加上 数字1 ,1>>、1>起到的效果是一样的。

标准错误输出(stderr)的重定向和 stdout 使用方法是一样的,只不过标准错误输出的重定向字符为 2> 和 2>>

  • 范例二:利用一般用户账号使用 find 命令查找/home 下面是否有 .bashrc的文件存在
1
2
3
4
5
6
dombro@ubuntu:~$ find /home -name .bashrc
/home/dongbo/.bashrc <== stdout
find: ‘/home/dongbo/.cache’: Permission denied <== stderr 这里是错误输出,很明显 dombro 这个用户没有 /home/dongbo 这个目录的读取权限
/home/user2/.bashrc <== stdout
/home/user1/.bashrc <== stdout
/home/dombro/.bashrc <== stdout

这个时候你可以使用 stdout 重定向 find /home -name .bashrc > list 正确的输出数据会被写入到 list 这个文件里,但错误的提示还是会在屏幕上。

  • 范例三:承接范例二,将 stdout 和 stderr 分别存到不同的文件中
1
2
3
4
dombro@ubuntu:~$ find /home -name .bashrc > list_right 2> list_error <== 屏幕不会显示任何信息, 
dombro@ubuntu:~$ ls -al list_right list_error
-rw-rw-r-- 1 dombro dombro 51 Dec 12 10:42 list_error <== stderr 的错误信息写入该文件
-rw-rw-r-- 1 dombro dombro 82 Dec 12 10:42 list_right <== stdout 的正确信息写入该文件
  • 范例四:承接范例三,将错误数据丢弃,屏幕显示正确数据

如果我不想使用文件保存 stderr 返回的错误信息,也不想让其在屏幕上显示 我们可以借助 /dev/null 这个设备 ,你可以把它想象成一个垃圾桶或者黑洞,他可以吃掉任何导向这个设备的信息

1
2
3
4
5
dombro@ubuntu:~$ find /home -name .bashrc 2> /dev/null 
/home/dongbo/.bashrc <== 仅会显示 stdout
/home/user2/.bashrc
/home/user1/.bashrc
/home/dombro/.bashrc

使用这种写法是因为你提前知道会出现哪些 stderr 的错误输出。

  • 范例五:将命令的返回数据全部写入名为list_show的文件中

我猜你肯定会这么写

1
2
3
4
5
6
dombro@ubuntu:~$ find /home -name .bashrc > list_show 2> list_show
dombro@ubuntu:~$ cat list_show
find: ‘/home/dongbo/home/user2/.bashrc
/home/user1/.bashrc <== 有没有注意到正确和错误的顺序怪怪的
/home/dombro/.bashrc
...已经显示全部了...

这是一种错误的写法!虽然 list_show 这个文件会创建以及被写入,但是 stdout 和 stderr 可能会交替写入,数据可能会丢失。

正确的两种写法

1
2
dombro@ubuntu:~$ find /home -name .bashrc > list_show 2>&1 <== 推荐写法,因为予以更加清晰
dombro@ubuntu:~$ find /home -name .bashrc &> list_show

1.1.3 标准输入

说了标准输出与标准错误输出应该看一下 标准输入stdin 了。stdin 重定向字符为 < 或 << ,这两个字符可以将原本需要由键盘输入的数据改由文件内容来代替。

  • 范例六:利用 cat 命令创建一个文件的简单流程

what ? cat 创建文件?当然可以

1
2
3
4
5
6
7
8
dombro@ubuntu:~$ cat > catfile
testing
cat file test
<== 这里按下 [ctrl] + d离开

dombro@ubuntu:~$ cat catfile
testing
cat file test

加入 > 在 cat后,所以那个catfile会被主动创建,内容就是键盘上的输入。

  • 范例七:承接范例六,用 stdin 替代键盘输入已创建新文件简单流程
1
2
3
4
dombro@ubuntu:~$ cat > catfile2 < catfile
dombro@ubuntu:~$ ls -al catfile catfile2
-rw-rw-r-- 1 dombro dombro 23 Dec 12 11:13 catfile
-rw-rw-r-- 1 dombro dombro 23 Dec 12 11:20 catfile2

这两个文件大小一模一样,就像是使用 cp 复制一样。

  • 范例八: stdin 的 <<
1
2
3
4
5
6
7
dombro@ubuntu:~$ cat > catfile << "end"
> This is a test
> now stop
> end <== 输入这个关键字,立刻结束不需要输入 [ctrl]+d
dombro@ubuntu:~$ cat catfile
This is a test
now stop

可以看到 << 表示结束输入的意思,利用 << 右侧的控制字符,我们可以终止一次输入,而不必使用 [ctrl]+d 来结束。

1.2 why

为什么要使用命令重定向呢?来说一说重定向的使用场景

1.屏幕输出的信息很重要,而且我们需要将它存下来

2.后台执行中的程序,不希望他干扰屏幕上的正常输出结果是

3.一些系统的执行命令的执行结果,希望它可以存下来

4.一些执行命令的可能已知错误信息时,可以用 “2>/dev/null”将它丢掉

5.错误信息与正确信息需要分别输出时

2. 命令执行的判断依据 ;、&&、||

科技生产力的进步离不开一个字——“懒”。如果我希望一次性执行输入的很多命令。咋办?一种是通过 shell script 脚本,另一种就是题目中的三种符号 ; && ||。

2.1 ;

有时候我们希望可以一次执行多个命令,可以写成这样

1
cmd;cmd

上述方法在执行第一个命令后会立即执行第二个命令。常用的是关机操作,通过连续的sync将内存缓存写入磁盘后,在关机。

1
dombro@ubuntu:~$ sync;sync;shutdown -h now

但是 ; 两端的命令并不能体现命令的相关性(联系),即前一个命令的执行结果与后一个命令没啥关系,后一个命令都会执行。如果想达到通过前一个命令执行结果决定后一个命令是否执行,可以通过 && 或 || 。

2.2 &&、||

看一下下面的表格

命令执行情况 说明
cmd1 && cmd2 若 cmd1 执行完毕且正确执行($?=0),则开始执行cmd2;若 cmd1 执行完毕且错误执行($?!=0),则cmd2不执行
cmd1丨丨 cmd2 若 cmd1 执行完毕且正确执行($?=0),则cmd2不执行; cmd1 执行完毕且错误执行($?!=0),则开始执行cmd2

这两个字符挺考验逻辑的,但也挺好理解。

  • 范例九:用 ls 测试/tmp/testing 是否存在,若存在则显示 “exit”,若不存在则显示 “not exit”
1
2
dombro@ubuntu:~$ ls -al /tmp/testingn 2>/dev/null  && echo "exit" || echo "not exit"
not exit

我们知道命令的执行顺序是从左到右,范例九当第一个命令执行执行失败,则导致第二个命令执行失败,第三个命令就会执行成功。这是一个很重要的点,例如下面给出错误示范

错误示范

1
2
3
dombro@ubuntu:~$ ls -al /tmp/testingn 2>/dev/null  || echo "not exit" && echo " exit"  <== 将两个信息都打印出来了
not exit
exit

当文件不存在时,第二个命令会执行,而第三个命令则根据第二个命令执行成功也会执行。

3. 管道命令

你用过 ps -aux|grep xxx 这个命令去查找进程吗?用过的话,恭喜你你已经接触过管道命令了。

3.1 what

那么啥是管道命令呢?简单来说就是前一个命令的输出(stdout),作为后一个命令的输入(stdin),命令之间用 | 隔开

  • 举个例子
1
dombro@ubuntu:~$ ls -al /etc | less

如上,由于 /etc 路径下文件太多了,如果直接 ls 屏幕就会被塞满了不知道前面输出了什么,于是利用 less 命令来协助,这样 ls 命令输出的内容就能被less读取,并且利用less功能,可以前后翻动相关信息。很方便~

  • 了解一下 |

这个孤零零的命令 | 仅能处理经过前一个命令传来的正确信息,也就是 standard output 的信息,对于standard error并没有直接处理能力

在每个管道后接的第一个数据一定要是一个命令!而且这个命令一定要能够接受 standard input 数据才行,这样的命令才是管道命令。如 less、more、head、tail 等都是可以接受 stdin 的管道命令,而 ls、cp、mv等就不是管道命令了,因为他们并不会接收来自stdin的数据。

3.2 一些管道命令

选取命令: cut,grep

选取命令就是将一段数据经过分析后,取出我们所需要的,或者经由分析关键字,取得我们所想要的那一行。不过,一般来说,选取的信息通常是针对 “行” 来分析,并不是整片信息分析。

  • cut

cut 切 的意思,没有错,这个命令可以将一段信息的某一段切出来,处理信息是以行为单位的。

1
2
3
4
5
6
7
8
语法:
cut(选项)(参数)

选项:

-c:仅显示行中指定范围的字符 <== 用于排列整齐的信息
-d:指定字段的分隔符,与 -f 一起使用 <== 用于分割字符
-f:一句 -d 的分割字符将一段信息切割成位数段,用 -f 取出第几段的意思

范例一:将 PATH 变量取出,找出第五个路径

1
2
3
4
5
dombro@ubuntu:~$ echo $PATH
/usr/java/jdk1.8.0_144/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

dombro@ubuntu:~$ echo $PATH |cut -d':' -f 5
/usr/bin

如果想列出第3到第5段

1
2
dombro@ubuntu:~$ echo $PATH |cut -d':' -f 3,5
/usr/local/bin:/usr/bin

范例二:将 exoprt 输出的信息取得第12字符以后的所有字符串

1
2
3
4
5
6
7
8
9
10
dombro@ubuntu:~$ export
declare -x CLASSPATH=".:/usr/java/jdk1.8.0_144/lib:/usr/java/jdk1.8.0_144/jre/lib"
declare -x CLUTTER_IM_MODULE="xim"
declare -x COMPIZ_CONFIG_PROFILE="ubuntu"
...省略...

dombro@ubuntu:~$ export | cut -c 12-
CLASSPATH=".:/usr/java/jdk1.8.0_144/lib:/usr/java/jdk1.8.0_144/jre/lib"
CLUTTER_IM_MODULE="xim"
COMPIZ_CONFIG_PROFILE="ubuntu"

如果想得到 第12-20 的字符,使用 cut -c 12-20 即可。

  • grep

这个命令其实很常见的,grep 实际上就是分析一行信息,若当中有我们需要的信息,就将该行拿出来。

1
2
3
4
5
6
7
8
grep [-acinv] [--color=auto] '搜尋字串' filename
選項與參數:
-a :將 binary 檔案以 text 檔案的方式搜尋資料
-c :計算找到 '搜尋字串' 的次數
-i :忽略大小寫的不同,所以大小寫視為相同
-n :順便輸出行號
-v :反向選擇,亦即顯示出沒有 '搜尋字串' 內容的那一行!
--color=auto :可以將找到的關鍵字部分加上顏色的顯示喔!

范例三:将 last 中,有出现 root 的那一行取出来

last 命令可以显示系统用户登录的姓名信息

1
2

dombro@ubuntu:~$ last | grep 'root'

范例四:将 last 中,没有出现 root 的行取出来

1
dombro@ubuntu:~$ last | grep -v 'root'

3.2.2 排序命令: sort、wc、uniq

  • sort

sort 是一个很有趣的命令,顾名思义,它可以帮助我们排序,还可以根据不同资料形态来排序!

1
2
3
4
5
6
7
8
9
10
11
sort [-fbMnrtuk] [file or stdin]

选项:
-f :忽略大小写的差异,例如 A 與 a 視为編碼相同;
-b :忽略最前面的空白字元部分;
-M :以月份的名字來排序,例如 JAN, DEC 等等的排序方法;
-n :使用『純數字』進行排序(預設是以文字型態來排序的);
-r :反向排序;
-u :就是 uniq ,相同的資料中,僅出現一行代表;
-t :分隔符號,預設是用 [tab] 鍵來分隔;
-k :以那個區間 (field) 來進行排序的意思

范例一:对记录个人账号的 /etc/password 中的账号排序

1
2
3
4
5
dombro@ubuntu:~$ cat /etc/passwd | sort 
_apt:x:105:65534::/nonexistent:/bin/false
avahi-autoipd:x:110:119:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin

可以看到,不加选项参数时,是以第一个字符来排序的

范例二:承接范例一,以第三个含后面字符来排序

1
2
3
4
5
6
7
dombro@ubuntu:~$ cat /etc/passwd | sort -t':' -k 3
root:x:0:0:root:/root:/bin/bash
dombro:x:1000:1000:ubuntu,,,:/home/dombro:/bin/bash
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
dongbo:x:1001:1002:,,,:/home/dongbo:/bin/bash
user1:x:1002:1003::/home/user1:
user2:x:1003:1004::/home/user2:

注意第三个 : 后面是数字开始,所以当然会从 0 开始排序。如果单纯想要第三个栏位来处理则是:

1
cat /etc/passwd | sort -t':' -k 3,3

如果想以数字顺序排序则是:

1
cat /etc/passwd | sort -t':' -k 3 -n <== 加上 -n 选项即可
  • uniq

如果使用 sort 排序完成,想要重复的资料仅列出一个就使用 uniq 这个命令

1
2
3
4
uniq [-ic]
選項與參數:
-i :忽略大小寫字元的不同;
-c :進行計數

范例一:使用 last 将账号列出,进去出账号栏,进行排序后仅取出一位

1
last | cut -d ' ' -f1 | sort | uniq

范例二:承接范例一,列出每个用户登录的次数

1
last | cut -d ' ' -f1 | sort | uniq -c <== 使用 -c 选项列出行数即可
  • wc

wc 命令可以帮助我们知道一个文件中有多少字,多少行,多少字符。

1
2
3
4
5
wc [-lwm]
選項與參數:
-l :仅列出行;
-w :仅列出多少字(英文單字);
-m :多少字符;

范例一:列出 /etc/passwd 有多少字符

1
2
dombro@ubuntu:~$ cat /etc/passwd | wc
43 <== 行 72 <== 字数 2353 <== 字符数

范例二:如何以一行命令串取得这个月份登录系统的总人次

1
dombro@ubuntu:~$ last | grep [a-zA-Z] | grep -v 'wtmp'|wc -l

首先你要知道 last 会输出空白行与wtmp字样在最后两行,这些不包含用户登录信息。于是:grep [a-zA-Z] 取出非空行,grep -v ‘wtmp’ 去掉 wtmp 那两行,在 wc -l 计算行数。

3.2.3 双向重定向:tee

双向重定向?天哪这又是啥?上面重定向的部分 说到 > 会将数据流整个传送给文件或设备,因此除非我们去读取这个文件或设备,否则就无法继续利用这个数据流。往往有时需要在屏幕显示输出的数据,但还要保存数据流处理的过程,那该怎么办?就可以用到我们的双重重定向命令 tee

  • tee

tee 会同时将数据流传送给屏幕和文件

1
2
3
4
tee [-a] file

选项:-a
已累加的方式将数据加入file中
  • 范例一 : 使用双重重定向例子
1
2
3
4
5
6
7
8
9
10
dombro@ubuntu:~$ ls -al | tee ls_file|grep newfile 
-rwxr-xr-x 1 dombro dombro 27 Aug 29 20:49 newfile
dombro@ubuntu:~$ cat ls_file <== 查看一下
total 184
drwxr-xr-x 19 dombro dombro 4096 Dec 12 11:13 .
drwxr-xr-x 6 root root 4096 Nov 13 14:04 ..
-rw-rwxr-- 1 dombro dombro 0 Aug 27 10:38 at_example.txt
-rw------- 1 dombro dombro 10455 Dec 12 14:03 .bash_history
-rw-r--r-- 1 dombro dombro 220 Aug 13 17:08 .bash_logout
-rw-r--r-- 1 dombro dombro 3771 Aug 13 17:08 .bashrc

如果想已累加的方式将数据流存入文件 tee -a file 即可。
tee 可以让 stdout 转存一份到文件内并将同样的数据继续送到屏幕去处理。这样除了可以让我们同时分析一份数据并记录下来之外,还可以作为处理一份数据的中间暂存盘记录。

3.2.4 字符转换命令: tr、col、join、paste、expand

下面介绍几个与字符转换相关的管道命令。

  • tr

tr 用来删除一段信息当中的文字,或者是进行文字信息的转换。

1
2
3
4
5
6
tr [-ds] SET1

选项:

-d : 删除信息中 SET1 这个字符串
-s : 替换重复的字符

范例一:将 ls 出的文件信息,全部变成大写

1
2
3
4
5
6
7
dombro@ubuntu:~$ ls -al | tr [a-z] [A-Z]
TOTAL 188
DRWXR-XR-X 19 DOMBRO DOMBRO 4096 DEC 13 13:40 .
DRWXR-XR-X 6 ROOT ROOT 4096 NOV 13 14:04 ..
-RW-RWXR-- 1 DOMBRO DOMBRO 0 AUG 27 10:38 AT_EXAMPLE.TXT
-RW------- 1 DOMBRO DOMBRO 10455 DEC 12 14:03 .BASH_HISTORY
...下面省略...

范例二:将 ls 出的文件信息中的空格去掉

1
2
3
4
5
6
7
dombro@ubuntu:~$ ls -al | tr -d ' '
total188
drwxr-xr-x19dombrodombro4096Dec1313:40.
drwxr-xr-x6rootroot4096Nov1314:04..
-rw-rwxr--1dombrodombro0Aug2710:38at_example.txt
-rw-------1dombrodombro10455Dec1214:03.bash_history
...下面省略...

范例三:将 /etc/passwd 转存成 dos 断行到 /root/passwd 中,再将 ^M 符号删除

1
2
3
4
5
6
7
8
9
10
11
[root@www ~]# cp /etc/passwd /root/passwd && UNIX2dos /root/passwd
[root@www ~]# file /etc/passwd /root/passwd
/etc/passwd: ASCII text
/root/passwd: ASCII text, with CRLF line terminators <==就是 DOS 断行
[root@www ~]# cat /root/passwd | tr -d '\r' > /root/passwd.linux
# 那个 \r 指的是 DOS 的断行字符,关于更多的字符,请参考 man tr
[root@www ~]# ll /etc/passwd /root/passwd*
-rw-r--r-- 1 root root 1986 Feb 6 17:55 /etc/passwd
-rw-r--r-- 1 root root 2030 Feb 7 15:55 /root/passwd
-rw-r--r-- 1 root root 1986 Feb 7 15:57 /root/passwd.linux
# 处理过后,发现文件大小与原本的 /etc/passwd 就一致了

  • col
1
2
3
4
5
col [-xb]

参数:
-x : 将 tab 键转换成对等的空格键
-b : 在文字内有反斜杠(/) ,仅保留反斜杠最后接的那个字符

范例一:利用 cat -A 显示出所有特殊按键,最后以 col 将 [tab] 转成空白

1
2
3
[root@www ~]# cat -A /etc/man.config <==此时会看到很多 ^I 的符号,那就是 tab
[root@www ~]# cat /etc/man.config | col -x | cat -A | more
#如此一来,[tab] 按键会被替换成为空格键,输出就美观多了。

范例二:将 col 的 man page 转存成为 /root/col.man 的纯文本文件

1
2
3
4
5
6
7
8
9
10
[root@www ~]# man col > /root/col.man
[root@www ~]# vi /root/col.man
COL(1) BSD General Commands Manual COL(1)
N^HNA^HAM^HME^HE
c^Hco^Hol^Hl - filter reverse line feeds from input
S^HSY^HYN^HNO^HOP^HPS^HSI^HIS^HS
c^Hco^Hol^Hl [-^H-b^Hbf^Hfp^Hpx^Hx] [-^H-l^Hl _^Hn_^Hu_^Hm]
# 你没看错。由于 man page 内有些特殊按钮会用来作为类似特殊按键与颜色显示,
# 所以这个文件内就会出现如上所示的一堆怪异字符(有 ^ 的)
[root@www ~]# man col | col -b > /root/col.man
  • join

join 看字面上的意义(加入/参加)就可以知道,它是在处理两个文件之间的数据,而且,主要是将两个文件当中有相同数据的那一行加在一起。我们利用下面的简单例子来说明:

1
2
3
4
5
6
7
[root@www ~]# join [-ti12] file1 file2
参数:
-t :join 默认以空格符分隔数据,并且对比“第一个字段”的数据,
如果两个文件相同,则将两条数据连成一行,且第一个字段放在第一个;
-i :忽略大小写的差异;
-1 :这个是数字的 1 ,代表第一个文件要用哪个字段来分析的意思;
-2 :代表第二个文件要用哪个字段来分析的意思。
  • paste

这个paste就要比join简单多了。相对于join必须要对比两个文件的数据相关性,paste就直接将两行贴在一起,且中间以[tab]键隔开而已。简单的使用方法如下:

1
2
3
4
[root@www ~]# paste [-d] file1 file2
参数:
-d :后面可以接分隔字符,默认是以 [tab] 来分隔的。
- :如果 file 部分写成 - ,表示来自 standard input 的数据的意思。
  • expand
1
2
3
4
就是将[tab]按键转成空格键,可以这样做:
[root@www ~]# expand [-t] file
参数:
-t :后面可以接数字。一般来说,一个[tab]按键可以用 8 个空格键替换。
CATALOG
  1. 1. 目录
  2. 2. 1. 数据流重定向
    1. 2.1. 1.1 what
      1. 2.1.1. 1.1.1 标准输出 & 标准错误输出
      2. 2.1.2. 1.1.2 数据流重定向功能
      3. 2.1.3. 1.1.3 标准输入
    2. 2.2. 1.2 why
  3. 3. 2. 命令执行的判断依据 ;、&&、||
    1. 3.1. 2.1 ;
    2. 3.2. 2.2 &&、||
  4. 4. 3. 管道命令
    1. 4.1. 3.1 what
    2. 4.2. 3.2 一些管道命令
      1. 4.2.1. 选取命令: cut,grep
      2. 4.2.2. 3.2.3 双向重定向:tee
      3. 4.2.3. 3.2.4 字符转换命令: tr、col、join、paste、expand