目录

Linux Shell中的重定向

经常会看到别人的shell脚本后面有一个 2>&1 ,一直没去深究,今天这个话题就以这个为出发点进行展开,学习一下Linux shell中重定向的话题。

特殊的东西

先来看一点Linux中特殊的东西,为后面的内容打下基础

特殊的文件

  • /dev/null 空,可以将垃圾内容导入其中,就会消失
  • /dev/zero 零,可以从中读出无穷无尽的0
  • /dev/urandom 随机数,可以从中读出无穷无尽的随机数
  • /dev/stdin 标准输入流
  • /dev/stdout 标准输出流
  • /dev/stderr 标准错误输出流

我们可以看到后三个文件其实是个链接,指向内核的文件描述符 0\1\2

1
2
3
lrwxrwxrwx 1 root root         15 Mar 24 16:20 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root         15 Mar 24 16:20 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root         15 Mar 24 16:20 stdout -> /proc/self/fd/1

特殊的文件描述符

在Linux shell中有三个特殊的文件描述符(File descriptor or fd):

  • fd0 是标准输入: stdin
  • fd1 是标准输出: stdout
  • fd2 是标准错误输出: stderr

通过这三个特殊的文件描述符我们可以控制输入输出流

重定向

我们经常会接触到 > 这个符号,叫做重定向,其实还有另一个符号 >> 有着类似的功能,他们之间有一点小区别:

  • > 是覆盖的方式
  • >> 是追加的方式

下面的内容将全部以 > 为例,>> 除了内容是追加之外没有其他区别,就不赘述

使用重定向

重定向到文件

先来看一下最基本的重定向的使用方法,我们将 echo 命令的输出重定向到一个文件中

echo "hello" > a.txt

执行结果:

1
2
3
root@ubuntu:~# echo "hello" > a.txt
root@ubuntu:~# cat a.txt
hello

这里是将 stdout 重定向到文件 a.txt 中,与下面的命令等价

echo "hello" 1> a.txt

执行结果:

1
2
3
4
root@ubuntu:~# rm a.txt
root@ubuntu:~# echo "hello" 1> a.txt
root@ubuntu:~# cat a.txt
hello

这里我们看到重定向符号 > 默认是将 stdout 也就是 fd1 重定向到别处

如果我们想要将标准错误输出stderr进行重定向,只需要将上面命令中的文件描述符1修改为标准错误输出的文件描述符2即可

重定向到文件描述符

有些情况下 stderr 是会被程序控制写入错误日志的,如果我们想要在命令运行的时候将错误显示在屏幕上,就需要将错误输出重定向到标准输出流中

我们先来尝试一下, 这里我们没有找到一个合适的命令,就拿 ls 命令查看一个不存在的目录,这样会产生错误输出

这里错误默认是会被输出到屏幕的,只是我暂时没有找到一个更好的程序,我们先假设他不会输出到屏幕

ls error 2>1

这里我们的猜想是将 stderr 重定向到 stdout, 所以写了 2>1, 我们来看一下会不会成功?

1
2
3
4
5
6
root@ubuntu:~# ls error 2>1
root@ubuntu:~#
root@ubuntu:~# ls
1
root@ubuntu:~# cat 1
ls: cannot access 'error': No such file or directory

我们看到了,并没有输出,而是在当前目录下生成了一个文件 1, 这说明如果我们只写 >1 会被当做重定向到文件 1

此时,我们的 & 就要上场了

>& 是将一个流重定向到一个文件描述符的语法,所以刚刚我们应该指明要重定向到 fd1, 也就是 &1

ls error 2>&1

执行结果:

1
2
root@ubuntu:~# ls error 2>&1
ls: cannot access 'error': No such file or directory

到这里我们就可以自主发挥了

将标准输出重定向到标准错误输出

echo "hello" 1>&2 or echo "hello" >&2

甚至我们可以玩点复杂的

(echo "hello" >&9) 9>&2 2>&1

1
2
root@ubuntu:~# (echo "hello" >&9) 9>&2 2>&1
hello

这里的文件描述符9会自动生成,但是去除括号就会提示错误了

1
2
root@ubuntu:~# echo "hello" >&9 9>&2 2>&1
bash: 9: Bad file descriptor

在 bash >4.0 的版本中,又出了新的重定向语法

1
2
3
$ ls -ld /tmp /tnt 2> >(sed 's/^/E: /') > >(sed 's/^/O: /')
O: drwxrwxrwt 17 root root 28672 Nov  5 23:00 /tmp
E: ls: cannot access /tnt: No such file or directory

这种写法我还没有学习,等我后面学会了再进行更新

格式化输出

来点高端点的用法

用于格式化输出, 将标准输出和错误输出两个流重定向到不同的处理中,最后汇总

((ls -ld /tmp /tnt |sed 's/^/O: /' >&9 ) 2>&1 |sed 's/^/E: /') 9>&1| cat -n

1
2
3
root@ubuntu:~# ((ls -ld /tmp /tnt |sed 's/^/O: /' >&9 ) 2>&1 |sed 's/^/E: /') 9>&1| cat -n
     1  O: drwxrwxrwt 1 root root 4096 Mar 22 18:59 /tmp
     2  E: ls: cannot access '/tnt': No such file or directory

相同作用的新版语法

cat -n <(ls -ld /tmp /tnt 2> >(sed 's/^/E: /') > >(sed 's/^/O: /'))

1
2
3
root@ubuntu:~# cat -n <(ls -ld /tmp /tnt 2> >(sed 's/^/E: /') > >(sed 's/^/O: /'))
     1  O: drwxrwxrwt 1 root root 4096 Mar 22 18:59 /tmp
     2  E: ls: cannot access '/tnt': No such file or directory

合并文件

将输出文件 m 和 n 合并: n >& m

将输入文件 m 和 n 合并: n <& m

输入边界

将开始标记 tag 和结束标记 tag 之间的内容作为输入: << tag

例如:

1
2
3
4
5
6
root@ubuntu:~# wc -l << EOF
    document line 1
    document line 2
    document line 3
EOF
3 //表明收到3行输入

它的作用是将两个 EOF 之间的内容(document) 作为输入传递给 command。

注意:

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进
  • 开始的delimiter前后的空格会被忽略掉

有关覆盖

如果我们用 set -o noclobber 设置bash,那bash将不会覆盖任何已经存在的文件,但是我们可以通过 >| 绕过这个限制

先来看一下默认的情况

1
2
3
4
5
6
7
root@ubuntu:~# testfile=$(mktemp /tmp/testNoClobberDate-XXXXXX)
root@ubuntu:~# date > $testfile ; cat $testfile
Tue 24 Mar 2020 05:05:53 PM CST
root@ubuntu:~# date > $testfile ; cat $testfile
Tue 24 Mar 2020 05:05:56 PM CST
root@ubuntu:~# date > $testfile ; cat $testfile
Tue 24 Mar 2020 05:06:13 PM CST

如预期的一样,每一次重定向都覆盖了原文件

下面我们设置 noclobber 标志

set -o noclobber

然后重复上面的操作试一下

1
2
3
4
5
6
root@ubuntu:~# date > $testfile ; cat $testfile
bash: /tmp/testNoClobberDate-yKVkaY: cannot overwrite existing file
Tue 24 Mar 2020 05:06:13 PM CST
root@ubuntu:~# date > $testfile ; cat $testfile
bash: /tmp/testNoClobberDate-yKVkaY: cannot overwrite existing file
Tue 24 Mar 2020 05:06:13 PM CST

我们看到了bash的提示,不能覆盖已存在的文件,实际结果也是一样

如何进行绕过呢? 我们来试一下用 >| 代替 >

1
2
3
4
root@ubuntu:~# date >| $testfile ; cat $testfile
Tue 24 Mar 2020 05:10:45 PM CST
root@ubuntu:~# date >| $testfile ; cat $testfile
Tue 24 Mar 2020 05:10:49 PM CST

我们发现此时可以覆盖已经存在的文件,我们查看一下目前的设置

1
2
root@ubuntu:~# set -o | grep noclobber
noclobber       on

noclobber 的确是开启的,所以 >| 的确可以绕过这一限制

使用 set +o noclobber 关闭这个限制,防止对我们后面的使用造成影响

1
2
3
4
root@ubuntu:~# set +o noclobber
root@ubuntu:~# set -o | grep noclobber
noclobber       off
root@ubuntu:~# rm $testfile

其他的小点

重定向到一处

如果我们要将 stdoutstderr 重定向到同一个地方,该怎么写呢?

下面两种哪种是对的?

  1. ls -ld /tmp /tnt 2>&1 1>a.txt
  2. ls -ld /tmp /tnt 1>b.txt 2>&1

验证一下

第一种写法

1
2
3
4
root@ubuntu:~# ls -ld /tmp /tnt 2>&1 1>a.txt
ls: cannot access '/tnt': No such file or directory
root@ubuntu:~# cat a.txt
drwxrwxrwt 1 root root 4096 Mar 24 17:15 /tmp

第二种写法

1
2
3
4
root@ubuntu:~# ls -ld /tmp /tnt 1>b.txt 2>&1
root@ubuntu:~# cat b.txt
ls: cannot access '/tnt': No such file or directory
drwxrwxrwt 1 root root 4096 Mar 24 17:15 /tmp

我们可以看到第二种写法是正确的

同理,下面这种写法也正确

ls -ld /tmp /tnt 2>b.txt 1>&2

套个娃a

来点奇葩的,如果我们将 stderr 重定向到 stdout, 同时又将 stdout 重定向到 stderr 会发生什么?

如此套娃会不会导致回环卡死?

试一下

1
2
3
root@ubuntu:~# ls -ld /tmp /tnt 2>&1 1>&2  | sed -e s/^/++/
++ls: cannot access '/tnt': No such file or directory
++drwxrwxrwt 1 root root 4096 Mar 24 17:15 /tmp

我们发现都会从标准输出出来

反过来呢?

1
2
3
root@ubuntu:~# ls -ld /tmp /tnt 1>&2 2>&1  | sed -e s/^/++/
ls: cannot access '/tnt': No such file or directory
drwxrwxrwt 1 root root 4096 Mar 24 17:15 /tmp

我们发现都没有从标准输出出来,都是从标准错误输出出来的

也就是说 a>&b b>&a 这种套娃写法中, b才是出口

阅读更多内容

如果你想了解功能,通过下面的命令查看官方文档吧

man -Len -Pless\ +/^REDIRECTION bash

本文的参考资料: stack overflow