【Linux】Shell - 多命令执行、管道(pipeline)和重定向(redirection)

Posted by 西维蜀黍 on 2019-09-17, Last Modified on 2024-05-02

list - 多命令之间无数据共享

; - 按顺序执行多个命令(无论命令是否执行成功)

Commands separated by a ; are executed sequentially; the shell waits for each command to terminate in turn. The return status is the exit status of the last command executed.

$ <command 1>; <command 2>

分号(;)是一个命令的结束符,使用分号可以在一行中执行多个命令:当前一个命令执行结束后,再执行下一个命令。

注意,使用分号时,下一个命令总是接着前一个命令执行完后就一定会执行(不管前一个命令执行成功或失败)。

而且,每一个命令都会作为当前 shell 的子进程来被执行。

$ echo "--";ll;echo "---"
--
total 16
-rw-r--r--@ 1 weishi  wheel     5B 17 Sep 11:00 1
-rw-r--r--@ 1 weishi  wheel    50B 17 Sep 11:08 2
---
$ clear; ls

上面例子中,Bash 先执行 clear 命令,执行完成后,再执行 ls 命令。

$ cat filelist.txt; ls -l filelist.txt

上面例子中,只要cat命令执行结束,不管成功或失败,都会继续执行ls命令。

&& - 逻辑与

The control operators && and || denote AND lists and OR lists, respectively. An AND list has the form command1 && command2

command2 is executed if, and only if, command1 returns an exit status of zero.

$ <command 1> && <command 2>

按顺序执行,且如果前一个的命令执行发生错误,后一个命令则不会被执行。

或者说,如果Command1命令运行成功,则继续运行Command2命令。

当后面的命令需要前面的命令正确执行做支持时(如环境搭建),可以用这种方式免去等待输入。

$ cat filelist.txt && ls -l filelist.txt

上面例子中,只有cat命令执行成功,才会继续执行ls命令。如果cat执行失败(比如不存在文件flielist.txt),那么ls命令就不会执行。

|| - 逻辑或

An OR list has the form command1 || command2

command2 is executed if and only if command1 returns a non-zero exit status. The return status of AND and OR lists is the exit status of the last command executed in the list.

$ <command 1> || <command 2>

按顺序执行,且当前一个命令执行成功时,后一个命令就不会执行。

或者说,如果Command1命令运行失败,则继续运行Command2命令。

当一件事可以有多个命令相互代替,不确定哪个可以正确执行时,可以用这种方式免去等待输入。

$ mkdir foo || mkdir bar

上面例子中,只有mkdir foo命令执行失败(比如foo目录已经存在),才会继续执行mkdir bar命令。如果mkdir foo命令执行成功,就不会创建bar目录了。

& - 启动一个新 shell 子进程执行命令

对于一些程序,在它运行时,它会占用当前bash shell process(直到这个程序执行完)。

所有如果当这个程序执行非常耗时时,我们可能希望当它运行后,不要占用当前 shell process。

换句话说,将这个程序在后台运行。

这时候,我们就可以使用&(当然,我们通过传递特定参数,来让程序在自身内部实现以后台模式运行,也是OK的)。

我们来通过一个小实验进行学习。

在最普通的情况下

# 获取当前 process (zsh shell) 的 PID,且为 464588
➜  ~ echo $$
464588
➜  ~ sleep 50000

与此同时,通过另外一个ssh session 来观测

$ ps axjf
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      1     972     972     972 ?             -1 Ss       0   0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
    972  464448  464448  464448 ?             -1 Ss       0   0:00  \_ sshd: sw [priv]
 464448  464587  464448  464448 ?             -1 S     1000   0:00  |   \_ sshd: sw@pts/0
 464587  464588  464588  464588 pts/0     464881 Ss    1000   0:00  |       \_ -zsh
 464588  464881  464881  464588 pts/0     464881 S+    1000   0:00  |           \_ sleep 50000
    972  464628  464628  464628 ?             -1 Ss       0   0:00  \_ sshd: sw [priv]
 464628  464701  464628  464628 ?             -1 S     1000   0:00      \_ sshd: sw@pts/1
 464701  464702  464702  464702 pts/1     464882 Ss    1000   0:00          \_ -zsh
 464702  464882  464882  464702 pts/1     464882 R+    1000   0:00              \_ ps axjf

如果增加 &

# 获取当前 process (zsh shell) 的 PID,且为 464588
➜  ~ echo $$
464588
➜  ~ sleep 50000 &
[1] 465180
$ ps axjf
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      1     972     972     972 ?             -1 Ss       0   0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
    972  464448  464448  464448 ?             -1 Ss       0   0:00  \_ sshd: sw [priv]
 464448  464587  464448  464448 ?             -1 S     1000   0:00  |   \_ sshd: sw@pts/0
 464587  464588  464588  464588 pts/0     464588 Ss+   1000   0:00  |       \_ -zsh
 464588  465180  465180  464588 pts/0     464588 SN    1000   0:00  |           \_ sleep 50000
    972  464628  464628  464628 ?             -1 Ss       0   0:00  \_ sshd: sw [priv]
 464628  464701  464628  464628 ?             -1 S     1000   0:00      \_ sshd: sw@pts/1
 464701  464702  464702  464702 pts/1     465184 Ss    1000   0:00          \_ -zsh
 464702  465184  465184  464702 pts/1     465184 R+    1000   0:00              \_ ps axjf

增加&后,这行command(sleep 50000 &)在成功启动一个新的 child process (对应的 PID 为 465180 )后,就会返回执行流到 zsh process(这意味着,你可以继续执行其他命令,而不是当这个新启动的 child process 执行结束后,才返回执行流)。

获取background process PID

比如我通过 systemctl 来执行下面的这个.sh

#!/bin/bash

while true; do
  MY_PID=""
  sleep 100 & MY_PID=$! # MY_PID=$! 的含义是:Capture value returnd by last command
  echo "my process's PID: $MY_PID"
  sleep 1000
done

启动后

$ journalctl -u sw_test.service -f
Apr 03 12:39:11 ubuntu bash[216927]: sw 216928

可以看到,通过MY_PID=$! 拿到了background process 的PID,这通常用于在一个脚本中,我们需要在某个条件时,需要kill掉这个background process。

$ ps axjf
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      1  216927  216927  216927 ?             -1 Ss       0   0:00 /bin/bash /home/sw/my_vpn.sh
 216927  216928  216927  216927 ?             -1 S        0   0:00  \_ sleep 100
 216927  216929  216927  216927 ?             -1 S        0   0:00  \_ sleep 1000

多命令之间有数据共享

| - 管道(pipeline)

A pipeline is a sequence of one or more commands separated by the character |. The format for a pipeline is:

The standard output of command is connected via a pipe to the standard input of command2.

Each command in a pipeline is executed as a separate process (i.e., execute in different subshell processes).

管道命令操作符是:|,它能且仅能获取由前面一个命令执行后输出的标准输出(standard output, stdout),而并不能获取前面一个命令执行后输出的标准错误输出(stdandard error, stderr)。然后,传递给下一个命令,作为它的标准输入(standard input, stdin)

$ cat test.sh | grep -n 'echo'
  echo "very good!";
  echo "good!";
  echo "pass!";
  echo "no pass!";
#读出test.sh文件内容,通过管道转发给grep 作为输入内容

常用来作为接收数据管道命令有:sed、awk、cut、head、top、less、more、wc、join、sort、split 等等,都是些文本处理命令。这其实是因为管道只能获取前一个命令执行后得到的标准输出,而不能得到标准错误输出。而又因为这些文本处理命令永远不会标准错误输出,因而我们并不需要使用到>< - 重定向(redirection)。

注意,重定向可以得到前一个命令执行后得到的标准输出(默认情况下)或标准错误输出

Demo

$ netstat -an

可以查看所有的网络连接情况。

LISTEN表示端口被监听,等待被人访问。ESTABLISHED表示有人正在使用这个端口

也就是说只要有一个ESTABLISHED,就表明有一个客户端正连接在这个服务器上,因此可以用管道符去查找这些信息行:

$ netstat -an | grep "ESTABLISHED"
tcp        0     52 192.168.0.112:22            192.168.0.104:2367          ESTABLISHED

从而统计服务器上连接了哪些人。还可以将这个结果再接一个管道符,用 wc -l 去处理这个结果,就知道有多少行(人)连接了这个服务器,而不去关心其它信息了:

$ netstat -an | grep "ESTABLISHED" | wc -l
3

>< - 重定向(redirection)

重定向(redirection)可以指定将前一个命令执行所输出的标准流(包括标准输入、标准输出标准错误输出)或者文件内容输出到特定位置。

  • >:输出重定向,表示重定向标准输出或标准错误输出,即将一个命令的执行结果的标准输出或标准错误输出(如果不指定,默认是标准输出,也可以指定为标准错误输出)重定向到哪里,例如 echo "123" > /home/123.txt ,即执行 echo 命令,其会将 123 字符串输入到标准输出,> 则会将 echo 执行结束的标准输出(通过标准输入)输出作为 123.txt 文件的内容(原内容会被覆盖掉)。
    • aaa > temp.txt:将 aaa 命令执行的标准输出覆盖写入 temp.txt(其执行后对应的标准错误输出会只显示在terminal,而不会被写入 temp.txt),等价于 aaa 1> temp.txt
    • aaa 2> temp.txt:将 aaa 命令执行的标准错误输出覆盖写入 temp.txt(其执行后对应的标准输出会被丢弃)
    • aaa >temp.txt 2>&1 :将 aaa 命令执行的标准输出标准错误输出都(覆盖)写入 temp.txt
  • <:输入重定向,表示将一个文件的内容作为标准输入重定向到一个命令中,比如,将一个文件的内容作为一个命令的标准输入
  • 1 :表示标准输出(stdout),重定向的默认值就是1,因此在 echo "123" > /home/123.txt 中,会将 echo 执行结束的标准输出通过标准输入)输出到文件中。
  • 2 :表示标准错误(stderr),比如我们希望将一个命令执行后输出的标准错误输出输出作为下一个命令的标准输入,就可以使用 <command_1> 2> <command_2>
  • & :表示等同于的意思,2>&1,表示2(即标准错误输出)的内容重定向等同于1(即标准输出)。换句话说,将标准错误输出的内容重定向到标准输出被重定向到的地方。
  • /dev/null :代表空设备文件

<command> 1>/dev/null 2>&1 语句含义,是 allow standard error to be redirected to the same destination that standard output is directed to,因为

  • 1 > /dev/null : 首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,说白了就是在终端不显示任何信息(在执行完一个命令后)。
  • 2>&1 :同时,标准错误输出重定向(等同于)标准输出,因为之前已经指定标准输出重定向到空设备文件,所以标准错误输出也重定向到空设备文件。

Case 1 - [输出重定向] 只需要标准错误输出或标准输出

# 重定向标准输出(忽略标准错误输出),等价于 echo "sw test" 1> file1
$ echo "sw test" > file1
$ cat file1
sw test
# 重定向标准错误输出(忽略标准输出)
$ echo "sw test" 2> file1
sw test
$ cat file1
$

可以看到,当执行 echo "sw test" 2> file1 后,file1中并没有内容,这是因为我们使用了 2>,因此将 echo 命令的执行结果的标准错误重定向到 file1 文件中,而 echo 命令的执行结果的标准错误内容为空,因此,file1中并没有内容。

注意,这里的 2 > *之间不可以有空格,*2> 是一体的时候才表示重定向标准错误输出。

Case 2 - [输出重定向] 不需要标准错误输出或/和标准输出

$ echo "sw test2"
sw test2

# 执行完后,在 terminal 并没有任何输出,是因为我们把标准输出重定向到了空设备文件
$ echo "sw test2" 1>/dev/null
$

# 类似地,如果我们不需要标准错误输出
wrongCommand
zsh: command not found: wrongCommand
$ wrongCommand 2>/dev/null
$

# 如果我们标准错误输出和标准错误都不需要
$ echo "sw test2" 1>/dev/null 2>&1

<command> 1>/dev/null 2>&1 语句含义,是 allow standard error to be redirected to the same destination that standard output is directed to,因为

  • 1 >/dev/null : 首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,说白了就是在终端不显示任何信息(在执行完一个命令后)。
  • 2>&1 :标准错误输出的重定向目的地(等于)标准输出的重定向目的地,因为之前已经指定标准输出重定向到空设备文件,所以标准错误输出也重定向到空设备文件。

Case 3 - 2>&1(将标准错误输出和标准输出同时记录到文件中)

下面通过一个例子来展示2>&1有什么作用:

$ cat test.sh
t
date

# 如果不做任何重定向,terminal 会显示当前所有标准输出和标准错误输出
$ ./test.sh
./test.sh: line 1: t: command not found
Sat May 30 15:38:55 +08 2020

# 将标准输出重定向到 test1.log中(因此 terminal 中只显示标准错误输出)
$ ./test.sh > test1.log
$ ./test.sh: line 1: t: command not found

$ cat test1.log
Sat May 30 15:40:02 +08 2020

$ ./test.sh > test2.log 2>&1
$ cat test2.log
./test.sh: line 1: t: command not found
Sat May 30 15:41:23 +08 2020

test.sh中包含两个命令,其中t是一个不存在的命令,因此执行会报错。

值得注意的是,在默认情况下,标准错误输出会只在terminal中显示(因为我们只将标准输出信息重定向到了 test1.log)。date 命令则能正确执行,因此输出时间信息到了标准输出。

而执行 ./test.sh > test2.log 2>&1时,stderr和stdout的内容都被重定向到 test2.log 文件中了。

Case 4- 输入重定向

# 将hadoop-hadoop-jobtracker-brix-00.out的内容作为test.sh的输入
$ sh test.sh < hadoop-hadoop-jobtracker-brix-00.out

管道命令与重定向区别

区别是:

  • stdout 和 stderr
    • 当使用管道时( <command_1> | <command_2> ),左边的命令应该有标准输出,右边的命令应该能接收标准输入(否则这个管道的使用并没有任何意义,因为不产生任何效果)
    • 当使用向右重定向(>)时,左边的命令应该有标准输出 > 右边只能是文件(普通文件,文件描述符,文件设备)
    • 左边的命令应该需要标准输入 < 右边只能是文件

例子

# 可以相互转换情况
# 输入重定向
$ cat test.sh | grep -n 'echo'
   echo "very good!";
   echo "good!";
   echo "pass!";
   echo "no pass!";
# "|"管道两边都必须是shell命令
  
$ grep -n 'echo' < test.sh    
  echo "very good!";
  echo "good!";
  echo "pass!";
  echo "no pass!";
# "重定向"符号,右边只能是文件(普通文件,文件描述符,文件设备)

#上面一个等同于这个
$ sed -n '1,$p' < test.sh | grep -n 'echo'
   echo "very good!";
   echo "good!";
   echo "pass!";
   echo "no pass!";
 
 
 
$ sed -n '1,10p' < test.sh | grep -n 'echo' <testsh.sh
10:echo $total;
18:echo $total;
21:     echo "ok";
#哈哈,这个grep又接受管道输入,又有testsh.sh输入,那是不是2个都接收呢。刚才说了"<"运算符会优先,管道还没有发送数据前,grep绑定了testsh.sh输入,这样sed命令输出就被抛弃了。这里一定要小心使用
 
#输出重定向

$ cat test.sh>test.txt
$ cat test.sh|tee test.txt &>/dev/null
#通过管道实现将结果存入文件,还需要借助命令tee,它会把管道过来标准输入写入文件test.txt ,然后将标准输入复制到标准输出(stdout),所以重定向到/dev/null 不显示输出
#">"输出重定向,往往在命令最右边,接收左边命令的,输出结果,重定向到指定文件。也可以用到命令中间。

【讨论】是否同时执行

; - 按顺序执行多个命令

如果使用 ;,按顺序执行的多个命令都会在同一个 shell 进程中执行(当然,他们是一个命令先执行完后,下一个命令才开始执行),或者说这些命令的父进程都是同一个 shell 进程

证明:

$ sleep 100; sleep 200

可以看到,两次 sleep 分别执行的时候他们的父进程都是 PID 为 31152 的 zsh shell 进程。

因而,由于这些命令的父进程都是同一个 shell 进程,在前一个命令中设置的变量,能在后一个命令中读到:

$ a=22 ; echo $a
22

再次强调,当 sleep 100 执行完成之后,sleep 200 才会开始执行,即依次按顺序执行(这点与管道不同)。

| - 管道(pipeline)

$ a=22 | echo $a

由于使用管道连接的多个命令会是同时开始执行的,因而读不到 $a 的值。

$ sleep 20 | sleep 200

可以看到,使用管道连接的多个命令会同时开始执行,而且每个命令都会作为当前的shell进程的子进程被执行

Reference