linux-管道pipe与xargs

linux-管道pipe与xargs

Linux CLI shell(如bash,zsh)通常情况下都是每输入一条指令,输出一个结果,一来一回的交互,但是有的时候一条指令的输出是冗长且繁杂的,我们需要对其输出进行再处理,才能找到我们需要的内容。此时,我们就需要像筛金子一样,逐层过滤掉无用的沙子,Linux提供了这样的筛子——管道,使我们能够接续处理数据。

管道——Pipe

管道(Pipeline)操作符为“|”,是一系列将标准输入输出链接起来的进程,其中每一个进程的输出被直接作为下一个进程的输入。管道中的组成元素也被称作过滤程序。这个概念是由道格拉斯·麦克罗伊为Unix 命令行发明的,因与物理上的管道相似而得名。

这是来自Wikipedia的定义。定义中指出,默认情况下,管道只会将上一个程序的标准输出(stdout),传递给下一个命令,作为标准的输入(stdin),对标准错误(stderr)信息没有直接处理能力。最后的命令将会把标准输出和标准错误都输出到屏幕上。画个简图来描述他们的关系:

管道命令示意图

注意:

  1. 管道命令只处理前一个命令正确输出,不处理错误输出。
  2. 管道命令右边命令,必须能够接收标准输入流命令才行。
  3. 管道触发两个子进程分别执行"|"两边的程序;而重定向是在一个进程内执行。
  4. 如果使用|&,则表示命令1的标准错误和标准输出都作为命令2的标准输入,这是2>&1 |的简写。

管道命令

管道需要搭配管道命令来使用,除了最开始的命令,在管道右边的命令,必须能够接收标准输入流命令才行。以下这些命令就是常用的管道命令:

  • 撷取命令: cut, grep
  • 排序命令: sort, uniq, wc
  • 双向重导向: tee
  • 字符转换命令: tr, col, join, paste, expand
  • 分割命令: split
  • 文本查看: cat, tac, more, less, head, tail
  • 文本比较: diff
  • 流文本处理: sed, awk
  • 参数代换: xargs

我们不鼓励现在就了解这些命令的具体用途,因为这样学习是枯燥且低效的。我们更鼓励只有当实际用到的时候再去查找这些命令的用法。

参数代换——xargs

上面这些命令中,有一个需要单独拎出来强调下,就是参数代换——xargs。它的运作方式有少许不同,一般的管道命令都是把上一个命令的标准输出作为本命令的标准输入;而xargs可以将来自管道的输出、标准输入或文件数据转换成命令行参数

我们先举个例子,看看xargs带来的区别:

 1# xargstest目录下有一个test.txt文件
 2$ tree xargstest/
 3xargstest/
 4└── test.txt
 50 directories, 1 file
 6$ cd xargstest/
 7# 直接使用管道命令
 8$ ls | cat
 9test.txt
10# xargs下的管道命令
11$ ls | xargs cat
12测试文件
13xargs的区别实例

首先,我们在xargstest/目录下使用ls命令,结果是显示其目录下的文件test.txt,也就是说标准输出(stdout)是"test.txt"。当我们直接使用管道命令ls | cat时,标准输出"test.txt"作为cat的标准输入(stdin),因此cat的输出结果就是显示标准输入的内容,即"test.txt"。当我们使用参数代换xargs时,前一个命令ls的标准输出作为参数传递给cat,而非标准输入,后一个命令在执行时,实际执行的命令是cat test.txt,因此最终结果是显示test.txt文件中的内容。

一句话总结:xargs命令的作用,是将标准输入转为命令行参数

区分标准输入和参数

大多数时候,xargs命令都是跟管道一起使用的。为了进一步区分标准输入和参数,我们可以看看在不使用管道时xargs的效果。通常境况下,xargs后面跟一个Linux shell命令,来自标准输入的内容都会是该命令的参数。如果我们单独使用xargs,就会使用默认命令echo,即xargs == xargs echo。当我们直接执行xargs后,会出现空行让我们随意输入内容,我们从键盘输入的内容就是标准输入,直到我们使用ctrl+d或者在一行仅输入“End of File”标志(使用-E参数指定)。然后,echo命令就会把前面的输入打印出来。

1$ xargs
2xargs测试 # 按Ctrl + d
3xargs测试 # 输出内容
4$ xargs -E EOF # 指定结束符为EOF
5xargs测试
6EOF
7xargs测试 # 输出内容

可以看出xargs + stdin == echo 'stdin',注意这里我用单引号,表示stdin内容作为echo的参数被执行时是直接输出,不用转义。这就是所说的xargs将标准输入转为命令行参数。如果不好理解,我们再举个使用其他命令的例子:

1$ xargs touch # 打算创建一个文件,文件名由键盘输入
2newfile.txt # 按Ctrl + d
3$ ls -l newfile.txt
4-rw-rw-r-- 1 username username 0 Nov  24 00:43 newfile.txt  # 确实创建了此文件
5$ echo 'newfile.txt' | xargs -t rm #我们可以使用-t参数,显示xargs实际执行的命令。
6rm newfile.txt

这个例子中,来自键盘的标准输入作为touch命令的参数,决定了被新建文件的名称,等同于touch newfile.txt。参数都是跟在命令后面的,比如touchxargs的参数,touch newfile.txtnewfile.txttouch的参数。而标准输入是独立于命令存在的,由用户决定其内容,通常是命令执行后,等待用户提供的,比如执行xargs命令后,等待来自用户键盘的输入内容。

其他xargs命令的参数,可以参考其man帮助页面

关于减号“-”的用途

减号“-”在shell脚本中根据使用上下文的不同,有不同的含义。在管道语境下,减号“-”代表着标准输入或标准输出,用来代替某个文件名参数(在非管道语境下,也可以表示标准输入或标准输出)。需要指出,并不是所有命令都支持减号“-”。

我们先举个做为标准输出的例子。

 1# 默认情况下,此命令会下载一个名为docker的文件,此为安装docker容器的脚本
 2$ wget https://get.daocloud.io/docker
 3--2021-11-24 09:22:21--  https://get.daocloud.io/docker
 4Resolving get.daocloud.io (get.daocloud.io)... 106.75.86.15
 5...... # 中间省略
 6HTTP request sent, awaiting response... 200 OK
 7Length: 18617 (18K) [None]
 8Saving to: ‘docker’ # 注意这里就是保存到文件
 9
10docker                             100%[=============================================================>]  18.18K  --.-KB/s    in 0.03s
11
122021-11-24 09:22:21 (566 KB/s) - ‘docker’ saved [18617/18617]
13$ ls -l docker
14-rw-r--r-- 1 username username 18617 Nov  24 09:22 docker
15# 当我们加上参数"-O"表示目标位置,通常情况下为文件名
16# 我们可使用"-O -"将下载的内容直接输出到标准输出(屏幕),而非文件
17$ wget https://get.daocloud.io/docker -O -
18--2021-11-24 10:03:50--  https://get.daocloud.io/docker
19Resolving get.daocloud.io (get.daocloud.io)... 106.75.86.15
20...... # 中间省略
21HTTP request sent, awaiting response... 200 OK
22Length: 18617 (18K) [None]
23Saving to: ‘STDOUT’ # 注意这里就是保存到标准输出,实际效果为输出到屏幕
24
25-                                    0%[                                                              ]       0  --.-KB/s               #!/bin/sh
26set -e
27# Docker CE for Linux installation script
28#
29# See https://docs.docker.com/engine/install/ for the installation steps.
30#
31...... # 中间省略
32                       echo "ERROR: Unsupported distribution '$lsb_dist'"
33                        echo
34                        exit 1
35                        ;;
36        esac
37        exit 1
38}
39
40# wrapped up in a function so that we have some protection against only getting
41# half the file during "curl | sh"
42do_install
43-                                  100%[=============================================================>]  18.18K  --.-KB/s    in 0.03s
44
452022-01-05 10:03:51 (684 KB/s) - written to stdout [18617/18617]

这里我们使用减号“-”代替“-O”参数指向的文件名,来表示将下载的内容发到标准输出。此时相当于只是在屏幕中输出文件内容,并非下载文件。

而将减号“-”用于标准输入,不仅需要命令的支持,还需要命令能够从标准输入接收参数。典型的例子是diff命令。例子如下:

 1$ echo -e "line 1 \nline 2\nline 3" > minus_1.txt
 2$ cat minus_1.txt
 3line 1
 4line 2
 5line 3
 6$ diff - minus_1.txt # - 表示会从标准输入(键盘)接收内容
 7line 1
 8line 2 changed
 9line 3 # 这里按ctrl+d
101,2c1,2
11< line 1
12< line 2 changed
13---
14> line 1
15> line 2

我们生成了一个三行的文件minus_1.txt,然后使用diff - minus_1.txt让从键盘输入的内容和文件内容比较。

知道了减号作为标准输入输出的用法,我们就可以把它运用到管道中,在管道左边的命令中,减号可以代表标准输出;在管道右边的命令中,减号代表标准输入。例子如下:

1# 将/var/log中的文件打包并压缩为log.zip
2tar -cvPf - /var/log | zip -r log.zip -

管道左边的第一条语句不再输入到具体文件,而输出到stdout中, 而作为第二条件的标准输入(stdin), 而stdin和 stdout都可以用“-”来取代。其实际效果等同于:

1# 将/var/log/中的文件打包到中间文件log.tar
2$ tar -cvPf log.tar /var/log/
3# zip压缩log.tar为log.tar.zip
4$ zip -r log.tar.zip log.tar
5# 删除中间文件log.tar
6$ rm -rf log.tar

使用减号不仅使命令更加简洁,也避免生成中间文件log.tar

参考内容