[edit this page]

命令与参数

我要如何给 bash 下指令?

关于什么是命令,如何发布他们;交互模式与脚本;命令语法,通过名称搜索命令与程序;参数与单词分割,以及输入和输出重定向

[edit this page]
本章的写作已完成,请享用。如果你发现任何值得改善的地方,请告知我们。
如果你对本篇指南的进展感兴趣,可以点击这里为源仓库加星。

Bash 命令是什么,我要如何编写与发布他们?

从开篇到这里,关于 bash 和其他进程怎样在终端内协同工作,我们已了解很多。现在让我们重新聚焦 bash,搞清楚到底怎样用它把事情做成。

之前已曾提及,bash 会等待你发出指令,然后尽它所能去执行他们。为了最大程度地用好 bash,同时,也为了避免因 bash 错误理解你的意图而损害系统,你一定要对 bash shell 语言的基础概念和用法特别留意。有非常多自以为熟悉 bash 的人,其实对最基本的概念都理解有误。结果就是,他们编写的程序可能会对毫无戒备的用户及系统造成严重损害。不要做这样的人

那么什么是 bash 命令?

Bash shell 语言的核心就是命令。你发出的命令会一步步、逐条告诉 bash 你想让它做什么。

bash 通常每次从你那接收一条命令,执行它,结束后返回等待你的下一条命令。我们将这种模式称为 同步 执行命令(synchronous command execution)。一定要理解,当 bash 忙于执行你的某条命令时,你将暂时无法再和它直接交互,需等它执行完当前命令后返回继续待命。对于绝大多数命令,你其实根本不会注意到这点,因为 bash 执行命令的速度如此之快,远在你察觉前它就已经返回等待下一条命令了。

不过,有一些命令的执行时间会比较长,特别是启动其他程序与你交互的命令。例如,某条命令可能会启动一个文档编辑器。当你与这个文档编辑器交互的时候,bash 就会退居幕后,等待文档编辑器程序结束运行(通常意味着你从中退出)。当文本编辑器停止运行后,意味着该条命令执行结束,bash 将返回等待你下一步操作的命令。你还会注意到,在文档编辑器运行的时候,bash 提示符就消失不见了;一旦你退出编辑器,bash 提示符又会再次出现:

$ ex启动 “ex” 程序的 bash 命令
: iex 命令,表示“插入”(insert)文本
Hello!
.如果一行内只有一个 . ,ex 会停止文本插入
: w greeting.txtex 命令,将文本“写入”(write)文档
"greeting.txt" [New] 1L, 7C written
: qex 命令,表示“退出”
$ cat greeting.txt从 ex 又回到 bash!
Hello!“cat“ 程序会显示文档内容
$ 

注意看上面这部分,我们首先向 bash 发出启动 ex 文档编辑器这条命令。命令发出后,我们终端内显示的提示符就发生了变化:此时我们输入的任何文本都是发送给 ex 程序的,而非 bash。当 ex 运行的时候,bash 就进入休眠,等待直到 ex 结束。当你使用 q 命令退出 ex,第一条 ex bash 命令终结束运行,bash 又开始等待接收新的命令。为了让你知道它又回来待命了,bash 提示符会再次出现,使你可以键入下一条命令。在上面例子中,我们最后以 cat greetings.txt 这条 bash 命令作结,让 bash 启动 cat 程序。cat 程序非常利于输出文件内容(它是 concatenate 的缩写,因为它的目的就是一个接一个地连缀输出你给它的所有文件的内容)。在这个例子中,cat 命令是用来查看并显示我们用 ex 程序编辑过的 greetings.txt 文档内的内容。

我要如何向 bash 发送命令?

因为前面我们已给出一些 bash 命令的示例,所以关于如何向 bash 发送基本的指令,你很可能已有正确的认识。

Bash 是一种基于行(line-based)的语言。相应地,当 bash 读取你的命令时,它也是一行一行地读。绝大多数命令都只占据一行,除非你 bash 命令的语法在行末明确表明该行命令还未完结会延续至下一行,否则 bash 就会立即将行末理解为命令输入的终止点。因此,输入一行文本并按下 (回车)的行为,通常就会引发 bash 启动执行你这行文本所表述的命令。

然而,有一些命令确实会跨越多行,他们通常都是命令块(block commands)或是命令中含有引用:

$ read -p "Your name? " name这条命令是完整的,可以立即被执行
Your name? Maarten Billemont
$ if [[ $name = $USER ]]; then"if"块开始但尚未结束
>     echo "Hello, me."
> else
>     echo "Hello, $name."
> fi到此“if”块结束,bash知道要开始执行命令了
Hello, Maarten Billemont.

逻辑上,如果 bash 对于它要做的事尚未掌握全部信息,那么它是无法执行一条命令的。在上面例子中(我们后面还会更具体地介绍这些命令在做什么),if 命令的首行没有包含足够完整的信息,bash 不知道如果测试通过了或是如果测试失败了,接下来要做什么。因此,bash 显示了一个特殊的提示符:>。这个提示符其实就是在说:你给我的命令尚未完结。我们后面又为这条命令输入了更多行的字符,直到抵达 fi。当我们结束那一行的输入,bash 终于知道你已完整给出了条件结果案例。Bash 就会立即开始执行整个命令块中的代码,从 iffi。我们很快就会看到 bash 语法中定义的不同种类的命令,前面例子中的 if 命令被称作 复合命令(Compound Command),因为它将一组基本的指令复合成为一个更大的逻辑组块。

在以上示例中,我们都是把命令发送给一个交互式的 bash。正如前面解释过的,bash 也能以非交互式的模式从文件或流中读取命令,而无需向你索要。在非交互模式下,bash 没有命令提示符。除此之外,它的操作与交互模式基本相同。例如,我们就可以把上面例子中的代码复制到一个文件中:

read -p "Your name? " name
if [[ $name = $USER ]]; then
    echo "Hello, me."
else
    echo "Hello, $name."
fi

至于你如何命名这个文件,并不重要。假设你将上面这些代码保存在 hello.txt 文件中,现在我们就可以使用 bash 直接运行文件中的命令,无需再向我们索要:

$ bash hello.txt启动一个新的 “bash” 进程
Your name? Maarten Billemont
Hello, Maarten Billemont.当文件中没有余下未执行的代码后,新启动的 “bash” 进程就结束
$ bash 命令执行结束后,交互式 bash 再次返回

注意上面这个例子中有两个 bash 进程。开始时,我们先启动了常用的交互式 bash shell。然后我们让这个 bash 进程执行一条命令,从而启动了一个新的 bash 进程。第二个进程将会以非交互式的方式执行它在 hello.txt 文件中读取到的所有命令。执行结束后(即文件中没有余下未执行的命令),非交互式的 bash 进程终止,交互式的 bash 进程完成了它对 bash hello.txt 命令的执行,进而提示符再次出现,等待运行你的下一条命令。

从包含一列命令的文档到一份真正意义上的bash 脚本(script),只需再迈出一小步。使用你喜欢的编辑器,再次打开 hello.txt 文档,在它的最上端添加一个 hashbang 作为脚本的首行:#!/usr/bin/env bash

#!/usr/bin/env bash
read -p "Your name? " name
if [[ $name = $USER ]]; then
    echo "Hello, me."
else
    echo "Hello, $name."
fi

祝贺你!你已创建了自己的第一份 bash 脚本。什么是 bash 脚本?就是其中含有 bash 代码的文档,系统内核可以像执行电脑中其他程序那样执行它。本质上,它就是一个程序,不过需要 bash 解释器将 bash 语言翻译为系统内核可以理解的指令。这就是为什么我们要在文档首行插入“hashbang”:它告诉系统内核需要用哪种解释器来理解并翻译这份文档中的语言,以及在哪里可以找到这种解释器。我们之所以称呼为“hashbang”是因为,bash 脚本总是从一个“hash”开始,后面紧跟着一个“bang” !。之后你的 hashbang 必须指定一条绝对路径,指向能理解、翻译文档中语言的程序,并且可以接收一个参数。我们这里的hashbang有些特殊:我们指向的程序 /usr/bin/env,其实并不是一个可以理解 bash 语言的程序。它实际是一个可以找到并启动其他程序的程序。在这个例子中,我们通过使用一个参数(argument)告诉这个程序去找到 bash 程序,并用它来解释翻译我们脚本中的语言。那为什么要使用这样一个叫做 env 的中间程序呢?全部的原因都在于这个名字前面的东西:路径(path)。我们很有把握地知道 env 这个程序生活在 /usr/bin 路径下,但是考虑到整个操作系统以及配置的庞杂,我们很可能不清楚 bash 这个程序被安装在哪里。这就是为什么我们会使用 env 这个程序来帮助我们找到它。有些复杂是吧!那么在加上 hashbang 之前与之后,我们的文档有什么不同吗?

$ chmod +x hello.txt将 hello.txt 标识为可被执行(executable)的程序
$ ./hello.txt让 bash 启动 hello.txt 程序

在内核允许将某个文档作为程序执行之前,绝大多数的系统都要求你首先将这个文档标识为 可执行(executable)。如此操作之后,我们就可以像启动其他程序那样启动 hello.txt。内核会读取这份文档,找到 hashbang,通过它找到 bash 解释器,最终使用 bash 解释器运行文档中的指令。现在你就有了自己的第一个 bash 程序!

学习说“bash”

如果你有认真阅读前面部分的内容,那应该对 bash 是什么、在系统的哪个位置、如何工作,以及如何使用它,都已有清楚的认识。

现在是时候开始学说“bash”了。接下来我们会介绍 bash shell 语言的语法,因此这份指南会变得更有技术性(technical)。但是不要担心,保持专注,你不会掉队的。如果觉得不安或不踏实,可以重读前面的内容再看后续的部分,以防彻底迷失。我们会尽可能覆盖新概念相关的所有“如何”(how's)或“为什么”(why's)的问题。如果仍有任何不清楚的地方,也欢迎你联系我们,这样我们就能为你以及之后其他学生改进这篇指南。我们的联系信息在指南的篇首。

关于意图与模糊性

相比和人类对话,与计算机对话最大的不同在于,计算机程序通常都不擅长把你的需求放在一个背景下去揣摩你的意图是什么。那些尝试去做,且真的能够基于模糊的输入、费尽周折搞清楚预期结果的程序,通常会被称为“聪明”。不幸的是,这种情况下的“聪明”,与我们对“聪明”人的期待是不同的:基于我们含糊不清的输入,计算机程序作出的推测常与实际相差甚远,并经常导致糟糕甚至灾难性的结果。

麻烦地是,我们人类却习惯于模糊不清地讲话:我们依赖于接收者能理解我们所提需求的背景(context),清楚我们最期待的行动是什么。当我们向伴侣要盐的时候,并不是真地要他们给我们一勺盐:而是期待他们理解我们的实际意图是让他们往饭菜中加一点盐,期待当他们把饭菜端给我们的时候,上面至少洒了一点点盐。

在开始与计算机程序对话之前,首先要认识到我们日常语言以及需求中的模糊性,进而在之后学习消除这种模糊性。如果你对此缺少经验,这很可能就是你前进过程中最大的挑战。按字面意思思考是需要练习的。下面这个技巧应该有用:想象我们是在和一个三岁大的小朋友讲话,每一次都要像第一次那样,教他们做你想让他们做的事。当只是说把那本动物书拿过来 还不够的时候,我们需要分步骤教他们:向四周看看,看到你身后的书了吗?棒!你能找到那本封面上有狮子和牛的书吗?就是那本,把它拿给我!好孩子,把它拿给爸爸!过来这边。嗨,你真棒,把书给我,坐下吧,我们一起来阅读。。一定程度上,写 bash 脚本就像是在教你的系统执行某项任务。区别在于,你三岁大的孩子自己就能在新的需求中辨识出过去习得的经验,但你的系统不会,每次你都需要明确指定它重复运行之前编写的任务描述代码。

一些语言解释器(类似 bash,解释器是一种可以理解语言的程序),试图通过严格地限定自己的语法来规避以上问题。背后的理念就是去除你语言中的模糊不清来避免系统不小心做错什么。解释器强制性地将表达的准确性限定到一定程度。这是一种相对成功的策略,通常会产出 bug 最少的程序。

可惜的是,bash 不是一个严格的解释器。
实际上,bash 的宽容在很大程度上导致大多数编写 bash 脚本的人能力都不合格,无论是新手还是专业技术人员。结果就与世纪之交网站开发的状态非常相似:许多网页的代码都写得极为糟糕,以至于严重限制他们在任一种标准浏览器内的正常渲染,这就倒逼浏览器使用各种所谓“聪明”的手段,努力以网页开发者期待的效果去渲染,而不是严格遵从他们实际写下的代码。类似地,你即将遇到的大多数bash 脚本都是 有bug的(buggy)。有些是轻微的,但经常能到这种程度:仅仅将它用在一个名字稍不太寻常的文件上,就可能对你的系统造成不可逆的破坏。

不要做这样的人。
这份指南的存在是要教你写出好的 bash 代码。它将赋能你准确传达自己的真实意图,并让计算机解决你的问题。既然 bash 是一种松懈的解释器,纪律的责任就在你身上。如果你不打算尊重这个前提,我建议你现在就停止阅读,另找一种严格的解释器。世界上已经有太多糟糕的 bash 代码,这份指南拒绝帮助人输出更多。

Bash 命令的基本语法

在最高水平上,bash 有几类不同的命令。我们接下来会解释每一种类型,给出一个简单的示例,然后在后续部分更深入地介绍每一类命令。现在不要太担心这些命令的语法,当我们之后专注到每种命令类型时,他们就会变得清晰。当前你只需要对 bash 命令的多种类型、规模以及不同语法,形成初步整体性的理解。

简单命令(Simple Commands)

这是最常见的命令类型。命令中会指定要执行的命令名称,后面跟着可选的 参数(arguments)环境变量(environment variables)文件描述符重定向(file descriptor redirections)

    [ var=value ... ] name [ arg ... ] [ redirection ... ]

echo "Hello world."
IFS=, read -a fields < file

在命令的名称之前,你可以 选择性 赋值一些 变量(var)。这些变量仅适用于这一次命令的执行环境。关于变量和环境,后面我们还会深入介绍。

命令的 名称(name) 是第一个单词(在选择性的变量赋值之后)。Bash 会找到这个名称对应的命令并启动它。我们后面还会介绍被命名的命令都有哪些种类,以及 bash 如何找到他们。

命令的名称后面可以选择性地接一列 参数(arg) ,我们很快就会学习什么是参数,以及他们的语法。

最后,命令中也可以有一组施加于它的 重定向(redirection) 设置。还记得我们前面对文件描述符的解释吗,重定向就是改变文件描述符插件指向的操作。他们会改变与命令进程相连接的流(stream)。在后面章节中我们会学习到重定向的作用。

管道(Pipelines)

相比使用基本语法,bash 有很多“语法糖(syntax sugar)”,使一般任务执行起来更为轻松。管道就是一种你经常会用到的语法糖。通过将第一个进程的标准输出与第二个进程的标准输入相连,它可以很方便地将两个命令连接在一起。这是终端命令最常用的与其他命令对话并传递信息的方法。

    [time [-p]] [ ! ] command [ [|||&] command2 ... ]

echo Hello | rev
! rm greeting.txt

我们基本不用 time 关键词,但是对于了解执行命令所需的时间,使用它还是很方便的。

感叹号 ! 这个关键词初看可能有些奇怪,和 time 关键词类似,它和连接命令也没多大关系。当我们讨论条件及测试命令是否执行成功时,会再介绍它的作用。

语法中的第一个命令 command 与第二个命令 command2 可以是任何类型。bash 会分别为他们创建一个 subshell,并将第一个命令的标准输出文件描述符设置为指向第二个命令的标准输入文件描述符。这两个命令会同时运行,而 bash 会等待他们全部执行结束。我们在后面的章节中会解释“subshell”究竟指什么。

在两个命令之间,有这样一个符号 |。这个也被称作“管道”符,它会告诉 bash 将第一个命令的输出与第二个命令的输入相连。此外,在两个命令之间也可以使用 |& 符号,意思是除了标准输出外,把第一个命令的标准错误输出也与第二个命令的输入相连。但最好不要这样做,因为标准错误文件描述符通常用来传递消息给用户。如果我们把这些消息传给第二个命令而不是终端显示器,需确保第二个命令可以处理接收到的这些消息。

列表(Lists)

列表就是一组命令序列。本质上,脚本就是一个命令列表:一个接一个的命令。列表中的命令用控制运算符分开,而控制运算符会示意 bash 应该如何执行它前面的命令。

    command control-operator [ command2 control-operator ... ]
 
cd music; mplayer *.mp3
rm hello.txt || echo "Couldn't delete hello.txt." >&2

列表语法中的命令可以是这部分介绍的其他任何命令类型。

命令之后的 控制运算符 会告诉 bash 应该如何执行这条命令。最简单的控制运算符就是换行,等价于 ;,用来告诉 bash 运行这条命令,等待结束,然后执行列表中的下一命令。第二个例子使用了 || 这个控制运算符,它用来告诉 bash 正常运行在它前面的命令,但是运行结束后,只有当前面那条命令运行失败 才需要继续运行后面第二条命令。如果前面那条命令运行成功,|| 会让 bash 跳过它后面的那条命令。这对于命令运行失败后显示错误信息非常有用。在后面章节中,我们会深入了解所有的控制运算符。

注意,因为bash脚本本质上是一个由多行命令组成的列表,所以它是一个有效的命令列表,即在所有命令之间使用换行作为控制运算符。

复合命令(Compound Commands)

复合命令指的是命令中包含特殊语法。他们可以做很多不同的事,但在命令列表中整体作为一条命令行事。最明显的例子就是命令块:组块本身就像单独一个命令,但是内部还含有一些构成命令(sub commands)。复合命令的种类也有很多,我们后面还会深度介绍他们。

    if list [ ;|<newline> ] then list [ ;|<newline> ] fi
    { list ; }

if ! rm hello.txt; then echo "Couldn't delete hello.txt." >&2; exit 1; fi
rm hello.txt || { echo "Couldn't delete hello.txt." >&2; exit 1; }

上面这两个例子中的命令完成的是完全相同的操作。第一个例子是一个复合命令,第二个例子是一个命令列表中含有一个复合命令。前面已简单提及 || 运算符:除非它前面的命令运行失败,否则它右边的命令会被 bash 跳过不执行。这个例子很好地展示了复合命令的一个重要特点:他们行事就像命令列表中的单独一项命令。在第二个例子中,复合命令从左侧大括号 { 开始,一直到右侧大括号 } 结束,括号内部的所有代码被整体当作一个命令执行。也就是说,在第二个例子中,我们有一个包含两条命令的命令列表:rm 命令后面跟着 { ... } 这条复合命令。如果去掉大括号,就是包含 三个 命令的命令列表:rm 命令后面跟着 echo 命令,再然后是 exit 命令。有或是没有大括号主要会影响 || 运算符,它需要决定如果前面 rm 命令运行成功接下来要做什么。如果 rm 成功,|| 会使 bash 跳过它后面的命令。假设没有大括号,它后面就只有 echo 这一条命令需要跳过;而大括号将 echoexit 命令合并为一个复合命令,在 rm 运行成功后,|| 运算符会使 bash 同时跳过这两个命令。

协作进程

协作进程不过就是更多的语法糖:它使你可以轻松地异步(asynchronously)运行命令(不需要等待命令结束,也可以说是“在后台运行”),还能设置新的文件描述符插件来直接连接新命令的输入与输出。你不会经常用到协作进程,但是当你做一些高级复杂的任务时,使用他们会非常方便。

    coproc [ name ] command [ redirection ... ]

coproc auth { tail -n1 -f /var/log/auth.log; }
read latestAuth <&"${auth[0]}"
echo "Latest authentication attempt: $latestAuth"

上面这个例子中,启动了一个异步命令 tail。当它在后台运行时,其他的脚本命令持续运行。首先,脚本会从协作进程 auth 的输出中读取一行结果(也就是 tail 命令的第一行输出)。接下来我们编写一条消息,显示从协作进程中读取的最近一次认证尝试。这个脚本可以持续运行,每一次它都会从协作管道中读取信息,tail 命令会使它换行。

函数(Functions)

当你在 bash 中声明一个函数时,本质上你在创建一个临时的新命令,在后面的脚本中还可以再次召唤它。函数是一种非常好的方式,用来将一列命令结合成组并自定义命名,以便你在脚本中重复执行某个任务。

    name () compound-command [ redirection ]

exists() { [[ -x $(type -P "$1" 2>/dev/null) ]]; }
exists gpg || echo "Please install GPG." <&2

首先需为你的函数指定一个 名称(name),这就是你新命令的名称,之后只需用它写一句简单命令就可以运行。

在命令的名称之后是一对括号 ()。有些语言会使用括号来声明函数接受的参数,但 bash 不这样做。这对括号的内部应该始终为空。他们只是用来标注你在声明一个函数而已。

之后跟着的是你每次运行这个函数时将会被执行的复合命令。

如果运行函数期间要改变脚本的文件描述符,可以选择性指定函数的自定义文件重定向。

简单命令(simple commands):所有 bash 命令的基础

哇,一下子好多内容是吧。前面讲的绝大多数东西肯定都已从你脑袋里溜走,但是没有关系。我们接下来将重返最简单的内容,帮助你基于透彻的理解逐渐构建自己的知识。重要的是记住 bash 有不同种类的命令,而且绝大多数的语法实际都很相似:大多数命令都包括 重定向控制运算符,某种程度上也都接受子命令(subcommand)。后续我们还会解释这些概念,当下先确保我们已很好地理解 简单命令

你是否充分理解简单命令非常关键,因为这是你之后在 bash 中做任何事的基础。在前面的介绍中,你或许已经注意到,所有其他 bash 命令都由至少一条简单命令构成。他们仅仅是把简单命令拿来并对其做了一些特殊操作罢了。

命令名称与运行程序

    [ var=value ... ] name [ arg ... ] [ redirection ... ]

再来看看简单命令的定义。我们会一步步地拆解它,因为虽然看起来很短,其实涉及很多内容。
我们首先聚焦命令的名称。名称(name)会告诉 bash 你这条命令是想让它做什么事。因此,为了理解并按你的意图行事,bash 会先执行搜索(search) 以找出具体要执行的任务。按顺序,bash 依据命令的 名称(name)会做如下搜索:

函数(function)
函数就是预先已被声明并命名的命令块。在前文你已大致看到我们如何声明一个函数。所有已声明的函数都被放在一个列表内,而bash会搜索这个列表来查看其中是否有和你要执行的命令同名的函数。
内建命令(builtin)
内建命令(builtin)是 bash 内部内置的微小程序。他们是编写在 bash 内部的小的操作,bash 不需要启动特别的程序去运行他们。我们之后会深入介绍 bash 提供的内建命令,以及他们的名称和作用。
程序(program)也被称作外部命令(also called an external command)
你的系统内安装了非常多的程序,有些负责小的任务,有些则执行大的任务。有些运行在终端内,有些运行的时候不可见,还有一些运行在你的图形界面内。Bash 会通过你系统配置的 PATH 查找这些程序。

如果 bash 根据你的命令名称找不到可运行的程序,那么就会产生错误,bash 会把这个错误以如下消息形式报告给你:

$ buy beer
bash: buy: command not found

在此简单提一下 别名(alias)。在 bash 执行搜索之前,它首先会查看你是否曾将该命令的名称设置为别名。如果设置过,它会在执行前用别名对应的值替换命令名称。别名基本没什么用,仅仅在交互模式下略有些作用,而且差不多完全可以用函数替代。因此绝大多数情况下,你应该避免使用他们。

程序的路径(PATH)

我们电脑里安装了各种程序,且不同程序安装在不同的位置。一些程序是我们操作系统预装的,一些是我们的发行版添加的,还有一些是我们自己或系统管理员安装的。在一个标准的 UNIX 系统内,有一些安装程序的标准位置。有的程序安装在 /bin 内,有的在 /usr/bin,有的在 /sbin,等等。如果我们要完全记住所有这些程序的安装位置简直就太费劲了,特别是他们可能还会因不同的系统而变化。于是环境变量 PATH 前来拯救我们了。你的 PATH 变量内包含了一组可以用来搜索程序的路径。

$ ping 127.0.0.1

    PATH=/bin:/sbin:/usr/bin:/usr/sbin
           │     │
           │     ╰──▶ /sbin/ping ?  找到啦!
           ╰──▶ /bin/ping ?  没找到

每当你要启动一个程序而 bash 不知道它在哪里时,就会去查看 PATH 变量下存储的这些路径。例如,假设你要启动安装在 /sbin/pingping 程序,如果你的 PATH 变量被设置为 /bin:/sbin:/usr/bin:/usr/sbin,那么 bash 首先会尝试启动 /bin/ping,结果不存在。接下来它就会尝试 /sbin/ping,如此就找到了 ping 程序,然后 bash 会记住这个位置以便你之后要再次运行 ping,剩下的就是为你运行找到的程序了。

如果你好奇 bash 根据命令名称到底在哪里找到要执行的程序的话,可以使用内建命令 type 查看:

$ type ping
ping is /sbin/ping
$ type -a echo -a 会告诉 type 向我们返回所有可能性
echo is a shell builtin如果我们运行 'echo',bash 就会执行第一种可能
echo is /bin/echo但内建变量 'echo' 之外,还有一个程序也叫 echo!

还记得前面说过 bash 有一些内建命令吗?其中一种就是 echo 程序。如果你在 bash 中运行 echo 命令,甚至在 bash 尝试 PATH 搜索之前,它就会根据名字注意到这是一个内建命令,进而使用它。type 是一种非常好用的将查询过程视觉化的方法。注意:bash 运行内建命令的速度远远快于启动外部程序。但如果你需要的是 bash 之外的 echo 功能,你也可以运行外部 echo 程序。

有时你可能需要运行一个安装位置不在 PATH 路径下的程序,这种情况下,除命令名称外,你还需手动指明程序所在的路径,以便 bash 找到这个程序:

$ /sbin/ping -c 1 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.075 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.075/0.075/0.075/0.000 ms
$ ./hello.txt还记得我们的 hello.txt 脚本吗?
Your name? 路径 “.” 表示“我们当前的路径”

你还可以添加更多的路径到 PATH 中。通常的做法是加入 /usr/local/bin~/bin~ 表示你这个用户的主目录(home directory))。记住PATH 是一个 环境变量,你可以按照下面的方法更新它:

$ PATH=~/bin:/usr/local/bin:/bin:/usr/bin
$ 

以上操作会改变你当前 bash shell 内的变量。一旦你关闭退出这个 shell,这次修改就会失效。在后面的章节中,我们会深入介绍环境变量的工作原理以及如何设置他们。

练习时间!

PATH.1. 运行 ls 程序。

$ ls

PATH.2. 找出bash会从哪里找到 ls 程序

$ type ls这里使用“type”或“command”都可以,“which”不对
ls 是 /bin/ls

PATH.3. 显示你系统的 PATH.

$ echo "$PATH"
/bin:/sbin:/usr/bin:/usr/sbin

PATH.4. 在你的主目录下创建一个脚本,把它添加到你的 PATH 中,然后像一般命令那样运行它

$ ex这里可以用你自己喜欢的编辑器替代
: i
#!/usr/bin/env bash
echo "Hello world."
.
: w myscript
"myscript" [New] 2L, 40C written
: q
$ chmod +x myscript
$ PATH=$PATH:~
$ myscript
Hello world.

命令参数与引用文字

    [ var=value ... ] name [ arg ... ] [ redirection ... ]

鉴于你已理解 bash 如何查找并运行你的命令,现在我们来学习如何将指令传达给命令,这些指令会告诉命令具体需要做什么。我们可能会运行 rm 命令来删除某个文件,使用 cp 命令复制文件,使用 echo 命令输出一个字符串,或是 read 命令读取一行文本。但是如果没有更多的细节信息,这些命令基本就很难做什么。我们需要告诉 rm 命令删除哪个文件,告诉 cp 复制什么文件以及复制后放在哪里。echo 需要知道你想让它输出什么信息,而 read 需被告知它读取的文本要放在哪里。因此我们使用参数来提供这些信息。

在上面的语法中可以看到,参数紧随命令的 名称(name) 之后,他们是被空格隔开的单词。在 bash 语境下,当我们提到单词的时候,并不是指通常语言中的单词概念。 在 bash 中,单词被定义为 可以被当作一个独立单元对待的一串字符序列单词 也被称为 token。一个 bash 单词中可以包含 许多 语言性的词汇,事实上,甚至可以包含散文。为了表述的清晰,这篇指南在接下来会统一全部使用 参数 概念来避免 单词 含义的模糊性。无论是称为单词还是参数,重要的是他们对于 shell 来说是一个独立单元:可以是文件名,变量名,程序名或人名:

$ rm hello.txt
$ mplayer '05 Between Angels and Insects.ogg' '07 Wake Up.ogg'

在上面的例子中,单词被高亮出来了。注意看他们并不是语言性的单词,但是是有意义的单元,都是文件名称。为了分隔多个参数,我们在其间使用空白,可以是空格或制表位(tab)。通常你会在参数之间使用一个空格。

一个问题由此产生:上面例子中,在 05 的后面我们使用了一个空格,将它与 Between 分开。Shell 怎么知道你的文件名称是 05 Between Angels and Insects.ogg 而非 05 呢?我们如何告诉 shell 05 后面的空格是 字面意义(literal) 的而不是分割单词的 语法?我们的意图是要保全文件名称的整体性,也就是说,名称内的空格不能将他们拆分为多个参数。我们需要一种方式来告诉 shell,它应该按字面意义对待某些单词,即按他们原本整体的样子使用,忽略任何语法意义。如果我们可以使这些空格成为字面性的,他们就不会让 bash 分割 05Between,bash 只会把空格当作普通的字符。

bash 中有两种方法使字符成为字面性的:引用(quoting)转义(excaping)。引用指的是用双引号 " 或单引号 ' 前后包裹我们想要使之保留字面意义的文本。转义是指在保留字面意义的字符前放置单个字符 \。上面例子中使用了单引号将整个文件名称字面化(literal),但不包括文件名之间的那个空格。我们强烈建议你使用引用而非转义,这样代码会更清晰可读。更重要的是,转义的使用会使人极难区分你代码中的哪些部分是字面性的,哪些不是。此外,如果后续想要编辑字面性的文本内容同时不产生错误,也非常困难。如果使用转义而非引用,上面的例子就会成为这样:

$ mplayer 05\ Between\ Angels\ and\ Insects.ogg 07\ Wake\ Up.ogg

作为 bash 用户,引用是你需要掌握的最重要的技能之一。它的重要性怎么强调都不为过。引用特别好的一点是,即使有时并非必需,引用你的数据基本不会出错。下面这两种方式都完全有效:

$ ls -l hello.txt
-rw-r--r--  1 lhunath  staff  131 29 Apr 17:07 hello.txt
$ ls -l 'hello.txt'
-rw-r--r--  1 lhunath  staff  131 29 Apr 17:07 hello.txt
$ ls -l '05 Between Angels and Insects.ogg' '07 Wake Up.ogg'

因此,如果有任何犹疑,引用你的数据。此外,绝对不要试图通过移除引号来使代码工作。

对于任何含有扩展(例如 $variable$(command))的参数,你应该使用 "双引号";其他任何参数,使用 '单引号' 。单引号内的一切字符都会是字面性的,双引号则允许某些 bash 语法,如扩展,依然生效:

echo "Good morning, $USER."双引号允许 bash 扩展 $USER
echo 'You have won SECOND PRIZE in a beauty contest.' \单引号甚至阻止 $ 语法触发扩展
     'Collect $10'

千万不要放松警惕!下面这样是绝对错误的:

$ ls -l 05 Between Angels and Insects.ogg
ls: 05: No such file or directory
ls: Angels: No such file or directory
ls: Between: No such file or directory
ls: Insects.ogg: No such file or directory
ls: and: No such file or directory

你的 shell 中不会有这些黄色高亮标识。试着养成习惯,自己在头脑中标识他们,从而避免犯错。你绝不会是第一个因一个游离或未加引用的空格字符而不小心毁掉主目录下全部文件的人。

你会发现,针对引用,养成一种实用主义的意识是很好的习惯:只要瞥一眼 bash 代码,未加引用的参数应该能立马跃入你眼前,而你在继续向下做任何事之前,感到有鼓强烈的冲动需先改正他们。人们寻求帮助的绝大多数 bash 问题,至少十分之九的核心问题都在引用上。看到就引用其实非常简单,而一个严于律己的引用规范用者会少去非常多麻烦。

使用重定向管理命令的输入与输出

    [ var=value ... ] name [ arg ... ] [ redirection ... ]

我们已简单介绍过 文件描述符 的概念,以及如何使用他们来连接进程。现在我们来看看如何在 bash 中实现这些操作。

回顾一下,进程使用文件描述符与流连接。每一个进程通常都有三种标准文件描述符: 标准输入(FD 0)标准输出(FD 1)标准错误输出(FD 2)。当 bash 启动一个程序,它首先会为程序设置一组文件描述符。这组文件描述符与 bash 自己的完全一样,即这个新的进程“ 继承 ”了 bash 的文件描述符。当你打开终端进入一个新的 bash shell,终端会将 bash 的输入和输出与自己相连。这样一来你键盘上输入的字符才进入到 bash中,而来自 bash 的消息最终会显示在你的终端窗口内。每次 bash 启动一个程序,它就会为这个程序设置一组与自己相同的文件描述符。如此一来,bash 命令的消息最终也会传到你的终端内,你键盘上的输入也会传送到程序中去(即命令的输出和输入与你的终端相连)。

                 ╭──────────╮
    Keyboard ╾──╼┥0  bash  1┝╾─┬─╼ Display
                 │         2┝╾─┘
                 ╰──────────╯

$ ls -l a b想象我们有一个文件 “a”,但没有文件 “b“
ls: b: No such file or directory错误消息会发送至 FD 2
-rw-r--r--  1 lhunath  staff  0 30 Apr 14:43 a结果会发送至 FD 1

                 ╭──────────╮
    Keyboard ╾┬─╼┥0  bash  1┝╾─┬─╼ Display
              │  │         2┝╾─┤ 
              │  ╰─────┬────╯  │
              │        ╎       │
              │  ╭─────┴────╮  │
              └─╼┥0  ls    1┝╾─┤
                 │         2┝╾─┘
                 ╰──────────╯

bash 启动 ls 进程,它首先查看自己的文件描述符,然后再为 ls 进程创建相同的文件描述符,并连接至与自己完全相同的流:即与 显示(Display) 相连的 FD1、FD2,以及与 键盘(keyboard) 相连的 FD 0。结果,ls 的错误消息(输出至 FD 2)以及它常规的结果输出(传递至 FD 1)最终都会显示在你的终端窗口内。

如果我们想要控制决定命令连接到哪里,就需要用到 重定向(redirection),使用这一操作会改变文件描述符的来源或终点。使用重定向我们可以将 ls 的结果写入一个文件内,而不再传至终端显示:


                 ╭──────────╮
    Keyboard ╾──╼┥0  bash  1┝╾─┬─╼ Display
                 │         2┝╾─┘
                 ╰──────────╯

$ ls -l a b >myfiles.ls我们将 FD 1 重定向至文档 “myfiles.ls”
ls: b: No such file or directory错误消息发送至FD 2

                 ╭──────────╮
    Keyboard ╾┬─╼┥0  bash  1┝╾─┬─╼ Display
              │  │         2┝╾─┤
              │  ╰─────┬────╯  │
              │        ╎       │
              │  ╭─────┴────╮  │
              └─╼┥0  ls    1┝╾─╌─╼ myfiles.ls
                 │         2┝╾─┘
                 ╰──────────╯

$ cat myfiles.lscat 命令可以向我们展示文件内容
-rw-r--r--  1 lhunath  staff  0 30 Apr 14:43 a结果现存在 myfiles.ls

通过将命令的标准输出重定向至一个文件,你刚完成了一次文件重定向操作。标准输出的重定向是通过使用 > 控制运算符实现的。可以将它想象为一个箭头,把输出从命令传送至文件。这是目前最常见也是最有用的重定向方式。

此外,重定向还常被用来隐藏错误消息。你会注意到我们重定向之后的 ls 命令仍然显示错误消息,通常这是好事。但有的时候,我们可能会觉得脚本中一些命令产生的报错消息对于用户来说是不重要的,应该被隐藏。为此,我们可以再次使用文件重定向,以相似的方式重定向标准错误输出,使 ls 的结果消失:


                 ╭──────────╮
    Keyboard ╾──╼┥0  bash  1┝╾─┬─╼ Display
                 │         2┝╾─┘
                 ╰──────────╯

$ ls -l a b >myfiles.ls 2>/dev/null我们将 FD 1 重定向至 “myfiles.ls”
将 FD 2 重定向至文档 "/dev/null"

                 ╭──────────╮
    Keyboard ╾┬─╼┥0  bash  1┝╾─┬─╼ Display
              │  │         2┝╾─┘
              │  ╰─────┬────╯
              │        ╎
              │  ╭─────┴────╮
              └─╼┥0  ls    1┝╾───╼ myfiles.ls
                 │         2┝╾───╼ /dev/null
                 ╰──────────╯

$ cat myfiles.lscat 命令会向我们展示文档内容
-rw-r--r--  1 lhunath  staff  0 30 Apr 14:43 a结果现存在 myfiles.ls 中
$ cat /dev/null/dev/null 文档是空的?
$ 

注意看,通过在 > 控制运算符的前面注明 FD 编号,你可以重定向任何 FD。我们使用 2> 将 FD 2 重定向至 /dev/null,使用 > 将 FD 1 仍旧重定向至 myfiles.ls。如果你省略了编号,输出重定向的默认项是 FD 1(标准输出)。

我们的 ls 命令不再显示错误消息,结果也被保存在 myfiles.ls。那么错误消息到哪里去了呢?我们已经将它写入文件 /dev/null。但是当我们显示该文件内容时,并没有看到错误消息。难道出了什么错?

解开这个小小谜团的线索在于路径名称。文件 null 位于 /dev 路径下:这是一个存放 设备文件 的特殊路径。设备文件是一种特殊文件,他们用来代表系统内的设备。当我们从中读取或向内写入数据时,是通过内核与他们直接交流。那个 null 设备是一个总是为空的特殊设备。你向其中写入的任何东西都会遗失,从中也不能读取任何信息。因此,对于丢弃信息来说它是一个非常好用的设备。我们将不想要的错误消息流向这个 null 设备,他们就遗失不见了。

如果我们想把通常显示在终端窗口内的所有输出,包括命令执行结果以及错误消息,都保存在 myfiles.ls 呢?直觉可能会是这样:

$ ls -l a b >myfiles.ls 2>myfiles.ls将两个文件描述符都重定向至 myfiles.ls?

                 ╭──────────╮
    Keyboard ╾┬─╼┥0  bash  1┝╾─┬─╼ Display
              │  │         2┝╾─┘
              │  ╰─────┬────╯
              │        ╎
              │  ╭─────┴────╮
              └─╼┥0  ls    1┝╾───╼ myfiles.ls
                 │         2┝╾───╼ myfiles.ls
                 ╰──────────╯

$ cat myfiles.ls取决于两条流如何抵达并交汇,内容很可能是错乱的
-rw-r--r--  1 lhunath  stls: b: No such file or directoryaff  0 30 Apr 14:43 a

如果你这样想就 错了!为什么不对呢?乍一看 myfiles.ls 似乎没问题,但实际可能会很危险。如果你足够幸运,会看到文档内的输出结果与你期待的不太一致,内容可能有些混乱、无序,甚至还有可能完全正确。但问题是,你不能预测也不能保证命令的结果。

这里到底发生了什么?问题出在两个文件描述符现在都将他们的流连接至这个文档。因为流内部的工作方式,这种操作是有问题的,不过这个话题已超出本指南的讨论范围,总之就是当两条流被融合到一个文件后,其结果会是两条流任意随机地混合。

为了解决这个问题,你需要将输出与错误消息都发送到同一条流内,进而你需要知道如何 复制文件描述符

$ ls -l a b >myfiles.ls 2>&1使FD 2 写入FD 1 所写之处

                 ╭──────────╮
    Keyboard ╾┬─╼┥0  bash  1┝╾─┬─╼ Display
              │  │         2┝╾─┘
              │  ╰─────┬────╯
              │        ╎
              │  ╭─────┴────╮
              └─╼┥0  ls    1┝╾─┬─╼ myfiles.ls
                 │         2┝╾─┘
                 ╰──────────╯

$ cat myfiles.ls
ls: b: No such file or directory
-rw-r--r--  1 lhunath  staff  0 30 Apr 14:43 a

复制文件描述符,就是将一个文件描述符的流连至另一个文件描述符。如此一来,两个文件描述符就与同一条流相连。我们会使用 >& 控制运算符,在它前面是我们想要改变的文件描述符,后面跟着我们想要复制进而连入同一条流的文件描述符。之后你会经常用到这个控制运算符,并且在绝大多数应用场景中都是像上面例子那样将 FD 2 复制为 FD 1。你可以把语法 2>&1 理解为 使 FD 2 写入(>)FD(&1 正在写入的地方

到此,我们已见到不少重定向操作,甚至已经会结合使用他们。在你自由驰骋之前,还有一条重要规则需要理解:重定向是从左至右评估执行的,和我们阅读的顺序是一致的。这看起来似乎没什么特别的,但是忽略这个规则已导致你前面许多人犯过如下错误:

    $ ls -l a b 2>&1 >myfiles.ls使 FD 2 连至 FD 1,FD 1 连至 myfiles.ls?

写下如上代码的人可能认为 FD 2 的输出连至 FD 1,而 FD 1 又会输出至 myfiles.ls,进而错误消息最终会在这个文档中。他们这种推理的逻辑错误在于认为 2>&1 会将 FD 2 的输出发送至 FD 1。但并非如此。 它会将 FD 2 的输出发送至 FD 1 连接的 ,此时可能是 终端 而非那个文档,因为 FD 1 尚未被重定向。上面命令的结果可能会让人沮丧,因为看起来似乎是标准错误的重定向没有生效,但实际上,你仅仅是把标准错误重定向到终端(标准输出的目的地),而这正是之前它已被指向的地方。

如果我们修正重定向的顺序:

    $ ls -l a b >myfiles.ls 2>&1使 FD 1 输出至 myfiles.ls,同时将 FD 2 设置为相同目的地

现在我们将 FD 1 的输出流至 myfiles.ls,然后将 FD 2 指向 FD 1 当前使用的流,即 myfiles.ls。两个文件描述符就都以 myfiles.ls 为目标,命令 ls 的任何输出,无论是写入 FD 2 还是 FD 1,最终都会存至该文档。

此外还有很多其他重定向控制运算符,但都不如我们以上学习的这些有用。对于人们来说, 被证明肯定有用的是,学习像阅读英文那样阅读命令的重定向。接下来我会逐个列举 bash 的重定向命令运算符,每个都附上一段简短的描述,以及你可以用来将操作命令翻译为日常英语的一句话。

文件重定向(File redirection)
    [x]>file, [x]<file

echo Hello >~/world
rm file 2>/dev/null
read line <file
使 FD x 写入或读取 文件(file)

为写入或读取,打开通向 文件(file) 的流,并连接至文件描述符 x。如果省略 x,默认设置是 FD 1(标准输出)写入,FD 0(标准输入)读取。

文件描述符复制(File descriptor copying)
    [x]>&y, [x]<&y

ping 127.0.0.1 >results 2>&1
exec 3>&1 >mylog; echo moo; exec 1>&3 3>&-
使 FD x 写入或读取 FD y 的流 。

将 FD x 连接至FD y 的流。第二个例子很复杂:为了理解它你需要知道 exec 可以用来改变 bash 自身的文件描述符(而不是一个新命令的),而且,如果你使用尚不存在的 x,bash 会使用这个编号为你创建一个新的文件描述符。

追加文件重定向(Appending file redirection)
    [x]>>file

echo Hello >~/world
echo World >>~/world
使 FD x 追加至 文件(file)末尾。

在追加模式下,连接 文件(file) 且供写入用的流会打开,连向文件描述符 x。使用常规文件重定向控制运算符 > 会首先清空文档内之前存储的全部内容,然后写入本次重定向输出过去的内容。在追加模式下(>>),文档内原有内容仍会保留,流只会把本次输出内容添加在原有内容的末尾。

重定向标准输出与标准错误输出(Redirecting standard output and standard error)
    &>file

ping 127.0.0.1 &>results
将 FD 1(标准输出)与 FD 2(标准错误输出)都写入 文件(file)

这是和 >file 2>&1 效果相同,但是更精简方便的控制运算符。同样的,可以使用双箭头 &>>file 实现追加写入效果。

Here 文档(Here Documents)/dfn>
    <<[-]delimiter
        here-document
    delimiter

cat <<. 我们选择 . 作为终止定界符
Hello world.
Since I started learning bash, you suddenly seem so much bigger than you were before.
.我们前面选择了 . 这个字符标识here文档的结束
使 FD 0(标准输入)读取 定界符(delimiter) 之间的字符串。

Here 文档是将大块文本内容喂给命令作输入的非常好用的方式。他们起始于你选用的定界符之后,终止于 bash 遇到一行 含有该定界符的命令。一定要记得你的终止定界符前不能有缩进,因为如此一来这一行中就不仅有定界符了(还有空白)。

在你的起始定界符前可以放置 -,这样 bash 就会忽略你添加在 here 文档前的所有制表符(tab)。这样,你就可以缩进 here 文档,但作为输入的字符串不会显示缩进。同样,终止定界符前也因此可以使用 tab 缩进。

最后要说的是,也可以在 here 文档的字符串中使用 变量扩展(variable expansions),这样你就可以在文档中置入变量数据。关于变量和扩展,后面我们还会学习更多,这里你只需要知道如果要回避使用扩展,你需要在首次声明 '定界符' 时为它加上引号。

Here 字符串(Here Strings)
    <<<string

cat <<<"Hello world.
Since I started learning bash, you suddenly seem so much bigger than you were before."
使 FD 0(标准输入)从 字符串 中读取。

Here 字符串与 here 文档非常相似但是更精简,因此更推荐使用。

关闭文件描述符(Closing file descriptors)
    x>&-, x<&-

exec 3>&1 >mylog; echo moo; exec 1>&3 3>&-
关闭 FD x

断开文件描述符 x 与流的连接,并将它从进程中移除。除非被再次创建,否则这个文件描述符不能再被使用。当 x 省略的时候,>&- 默认会关闭标准输出,而 <&- 默认会关闭标准输入。你基本不会用到这个运算控制符。

移动文件描述符(Moving file descriptors)
    [x]>&y-, [x]<&y-

exec 3>&1- >mylog; echo moo; exec >&3-
用 FD y 替换 FD x

文件描述符 y 复制到 x 然后关闭 y,即用 y 替换 x。也是 [x]>&y y>&- 的简便操作。同样的,你基本也不会用到它。

使用文件描述符读取或写入(Reading and writing with a file descriptor)
    [x]<>file

exec 5<>/dev/tcp/ifconfig.me/80
echo "GET /ip HTTP/1.1
Host: ifconfig.me
" >&5
cat <&5
为读取和写入 文档(file),打开 FD x

文件描述符 x 被打开,并通过流与可以读取和写入字节的文档相连。通常为实现这一目的,你会使用两个文件描述符。但在一些罕见情况下,像是要将流与读/写设备如网络接口(network socket)相连时,这种方式会很有用。上面例子就将数行 HTTP 写入 host 在 80 端口(标准 HTTP 端口)的 ifconfig.me,并读取网络返回的数据,使用的都是 exec 为此配置的相同的文件描述符 5

作为对重定向最后的说明,我想要指出的是对于简单命令,重定向控制运算符可以出现在命令中的任何位置,也就是说,他们不一定是在命令的末尾出现。虽然出于一致性以及在长命令中避免出现意外或遗漏运算符的考虑,将重定向控制运算符放在命令末尾是一个好主意,但是有些情况下,一些人习惯将他们放在其他位置。特别是当有一串 echoprintf 命令时,为了可读性,会习惯将重定向控制运算符放在命令名称之后:

echo >&2 "Usage: exists name"
echo >&2 "   Check to see if the program 'name' is installed."
echo >&2
echo >&2 "RETURN"
echo >&2 "   Success if the program exists in the user's PATH and is executable.  Failure otherwise."

 

练习时间!

REDIR.1. 执行一条命令,在标准输出处生成一条消息

$ ls /bin/bash
/bin/bash*

REDIR.2. 执行一条命令,在标准错误输出处生成一条消息

$ ls /bob/bash
ls: /bob/bash: No such file or directory

REDIR.3. 执行一条命令,同时在标准输出与标准错误输出处各生成一条消息

$ ls /bin/bash /bob/bash
ls: /bob/bash: No such file or directory
/bin/bash*

REDIR.4. 将上一条命令的标准错误消息发送至一个名为 errors.log 的文档,并在终端内显示该文档的内容

$ ls /bin/bash /bob/bash 2>errors.log
/bin/bash*
$ cat errors.log
ls: /bob/bash: No such file or directory

REDIR.5. 将上一条命令的标准输出与标准错误输出消息追加至名为 errors.log 的文档,然后再次在终端内显示该文档的内容

$ ls /bin/bash /bob/bash >>errors.log 2>&1
$ cat errors.log
ls: /bob/bash: No such file or directory
ls: /bob/bash: No such file or directory
/bin/bash*

REDIR.6. 使用 here 字符串在终端内显示字符 Hello world.

$ cat <<< 'Hello world.'
Hello world.

REDIR.7. 修改后面的命令,使得消息可以被正确保存在 log 文档内,之后关闭 FD 3:exec 3>&2 2>log; echo 'Hello!'; exec 2>&3

$ exec 3>&1 >log; echo 'Hello!'; exec 1>&3 3>&-
Fork me on GitHub