跳转到内容

Bourne Shell 脚本/文件和流

来自维基教科书,开放世界中的开放书籍

Unix 世界:一个接一个的文件

[编辑 | 编辑源代码]

当您想到一台计算机及其相关的一切时,您通常会列出各种不同的东西。

  • 计算机本身
  • 显示器
  • 键盘
  • 鼠标
  • 您的硬盘驱动器以及其中的文件和目录
  • 连接到互联网的网络连接
  • 打印机
  • DVD 播放器
  • 等等

让您感到意外的是:Unix 没有这些东西。好吧,几乎没有。Unix 当然有 _文件_。Unix 有无穷无尽的文件。由于 Unix 有文件,它也具有“文件之间”的概念(这样想:如果您的宇宙只包含盒子,您会自动知道没有盒子的空间)。但 Unix 除了这些什么都不知道。整个(Unix)宇宙中的所有东西都是文件。

一切都是文件。即使是一些很难想象成文件的东西,也是文件。您的(数据)文件是文件。您的目录是文件。您的硬盘驱动器是文件。您的 _键盘_、_显示器_ 和 _打印机_ 都是文件。是的,真的:您的键盘是无限大小的只读文件。您的显示器和打印机是无限大小的只写文件。您的网络连接是可读写文件。

在这一点上,您可能想知道:_为什么?_ 为什么 Unix 系统的设计者会想出这种疯狂的想法?为什么所有东西都是文件?答案是:因为如果所有东西都是文件,那么您可以 _像文件一样对待_ 所有东西。或者换句话说,您可以以相同的方式对待 Unix 世界中的所有东西。并且,正如我们很快就会看到的那样,这意味着您也可以 _使用文件操作来组合_ 几乎所有东西。

在我们继续之前,再给您增加一层怪异:Unix 中的所有东西都是文件。包括运行程序的 _进程_。实际上,这意味着运行的程序也是文件。包括您一直在运行以练习脚本编写的交互式 shell 会话。是的,真的,带有闪烁光标的文本屏幕也是文件。我们也可以证明这一点。您可能还记得,在关于 运行命令 的章节中,我们提到过您可以使用 Ctrl+d 键组合退出 shell。因为这个组合会生成 Unix 字符用于... 没错,文件结束符!

流:文件之间传递的内容

[编辑 | 编辑源代码]

正如我们在上一节中提到的,Unix 中的所有东西都是文件——除了 _文件之间_ 的东西。在文件之间,Unix 定义了一种机制,允许数据逐位地从一个文件移动到另一个文件:_流_。流字面上就是它的意思:从一个文件流向另一个文件的一小条数据流。实际上,桥梁可能是一个更好的名字,因为与流(它是持续的水流)不同,文件之间的数据流不一定持续,甚至可能根本不使用。

标准流

[编辑 | 编辑源代码]

在 Unix 世界中,一般约定是每个文件都连接到至少三个流(这是因为对于那些实际上是进程或正在运行的程序的文件来说,这个数字是最有用的)。可以有更多流,实际上每个文件都可以连接到任意数量的流(例如,程序可以打印并打开网络连接)。但是,所有文件都有三个基本流,即使它们可能并不总是可用或使用。这些流被称为“标准”流

标准输入 (stdin)
文件的标准输入流。
标准输出 (stdout)
文件的标准输出流。
标准错误 (stderr)
文件的标准错误输出流。

正如您可能猜到的那样,这些流非常适合那些实际上是系统进程的文件。实际上,许多编程语言(如 C、C++、Java 和 Pascal)在其标准 I/O 操作中使用完全相同的约定。由于 Unix 操作系统家族在系统定义的核心部分包含了这些流,因此这些流也是 Bourne Shell 的核心。

在脚本中获取标准流

[编辑 | 编辑源代码]

所以现在我们知道 Unix 中有一个基本的输入和输出通用机制;但是您如何在脚本中获取这些流?您必须做什么才能将您的脚本连接到标准输出,或者从标准输入读取数据?好消息是:什么也不需要。您的脚本会自动连接到运行它们的进程的标准输入、输出和错误流。当您读取输入时,它会自动从标准输入获取。您的输出会直接发送到标准输出。程序错误会直接发送到标准错误。实际上,您已经使用过这些流:到目前为止,每个打印过任何内容的示例都是通过脚本的标准输出流完成的。

那么交互模式下的 shell 呢?它也使用这些标准流吗?是的,它使用。在交互模式下,标准输入流连接到键盘文件。标准输出和标准错误连接到显示器文件。

好的... 但有什么用呢?

[编辑 | 编辑源代码]

到目前为止,关于文件和流的讨论非常有趣,并且对 Unix 的深度洞察力。但是,了解这一切对您有什么用呢?啊,问得好!

Bourne Shell 有一些内置功能,允许您完成一些涉及文件及其流的巧妙技巧。您看,文件不仅仅有流——您还可以交叉连接两个文件的流。在上一节的最后,我们说交互式会话的标准输入连接到键盘文件。实际上,它连接到键盘文件的 _标准输出_ 流。交互式会话的标准输出和错误连接到显示器文件的 _标准输入_ 流。因此,您可以将交互式会话的流连接到设备的流。

等等。你还记得上面关于 Unix 将所有内容都视为文件,意味着所有内容都像文件一样处理的评论吗?这就是它重要的原因:你可以将来自 *任何* 文件的流连接到 *任何* 其他文件的流。你可以将交互式 shell 会话连接到打印机或网络,而不是连接到监视器(或者除了监视器之外)。你可以运行一个程序,并将它的输出直接发送到打印机,方法是重新连接程序的标准输出流。你可以将一个程序的标准输出流直接连接到 **另一个程序** 的标准输入流,并创建程序链。Bourne Shell 使这一切变得非常简单。

你是否突然感觉好像把手指插进了电源插座?这就是 shell 的强大力量流经你身体的感觉……

重定向:在 shell 中使用流

[edit | edit source]

正如上一节所述,shell 进程通过标准流连接到(默认情况下)键盘和监视器。但很多时候你可能想要改变这种连接。将文件连接到流是一个非常常见的操作,所以你可能会期望它被称为“连接”或“链接”。但由于 Bourne Shell 有默认的连接,并且你所做的一切都是对默认连接的 *改变*,所以使用 shell 将文件连接到(不同的)流实际上被称为 **重定向**。

Bourne Shell 中内置了几个与重定向相关的操作符。最基本也是最通用的一个是管道操作符,我们将在后面详细介绍。其他的都与重定向到文件有关。

重定向到文件

[edit | edit source]

正如我们在上一节解释(或者更确切地说:暗示)的那样,Bourne Shell 在 Unix 操作系统之上的一个极其强大的功能是能够将程序串联在一起。执行一个程序,让它生成输出,然后自动将该输出作为输入发送到另一个程序。可能的组合是无限的,你所能实现的功能的强大程度也是无限的。

你可能想要发送程序输出的最常见地方之一是文件系统中的文件。而这一次,文件指的是一个普通的、传统的文本文件,而不是 Unix 的“一切都是文件,包括你的硬件”的文件。为了实现这一点,你可以想象我们可以使用上面描述的串联机制:让一个程序通过标准输出流生成输出,然后将该流(即 *重定向输出*)连接到一个程序的标准输入流,该程序在文件系统中创建一个文本文件。这样做绝对有效。但是,重定向到文本文件是一个如此常见的操作,你不需要一个单独的链末端程序来完成它。重定向到文件直接内置在 Bourne Shell 中,通过以下操作符实现

process > data file
process 的输出重定向到文本文件;如果需要,创建文件,否则覆盖其现有内容。
process >> data file
process 的输出重定向到文本文件;如果需要,创建文件,否则追加到其现有内容。
process < data file
读取文本文件的内容,并将这些内容作为输入重定向到 process


重定向输出

[edit | edit source]

让我们通过一些示例仔细看看这些操作符。以以下名为 'hello.sh' 的简单 Bourne shell 脚本为例

一个生成一些输出的简单 shell 脚本
#!/bin/sh
echo Hello


此代码可以使用在 运行命令 一章中描述的任何方式运行。当我们运行脚本时,它只是将字符串 "Hello" 输出到屏幕,然后将我们返回到提示符。但是,假设我们想要将输出重定向到文件而不是屏幕。我们可以使用重定向操作符来轻松实现这一点

将输出重定向到文本文件
$ hello.sh > myfile.txt
$


这一次,我们没有在屏幕上看到字符串 'Hello'。它到哪里去了呢?好吧,它正处在我们想要它去的地方:名为 'myfile.txt' 的(新)文本文件。让我们使用 'cat' 命令检查一下这个文件

检查重定向一些输出的结果
$ cat myfile.txt
Hello
$


让我们再次运行该程序,这次使用 '>>' 操作符,然后再次使用 'cat' 命令检查 'myfile.txt'

使用追加重定向进行重定向
$ hello.sh >> myfile.txt
$ cat myfile.txt
Hello
Hello
$


你可以看到,'myfile.txt' 现在包含两行——输出已添加到文件的末尾(或连接);这是由于使用了 '>>' 操作符。如果我们再次运行脚本,这次使用单个大于号操作符,我们将得到

使用覆盖重定向进行重定向
$ hello.sh > myfile.txt
$ cat myfile.txt
Hello
$


只有一个 'Hello' 再次出现,因为 '>' 将始终覆盖现有文件的內容(如果有)。

重定向输入

[edit | edit source]

好的,很明显我们可以将输出重定向到文本文件。但从文本文件中读取数据呢?这也很常见。Bourne Shell 也在这里帮助我们:整个从文件读取数据并将其输入到流中的过程被 '<' 操作符捕获。

默认情况下,'stdin' 从你的键盘输入;运行 'cat' 命令,不带任何参数,它将一直等待,直到你输入一些东西

cat ???
$ cat
我可以在这里一整天输入,但似乎永远无法从
这台愚蠢的机器上得到我的提示符。

我甚至按了几次回车键 !!!
.....等等....等等


实际上,'cat' 会一直等待,直到你输入 'Ctrl+D'(简称 'End of File Character' 或 'EOF')。要从其他地方重定向标准输入,请使用 '<'(小于号操作符)

重定向到标准输入
$ cat < myfile.txt
Hello
$


所以 'cat' 现在将从文本文件 'myfile.txt' 中读取数据;文件末尾也会生成 'EOF' 字符,因此 'cat' 将像以前一样退出。

请注意,我们之前以这种格式使用过 'cat'

$ cat myfile.txt


它在功能上与

$ cat < myfile.txt


相同。但是,这两种机制从根本上是不同的:一种使用命令的参数,另一种更通用,它重定向 'stdin'——这就是我们在这里关心的。使用文件名作为 'cat' 的参数更方便,这也是 'cat' 的发明者将其添加进去的原因。但是,并非所有程序和脚本都接受参数,所以这只是一个简单的例子。

组合文件重定向

[edit | edit source]

可以在一行中重定向 'stdin' 和 'stdout'

同时重定向 'cat' 的输入和输出
$ cat < myfile.txt > mynewfile.txt


上面的命令将复制 'myfile.txt' 的内容到 'mynewfile.txt'(并将覆盖 'mynewfile.txt' 的任何先前内容)。这再次只是一个方便的例子,因为我们通常会使用 'cp myfile.txt mynewfile.txt' 来实现这种效果。

重定向标准错误(和其他流)

[edit | edit source]

到目前为止,我们已经查看了与文件相关的“正常”标准流的重定向,即在一切按计划进行且没有错误时使用的文件。但另一个流呢?用于错误的流?我们如何重定向它?例如,如果我们想将错误数据重定向到日志文件。

例如,考虑 ls 命令。如果你运行 'ls myfile.txt' 命令,它只会列出文件名 'myfile.txt'——如果该文件存在。如果文件 'myfile.txt' *不存在*,'ls' 会将错误返回到 'stderr' 流,默认情况下,在 Bourne Shell 中,它也连接到你的监视器。

所以,让我们运行 'ls' 几次,首先在一个存在的文件上,然后在一个不存在的文件上

列出一个存在的文件

代码:

$ ls myfile.txt

输出:

myfile.txt
$

然后

列出不存在的文件

代码:

$ ls nosuchfile.txt

输出:

ls: 没有这样的文件或目录
$

再次,这次只重定向 'stdout'

尝试重定向...

代码:

$ ls nosuchfile.txt > logfile.txt

输出:

ls: 没有这样的文件或目录
$

我们仍然看到错误消息;'logfile.txt' 将被创建,但为空。这是因为我们现在重定向 stdout 流,而错误消息写入错误流。那么我们如何告诉 shell 我们想重定向错误流呢?

为了理解答案,我们必须再讲一些关于 Unix 文件和流的理论知识。你看,我们可以用简单的运算符重定向 stdin 和 stdout 的根本原因是,重定向这些流非常常见,shell 让我们对这些流使用简写符号。但实际上,为了完全正确,我们应该在每种情况下都告诉 shell 我们想要重定向的是哪个流。通常情况下,shell 无法知道:可能会有大量流连接到任何文件。为了区分这些流,每个连接到文件的流都关联着一个数字:按照惯例,0 是标准输入,1 是标准输出,2 是标准错误,其他任何流的编号都以此类推。要重定向任何特定的流,请在重定向运算符之前加上流号(称为文件描述符)。因此,要重定向我们示例中的错误消息,我们将在重定向运算符之前加上 2,表示 stderr 流。

重定向 stderr 流

代码:

$ ls nosuchfile.txt 2> logfile.txt

输出:

$

没有输出到屏幕,但如果我们检查 'logfile.txt'

检查日志文件

代码:

$ cat logfile.txt

输出:

ls: 没有这样的文件或目录
$

正如我们之前提到的,没有数字的运算符是简写符号。换句话说,

$ cat < inputfile.txt > outputfile.txt


实际上是

$ cat 0< inputfile.txt 1> outputfile.txt


我们也可以像这样独立地重定向 'stdout' 和 'stderr'

$ ls nosuchfile.txt > stdio.txt 2>logfile.txt
$


'stdio.txt' 将为空,'logfile.txt' 将包含之前一样的错误。

如果我们想将 stdout 和 stderr 重定向到同一个文件,我们也可以使用文件描述符

$ ls nosuchfile.txt > alloutput.txt 2>&1


这里的 '2>&1' 意味着类似于 '将 stderr 重定向到 stdout 所重定向的同一个文件'。小心顺序!如果你这样做

$ ls nosuchfile.txt 2>&1 > alloutput.txt


你将把 stderr 重定向到 stdout 指向的文件,然后将 stdout 发送到其他地方 - 并且这两个流最终将被重定向到不同的位置。

特殊文件

[edit | edit source]

我们之前说过,到目前为止讨论的所有重定向运算符都重定向到数据文件。虽然这在技术上是正确的,但 Unix 的魔法意味着不仅仅是这样。你看,Unix 文件系统往往包含一些被称为“设备”的特殊文件,按照惯例,这些文件都收集在 /dev 目录中。这些设备文件包括代表你的硬盘驱动器、DVD 播放器、USB 存储器等的那些文件。它们还包括一些特殊文件,如 /dev/null(也称为垃圾桶;写入该文件的任何内容都会被丢弃)。你也可以将数据重定向到设备文件,就像重定向到普通数据文件一样。这里要小心;你真的不想将原始文本数据重定向到硬盘驱动器的引导扇区(而且你可以这样做!)。但是,如果你知道自己在做什么,就可以通过重定向到设备文件来使用它们(例如,这就是在 Linux 中刻录 DVD 的方式)。

作为一个例子,说明你可能实际使用设备文件的方式,在 Unix 的 'Solaris' 版本中,扬声器及其麦克风可以通过文件 '/dev/audio' 访问。因此

# cat /tmp/audio.au > /dev/audio

将播放声音,而

# cat < /dev/audio > /tmp/mysound.au


将录制声音(你需要使用 CTRL-C 来完成...)。

这很有趣

# cat < /dev/audio > /dev/audio


现在,一边喊叫一边挥动麦克风 - 就像吉米·亨德里克斯那样的反馈。太棒了。你可能需要以 'root' 用户身份登录才能尝试这样做。

一些重定向警告

[edit | edit source]

敏锐的读者会注意到上面的讨论中有一两件事。首先,一个文件可以拥有不止与之关联的标准流。重定向这些流合法吗?这甚至可能吗?答案是,技术上来说是可能的。你可以重定向文件中的第 4 或第 5 个流(如果它们存在)。不过不要尝试这样做。如果有不止几个流,你将无法知道自己重定向的是哪个流。另外,如果一个程序需要不止标准流,那么这个程序也很有可能需要将额外的流发送到特定位置。

其次,你可能已经注意到文件描述符 0 是标准输入流。这是否意味着你可以将程序的标准输入重定向远离程序?你可以执行以下操作吗?

$ cat 0> somewhere_else


答案是,可以。如果你这样做,事情就会崩溃。

管道、三通和命名管道

[edit | edit source]

所以,在讨论了所有这些关于重定向到文件的知识之后,我们终于要开始讨论它了:通过交叉连接流进行的通用重定向。这是重定向的最通用形式,也是最强大的形式。它被称为管道,使用管道运算符 '|' 执行。管道允许你通过“管道”将两个进程连接在一起,该管道直接将一个文件的 stdout 连接到另一个文件的 stdin。

举个例子,让我们考虑一下 'grep' 命令,它在给定关键字和要搜索的文本的情况下返回匹配的字符串。让我们也使用 ps 命令,它列出机器上正在运行的进程。如果你执行

$ ps -eaf


它通常会列出你机器上运行的多个页面,你需要手动筛选这些页面才能找到你想要的内容。假设你正在寻找一个你已知包含单词 'oracle' 的进程;使用 'ps' 的输出通过管道传递到 grep,grep 将只返回匹配的行

$ ps -eaf | grep oracle


现在你只会收到你需要的行。如果仍然有很多行怎么办?没问题,将输出通过管道传递到命令 'more'(或 'pg'),如果屏幕满了,它会暂停你的屏幕

$ ps -ef | grep oracle | more


如果要杀死所有这些进程怎么办?你需要 'kill' 程序,以及每个进程的进程号(ps 命令返回的第二列)。很简单

$ ps -ef | grep oracle | awk '{print $2}' | xargs kill -9


在这个命令中,'ps' 列出进程,'grep' 将结果缩小到 oracle。'awk' 工具提取每行中的第二列。'xargs' 将每一行作为命令行参数,一次一行地提供给 'kill'。

管道可以用来链接任意数量的程序,只要在合理范围内(我们不知道这些范围是什么!)

不要忘记你仍然可以将重定向器组合在一起

$ ps -ef | grep oracle > /tmp/myprocesses.txt


还有另一个可以与管道一起使用的有用机制:'tee'。要理解 'tee',想象一个形状像 'T' 的管道 - 一个输入,两个输出

$ ps -ef | grep oracle | tee /tmp/myprocesses.txt


'tee' 将复制传递给其 stdin 的任何内容,并将此内容重定向到给定的参数(文件);然后,它还会将另一个副本发送到其 stdout - 这意味着你可以有效地截取管道,在此阶段获取副本,然后继续通过其他命令进行管道传输;这可能对输出到日志文件和复制到屏幕很有用。

关于管道命令的一点说明:管道进程在 Unix 环境中并行运行。有时,一个进程会被阻塞,等待另一个进程的输入。但原则上,管道中的每个进程都与所有其他进程同时运行。

命名管道

[edit | edit source]

我们一直在讨论的内联管道有一种变体,叫做“命名管道”。命名管道实际上是一个拥有自己的 'stdin' 和 'stdout' 的文件 - 你可以将进程附加到这些文件。这对于允许程序相互通信非常有用,尤其是在你不知道一个程序何时会尝试与另一个程序通信(等待备份完成等)以及不想编写复杂的基于网络的监听器或执行笨拙的轮询循环的情况下。

要创建“命名管道”,可以使用 'mkfifo' 命令(fifo=先入先出;因此数据以写入时的相同顺序读出)。

$ mkfifo mypipe
$


这将创建一个名为 'mypipe' 的命名管道;接下来,我们可以开始使用它。

此测试最好使用两个登录的终端运行

1. 从“终端 a”

$ cat < mypipe


'cat' 将坐在那里等待输入。

2. 从“终端 b”

$ cat myfile.txt > mypipe
$


这应该会立即完成。切换回“终端 a”;现在它将从管道中读取并接收 'EOF',你将在屏幕上看到数据;命令将完成,你将回到命令提示符。

现在尝试相反的方式

1. 从终端 'b'

$ cat myfile.txt > mypipe


这将一直等待,因为管道另一端没有其他进程来“清空”它 - 它被阻塞了。

2. 从终端 'a'

$ cat < mypipe


与之前一样,两个进程现在都将完成,输出显示在终端 'a' 上。

这里文档

[edit | edit source]

到目前为止,我们已经讨论了从数据文件重定向到数据文件以及交叉连接数据流。所有这些 shell 机制都基于拥有数据 "物理" 源——一个进程或一个数据文件。然而,有时你可能想向目标提供一些数据,而没有源提供它。在这种情况下,你可以使用一个名为 "Here Document" 的 "动态" 文档。Here Document 意味着你打开一个虚拟文本文档(在内存中),像往常一样在其中输入内容,关闭它,然后将其视为任何普通文件。

创建 Here Document 是通过使用输入重定向操作符的变体来完成的:"&<<" 操作符。与输入重定向操作符一样,Here Document 操作符也接受一个参数。对于输入重定向操作符,这个操作数是要输入的文件的名称。对于 Here Document 操作符,它是用于终止 Here Document 的字符串。因此,使用 Here Document 操作符看起来像这样

目标 &<< 终止符字符串

Here Document 内容

终止符字符串


例如

使用 Here Document

代码:

cat << %%
> This is a test.
> This test uses a here document.
> Hello world.
> This here document will end upon the occurrence of the string "%%" on a separate line.
> So this document is still open now.
> But now it will end....
> %%

输出:

这是一个测试。
这个测试使用了一个 Here Document。
你好世界。
这个 Here Document 将在单独的行上出现 "%%" 字符串时结束。
所以现在这个文档仍然是开放的。
但现在它将结束……

当在使用 Here Document 与变量或命令替换结合使用时,重要的是要意识到替换是在 Here Document 被传递之前进行的。例如

使用带有替换的 Here Document

代码:

$ COMMAND=cat
$ PARAM='Hello World!!'
$ $COMMAND <<%
> `echo $PARAM`
> %

输出:

你好世界!!


下一页: 模块化 | 上一页: 控制流
首页: Bourne Shell Scripting
华夏公益教科书