关于 Shell 输入输出重定向的研究

Linux 的输入输出流有三类:

  • STDIN:标准输入流,文件描述符为 0。
  • STDOUT:标准输出流,文件描述符为 1。
  • STDERR:标准错误输出流,文件描述符为 2。

默认情况下,STDIN 由键盘读入,STDOUTSTDERR 均输出到屏幕上。


Problem 1 - rewire streams

普通的输入输出重定向就不介绍了,无非是遵循 command < fileIn > fileOut 2> fileErr 的格式。令我产生疑惑的是 Missing Semester 第二课中课后作业中提到的这样一个程序 number.sh

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash

n=$(( RANDOM % 100 ))

if [[ n -eq 42]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi

echo "Everything went according to plan"

在这里,>&2 是什么意思?

由于出现了 &,我首先联想到的是这样一个例子:command > file 2>&1。这一命令将会在文件 file 中同时写入命令 command 的输出信息与错误信息。这是如何做到的呢?在研究的过程中,我发现了许多之前不曾注意过的知识盲区。

  • & 的作用:& 纯粹是为了强调其之后的数字是一个文件标识符而不是文件名。2>1 代表将 STDERR 重定向至一个名为 1 的文件中;而 2>&1 代表将 STDERR 重定向至 STDOUT。除此之外,当 & 出现在重定向符的左侧时,其表示全部重定向;例如,./test.sh &> tmp 代表将 test.sh 的标准输出流与标准错误流全部重定向至 tmp 文件。

  • 注意,command 1>file 2>filecommand 1>file 2>&1 并不是等价的。若使用前者,标准错误流与标准输出流会因抢占 file 文件的管道而出现乱码,缺失,覆盖的现象。所以当我们想将多个流重定向至同一个文件时,一定要使用 & 进行流间的重定向。

  • 标准输入输出的缺省:我们会注意到重定向符号 <> 的左侧有时是没有标识符的。事实上,<...0<... 是等价的,>...1>... 是等价的。

  • 一切皆是文件:之前我一直认为标准流与屏幕/键盘的关系是这样的:键盘“指向” STDINSTDOUTSTDERR “指向”屏幕。这是一个很自然的想法,因为我们潜意识中认为屏幕与键盘属于硬件,是应用端的一部分。但我在实践中找到了很多无法用这个想法解释的问题。

    查阅资料后我发现,标准输入流对应屏幕本身,标准输出流与标准错误流对应键盘本身。这是因为万物皆文件是 Unix/Linux 的基本哲学之一。在 Linux 系统中,即使不是文件,也以文件的形式来管理;例如所有的硬件设备,进程,套接字等都抽象成文件,使用统一的用户接口。

  • 有序性:流的重定向是按照输入顺序一步一步执行的。命令 command > file 2>&1 可以达到预期的效果;首先将标准输出流重定向至文件 file,再将标准错误流重定向至标准输出流,即文件 file;这样实现了标准输出流与标准错误流均重定向至 file

    而调转顺序,command 2>&1 1>file 则会出现错误;首先将标准错误流重定向至标准输出流,即屏幕,这一步实际上并没有作出任何改变。第二步将标准输出流重定向至文件 file。这样的写法无法令标准错误流重定向至文件 file

有了以上知识之后,我们就能理解 >&2 的含义了。它是 1>&2 的省略写法,代表着将 echo 命令的标准输出流重定向至标准错误流。而这一句 echo 命令是整个 number.sh 脚本的一部分,我们不妨直接理解为将该句 echo 命令的标准输出流重定向至 number.sh 的标准错误流。

所以,当 \(n=42\) 时:

1
2
3
4
5
6
7
8
# the result is based on the assumption that n=42
$ ./number.sh
Something went wrong
The error was using magic numbers
$ ./number.sh 2>tmp
Something went wrong
$ cat tmp
The error was using magic numbers

这里顺便贴一下完整的题目与答案:

Say you have a command that fails rarely. In order to debug it you need to capture its output but it can be time consuming to get a failure run. Write a bash script that runs the following script until it fails and captures its standard output and error streams to files and prints everything at the end. Bonus points if you can also report how many runs it took for the script to fail.

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash
cnt=0
while true; do
./number.sh &> tmp
if [[ "$?" -ne 0 ]]
then
break
fi
cnt=$[cnt+1]
done
echo "the number of runs: $cnt"
cat tmp

注意到这里使用 &> 进行全部重定向:number.sh 的标准输出流与标准错误流都将重定向至 tmp 文件中。


Problem 2 - pipes and xargs

这是 Missing Semester 第二课课后作业的另一个问题,也加深了我对用管道 pipe 对 shell 输入输出进行重定向的认识。题目原文如下:

As you have seen so far commands will take input from both arguments and STDIN. When piping commands, we are connecting STDOUT to STDIN, but some commands like tar take inputs from arguments. To bridge this disconnect there's the xargs command which will execute a command using STDIN as arguments. For example ls | xargs rm will delete the files in the current directory.

Your task is to write a command that recursively finds all HTML files in the folder and makes up a zip with them. Note that your command should work even if the files have spaces.

记得当初在学 ENGG1340 时被这个问题困扰了很久,现在才彻底弄清楚。有些命令从标准输入流 STDIN 中获取输入,有些从参数中指定的文件获取输入,而有些两者都支持。

以我们熟悉的 cat 命令为例:它连接参数中指定的文件并输出到标准输出流 STDOUT 中。

1
2
$ cat test
Hibike!

事实上,cat 也能从标准输入流 STDIN 中获取输入;在下面这个例子中,我们使用 pipe |echo 命令的标准输出流连接至 cat 命令的标准输入流。即使没有指定文件参数,cat 也能正常输出结果到屏幕。

1
2
$ echo "Euphonium" | cat
Euphonium

我们知道,cat 也能连接两个文件中的字符串并将其输出到 STDOUT 中;但是下例中命令的效果却没有达到预期。也就是说,cat 即使在 STDIN 非空的情况下,也会优先从文件参数中获取输入。

1
2
$ echo "Euphonium" | cat test
Hibike!

如何解决这一问题?我们使用参数 -:大多数命令都存在这样一个参数 - ,其表示从标准输入 STDIN 中进行读取。记得当时未老师教了我这个方法,不过我当时只是知其然而不知其所以然。

1
2
3
4
$ echo "Euphonium" | cat - test
Euphonium Hibike!
$ echo "Euphonium" | cat test -
Hibike! Euphonium

再例如,使用 - 参数,grep 命令也能够同时处理文件参数与 STDIN 中的输入:

1
2
3
$ echo "kumiko" | grep "ik" - test
(standard input):kumiko
test:Hibike!

可以看到,大多数命令一般先在命令行参数中读取要处理的内容,如果找不到则默认从标准输入流中读取。而参数 - 的利用使得命令可以将标准输入流中的内容当作命令行参数进行处理。但是说到底这还是取决于命令的内部实现;比如 rm 命令无论如何都只能删除命令行参数中指定的文件,而不从标准输入流中获取处理内容。

1
2
3
4
5
6
$ echo "test" | rm -f
$ ls
test
$ echo "test" | rm -f -
$ ls
test

但是有时我们的脚本需要类似 echo "test" | rm -f 这样的效果,这是一个很常见的需求,即删除标准输入流中指定的内容。解决方法有两种:

  • 拼接字符串得到命令:例如,rm -f $(echo "test")。这样的实现可以接受,但是多少有点尴尬。
  • 使用 xargs 命令:echo "test" | xargs rm -f

xargs 命令就是为了解决这个问题而诞生的;它将其标准输入流 STDIN 中的内容以空白符号 (包括空格,Tab,换行符等) 分割成若干个后当作命令行参数传递给其后的命令并运行。

1
2
3
4
5
6
$ cat test1 test2
Hibike!
Euphonium
$ echo "test1 test2" | xargs cat
Hibike!
Euphonium

有了这些知识之后,解决上面的问题就比较简单了:

1
find . -name "*.html" -print0 | xargs -0 zip -r html.zip


Reference

-----------------------------------そして、次の曲が始まるのです。-----------------------------------