最近工作,生活中有一堆事情要处理,博客好久没更新了,请见谅。
拖了好久才翻译好这篇文章,这篇翻译下来好累,主要是原文废话太多了,后来还是决定尽量省略部分内容,建议有时间的还是去看下原文。这一篇太长了,先翻译部分,我会继续不断的更新。
这是 Bash One-Liners Explained 系列的第三篇文章。在这一篇里,我会给大家介绍重定向相关的内容。我会选择用最合适的 Bash 方法,各种常见的语法和技巧,向各位阐明如何用 Bash 内置的命令和 Bash 编程语言来完成各式各样的任务。
重定向其实是通过操作文件描述符来完成的,这样会更容易理解。当 Bash 启动时,会自动创建三个标准的文件描述符,它们分别是 stdin(标准输入,文件描述符为0),stdout(标准输出,文件描述符为1)和 stderr(标准错误输出,文件描述符为2)。你也可以创建更多的文件描述符,例如3,4,5等等,或者关闭它们,又或者拷贝它们。你可以从对应的文件中读取或者写入内容。
文件描述符指向某个文件(除非它们被关闭)。通常情况下,Bash 启动的三个文件描述符 —— stdin,stdout 和 stderr 都是指向你的终端,从终端输入中读取内容,并且把标准输出和标准错误都送到终端上。
假设你的终端对应的设备文件是 /dev/tty0
,下面的截图解释了 Bash 启动时文件描述符表的样子:
当 Bash 执行一个命令时,他会 fork 一个子进程(查看man 2 fork
)。子进程会从父进程继承所有的文件描述符,设置好指定的重定向,最后执行该命令(查看man 3 exec
)。
可以尝试用图表来可视化展现,重定向发生时文件描述符表的变化过程,这种方法可以帮助你更好的理解重定向功能。
1. 重定向命令的 stdout 到文件
$ command >file
>
是输出重定向操作符。Bash 首先会打开文件准备写入,如果文件打开成功,则将命令command
的 stdout 指向之前打开的文件。如果打开失败,则不会继续执行命令。command >file
的写法和command 1>file
的写法是一样的,1
是 stdout 对应的文件描述符。
下面的图片描述了上述命令执行时文件描述符表的变化过程:
1. 重定向命令的 stderr 到文件
$ command 2> file
Bash 将这行命令的 stderr(文件描述符为2)重定向到文件file
中。
下面是重定向后的文件描述符表:
3. 重定向命令的 stdout 和 stder 到同一个文件中
$ command &>file
这一行命令使用了&>
操作符,它将命令command
的 stdout 和 stderr 都重定向到文件file
中。
除此之外,还有几种方法可以将 stdout 和 stderr 同时重定向到同一个文件中。你可以依次重定向每个输出。
$ command >file 2>&1
上面是一种更加常见的方法,首先重定向 stdout 到文件file
,然后将 stderr 重定向到和 stdout 同样的文件中。
当 Bash 在命令中遇到多个重定向操作时,它会从左到右依次处理。我们通过图表来依次推导这整个过程。初始时文件描述符表的样子:
现在 Bash 处理第一组重定向>file
,我们之前已经解释过,它将使得 stdout 指向文件file
:
接下来,Bash 开始处理第二组重定向2>&1
,它会把 stderr 重定向到 stdout 所指向的文件:
这里要注意不要错误的写成:
$ command 2>&1 >file
重定向的顺序是很重要的,这行命令只会把 stdout 重定向到文件,而 stderr 会继续输出到终端屏幕上。为了理解原因,我们同样来推导依次整个处理过程。
当 Bash 遇到2>&1
时,它会把 stderr 指向 stdout 对应的文件(这里是终端):
紧接着,Bash 看到>file
,按照之前我们解释的,它会把 stdout 重定向到文件file
:
从上面的图片中可以看出,stdout 指向了文件file
,但是 stderr 依然指向终端。所以,一定要注意重定向的书写顺序。
4. 丢弃命令的 stdout 输出
$ command > /dev/null
/dev/null
是一个特殊的文件,任何写入到该文件的内容都会被丢弃。所以,我们需要做的就是把 stdout 重定向到文件/dev/null
。
类似的,基于前一条命令,我们可以做到把输出到 stdout 和 stderr 的内容都丢弃:
$ command >/dev/null 2>&1
或者简单的写成:
$ command &>/dev/null
此时的文件描述符表为:
5. 重定向文件到命令的 stdin
$ command <file
Bash 在执行命令之前,打开文件file
准备读入。如果打开文件出错,Bash 会直接返错,不会继续执行命令。相反如果打开成功,Bash 会使用打开的文件的文件描述符作为命令的标准输入。此时,文件描述符表的样子为:
下面是一个例子,假如你想把文件的第一行读入到变量中:
$ read -r line < file
6. 重定向一堆字符串到命令的 stdin
$ command <<EOL your multi-line text goes here EOL
这里用到了 here document 的语法<<MARKER
。当 Bash 遇到该操作符是,它会从标准输入读取每一行,直到遇到一行以MARKER
开头为止。这个例子中,Bash 读取到所有内容并传给command
的 stdin。
假设你想去除一堆 URL 地址中的http://
部分,可以用下面的一行命令:
$ sed 's|http://||' <<EOF http://url1.com http://url2.com http://url3.com EOF
输出结果为:
url1.com url2.com url3.com
7. 重定向一行文本到命令的 stdin
$ command <<< "foo bar baz"
等价于:
$ echo "foo bar baz" | command
8. 重定向所有命令的 stderr 到文件中
$ exec 2>file $ command1 $ command2 $ ...
这一行命令中使用了 Bash 的内置命令exec
。如果你在它之后指定重定向操作,重定向的效果为一直持续到显示改变或者脚本退出为止。
在这个例子中,2>file
处理之后,随后所有命令的 stderr 都会重定向到文件file
中。通过这种方法,你可以很方便的把脚本中所有命令的 stderr 都汇总到一个文件,同时又不用每一个命令之后都指定2>file
。
9. 打开文件并通过特定文件描述符读
$ exec 3<file
上面我们再次用到了exec
命令,3<file
告诉它以只读方式打开文件 file,并将文件描述符 3 指向打开的文件:
随后你可以通过描述符 3 来读取文件内容:
$ read -u 3 line
一些常规的命令,例如 grep,还可以这么用:
$ grep "foo" <&3
执行了上面的命令后,grep 命令的 stdin 指向了之前打开的文件,看起来好像将文件描述符 3 复制成了 0。
当你使用完成后,通过下面的方法关闭该文件:
$ exec 3>&-
这里文件描述符 3 指向&-
,就意味着关闭改文件描述符。
10. 打开文件并通过特定文件描述符写
$ exec 4>file
同上面一条类似,这里我们将文件描述符 4 指向以写方式打开的文件:
你可以看到,你并不需要按顺序使用文件描述符,可以任意挑选从 0 到 255 之内的所有未被使用的描述符。
接下来,我们可以很方便的通过描述符 4 来写文件:
$ echo "foo" >&4
或者关闭描述符:
$ exec 4>&-
11. 打开文件并通过特定文件描述符读写
$ exec 3<>file
这里我们用到了菱形操作符(diamond operator) <>
,该操作符表示打开的文件既可以用于读也可以用于写。例如:
$ echo "foo bar" > file # write string "foo bar" to file "file". $ exec 5<> file # open "file" for rw and assign it fd 5. $ read -n 3 var <&5 # read the first 3 characters from fd 5. $ echo $var
结果会输出foo
。然后,我们可以往里写些内容:
$ echo -n + >&5 # write "+" at 4th position. $ exec 5>&- # close fd 5. $ cat file
结果输出foo+bar
。
12. 重定向一组命令的 stdout 到文件中
$ (command1; command2) >file
这一行命令使用(commands)
语法,commands 会在一个子 shell 中执行。所以在这里,command1
和command2
会在子 shell 中运行,然后 Bash 将子 shell 的 stdout 重定向到文件中。
13. 在 Shell 中通过文件中转执行的命令
打开两个 shell,在第一个中执行以下命令:
mkfifo fifo exec < fifo
而在第二个中,执行:
exec 3> fifo; echo 'echo test' >&3
回头你会发现在第一个 shell 中会输出 test,你可以继续不断地往文件 fifo 中输入命令,第一个 shell 中 会一直执行这些命令。
我们来解释下这里的原理。
在第一个 shell 中,我们使用 mkfifo
命令创建了一个命名管道fifo
。命名管道(也可以叫做 FIFO)类似之前提到的管道(匿名管道),除了前者是以文件系统上的文件的方式存在(标识一条特殊的进程通信的内核通道)。命名管道可以被多个进程打开同时读写,当多个进程通过 FIFO 交换数据时,内核并没有写到文件系统中,而是自己私下里传递了这些数据。所以,FIFO 这中特殊的文件,它在文件系统中是没有存放数据块的。文件系统只是通过文件名的形式提供标识,以便进程间可以利用这个标识来访问管道。
接下来,我们通过exec < fifo
命令,使用 fifo 作为当前 shell 的标准输入。
现在,我们在第二个 shell 中以写的方式打开命名管道,并将文件描述符 3 指向改它。接下来,我们只要简单地把echo test
文件描述描述符 3,最总会写到管道 fifo 中。因为第一个 shell 的标准输入连接到管道的读的一段,它会接受到传递过来的内容并执行。
14. 通过 Bash 访问 Web 站点
$ exec 3<>/dev/tcp/www.google.com/80 $ echo -e "GET / HTTP/1.1\n\n" >&3 $ cat <&3
Bash 将/dev/tcp/host/port
当作一种特殊的文件,它并不需要实际存在于系统中,这种类型的特殊文件是给 Bash 建立 tcp 连接用的。
在这个例子中,我们首先以读写的方式打开文件描述符 3,并把它指向/dev/tcp/www.google.com/80
,后者是一个连接,表示连接到 www.google.com 的 80端口。
接下来,我们往文件描述符 3 写GET / HTTP/1.1\n\n
。完成之后,我们使用cat
命令从同样的地方读取返回内容。
类似的,你也可以通过/dev/udp/host/port
来创建一个 UDP 连接。
使用/dev/tcp/host/port
,你甚至可以使用 Bash 写一个端口扫描程序。
15. 重定向输出时防止覆盖已有的文件
$ set -o noclobber
这行命令将当前 shell 的noclobber
选项打开,这个选项的作用是,防止>
重定向操作符覆盖已有的文件内容。
这时如果你重定向写入到一个文件,会返回一个错误:
$ program > file bash: file: cannot overwrite existing file
如果你100%确定你要覆盖一个文件,可以使用>|
重定向操作符:
$ program >| file
上面的命令会正确的执行,因为它覆盖了noclobber
选项。
16. 重定向标准输入到文件,同时打印到标准输出
$ command | tee file
tee
是一个很方便的命令,它并不是 Bash 的一部分,但是你会经常用到这个命令。它将接收到的输入,同时打印到标准输出和一个文件中。
下面的图片描述了上面命令执行的过程:
17. 重定向进程的标准输出到另外一个进程的标准输入
$ command1 | command2
这是一个大家非常熟悉的管道用法:
18. 重定向进程的标准输出和标准错误到另外一个进程的标准输入
$ command1 |& command2
以上用法只在 Bash 4.0 以后的版本才能使用,对于老的版本,比较通用的做法是:
$ command1 2>&1 | command2
下面的图片描述了上面命令执行的过程:
19. Give file descriptors names
略,Bash 4以上新增功能。
20. 重定向顺序
你可以将重定向放到命令的任意位置,一下三个命令的执行结果都是一样的:
$ echo hello >/tmp/example $ echo >/tmp/example hello $ >/tmp/example echo hello
21. 交换标准输出与标准错误输出
$ command 3>&1 1>&2 2>&3
在这里,我们首先让文件描述符3指向 stdout,然后将 stdout(文件描述符1)指向 stderr(文件描述符2)。最后有把 stderr(文件描述符2)指向文件描述符3,即 stdout。最终,我们交换了 stdout 与 stderr。
下面我们通过图来展示以上过程,初始的时候是这样的:
首先,执行了3>&1
之后,文件描述符3指向 stdout:
接下来,执行1>&2
,文件描述符1指向了 stderr:
最后,执行2>&3
,文件描述符2执向了 stdout:
如果你是一个追求完美的人,可以将文件描述符3关闭:
$ command 3>&1 1>&2 2>&3 3>&-
最终的文件描述符图会是这样的:
22. 重定向标准输出和标注错误输出给不同的进程
$ command > >(stdout_cmd) 2> >(stderr_cmd)
这一行命令用到了进程替换(Process Substitution)语法。>(...)
操作符的执行过程是,运行里面的命令,同时将命令的标准输入连接到一个命名管道的读段。Bash 随后会用命名管道的实际文件名替换这个操作符。
例如,假设第一个替换操作>(stdout_cmd)
返回/dev/fd/60
,而后一个返回/dev/fd/61
。替换后,最初的命令变成以下形式:
$ command > /dev/fd/60 2> /dev/fd/61
从上面可以看出,标准输出重定向到了/dev/fd/60
,而标准错误输出则重定向到了/dev/fd/61
。
当命令执行是输出内容到 stdout,则管道/dev/fd/60
后面的进程(stdout_cmd
)会从另外一侧读取到数据。同样的,进程stderr_cmd
也能从命令的 stderr 输出中读取。
23. 获取管道流中的所有命令执行退出码
假设你用管道流执行多个命令:
$ cmd1 | cmd2 | cmd3 | cmd4
然后你想获取所有命令的退出码,但是这里并没有一种简单的做法可以实现,因为 Bash 只会返回最后一个命令的退出码。
Bash 的开发者同样思考了这个问题,他们添加了PIPESTATUS
数组,这个数组中存放了管道流中所有命令的退出码。
下面是一个简单的例子:
$ echo 'pants are cool' | grep 'moo' | sed 's/o/x/' | awk '{ print $1 }' $ echo ${PIPESTATUS[@]} 0 1 0 0
赞一个。通俗易懂。
好强大的输入输出重定向机制!辛苦 kodango 先生了。
@御宅暴君:客气,大家喜欢就好 🙂