-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcommands.html
More file actions
811 lines (611 loc) · 73.4 KB
/
commands.html
File metadata and controls
811 lines (611 loc) · 73.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
---
id: commands
layout: chapter
chapter: 2
title: 命令与参数
subtitle: 我要如何给 bash 下指令?
status: beta
description: 关于什么是命令,如何发布他们;交互模式与脚本;命令语法,通过名称搜索命令与程序;参数与单词分割,以及输入和输出重定向
published: true
---
<section>
<h1>Bash 命令是什么,我要如何编写与发布他们?</h1>
<p>从开篇到这里,关于 bash 和其他进程怎样在终端内协同工作,我们已了解很多。现在让我们重新聚焦 bash,搞清楚到底怎样用它把事情做成。</p>
<p>之前已曾提及,bash 会等待你发出指令,然后尽它所能去执行他们。为了最大程度地用好 bash,同时,也为了避免因 bash 错误理解你的意图而损害系统,你一定要对 bash shell 语言的基础概念和用法特别留意。有非常多自以为熟悉 bash 的人,其实对最基本的概念都理解有误。结果就是,他们编写的程序可能会对毫无戒备的用户及系统造成严重损害。<em>不要做这样的人</em>。</p>
<h2>那么什么是 bash 命令?</h2>
<p>Bash shell 语言的核心就是命令。你发出的命令会一步步、逐条告诉 bash 你想让它做什么。</p>
<p>bash 通常每次从你那接收一条命令,执行它,结束后返回等待你的下一条命令。我们将这种模式称为 <dfn>同步</dfn> 执行命令(synchronous command execution)。一定要理解,当 bash 忙于执行你的某条命令时,你将暂时无法再和它直接交互,需等它执行完当前命令后返回继续待命。对于绝大多数命令,你其实根本不会注意到这点,因为 bash 执行命令的速度如此之快,远在你察觉前它就已经返回等待下一条命令了。</p>
<p>不过,有一些命令的执行时间会比较长,特别是启动其他程序与你交互的命令。例如,某条命令可能会启动一个文档编辑器。当你与这个文档编辑器交互的时候,bash 就会退居幕后,等待文档编辑器程序结束运行(通常意味着你从中退出)。当文本编辑器停止运行后,意味着该条命令执行结束,bash 将返回等待你下一步操作的命令。你还会注意到,在文档编辑器运行的时候,bash 提示符就消失不见了;一旦你退出编辑器,bash 提示符又会再次出现:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ex</kbd><em>启动 “ex” 程序的 bash 命令</em>
: <kbd>i</kbd><em>ex 命令,表示“插入”(insert)文本</em>
<kbd>Hello!
.</kbd><em>如果一行内只有一个 . ,ex 会停止文本插入</em>
: <kbd>w greeting.txt</kbd><em>ex 命令,将文本“写入”(write)文档</em>
"greeting.txt" [New] 1L, 7C written
: <kbd>q</kbd><em>ex 命令,表示“退出”</em>
<span class="prompt">$ </span><kbd>cat greeting.txt</kbd><em>从 ex 又回到 bash!</em>
Hello!<em>“cat“ 程序会显示文档内容</em>
<span class="prompt">$ </span>
</pre>
<p>注意看上面这部分,我们首先向 bash 发出启动 <kbd>ex</kbd> 文档编辑器这条命令。命令发出后,我们终端内显示的提示符就发生了变化:此时我们输入的任何文本都是发送给 ex 程序的,而非 bash。当 ex 运行的时候,bash 就进入休眠,等待直到 ex 结束。当你使用 <kbd>q</kbd> 命令退出 ex,第一条 <code>ex</code> bash 命令终结束运行,bash 又开始等待接收新的命令。为了让你知道它又回来待命了,bash 提示符会再次出现,使你可以键入下一条命令。在上面例子中,我们最后以 <kbd>cat greetings.txt</kbd> 这条 bash 命令作结,让 bash 启动 cat 程序。cat 程序非常利于输出文件内容(它是 con<em>cat</em>enate 的缩写,因为它的目的就是一个接一个地连缀输出你给它的所有文件的内容)。在这个例子中,cat 命令是用来查看并显示我们用 ex 程序编辑过的 <code>greetings.txt</code> 文档内的内容。</p>
<footer>
Bash 命令是 bash 可以独立运行的最小代码单元。在 bash 执行命令期间,你将不能和 bash shell 交互。一旦它完成执行,就会返回等待你的下一条命令。
</footer>
<h2>我要如何向 bash 发送命令?</h2>
<p>因为前面我们已给出一些 bash 命令的示例,所以关于如何向 bash 发送基本的指令,你很可能已有正确的认识。</p>
<p>Bash 是一种基于行(line-based)的语言。相应地,当 bash 读取你的命令时,它也是一行一行地读。绝大多数命令都只占据一行,除非你 bash 命令的语法在行末明确表明该行命令还未完结会延续至下一行,否则 bash 就会立即将行末理解为命令输入的终止点。因此,输入一行文本并按下 <kbd title="enter/return">⏎</kbd> (回车)的行为,通常就会引发 bash 启动执行你这行文本所表述的命令。</p>
<p>然而,有一些命令确实会跨越多行,他们通常都是命令块(block commands)或是命令中含有引用:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>read -p "Your name? " name</kbd><em>这条命令是完整的,可以立即被执行</em>
Your name? <kbd>Maarten Billemont</kbd>
<span class="prompt">$ </span><kbd>if [[ $name = $USER ]]; then</kbd><em>"if"块开始但尚未结束</em>
> <kbd> echo "Hello, me."</kbd>
> <kbd>else</kbd>
> <kbd> echo "Hello, $name."</kbd>
> <kbd>fi</kbd><em>到此“if”块结束,bash知道要开始执行命令了</em>
Hello, Maarten Billemont.
</pre>
<p>逻辑上,如果 bash 对于它要做的事尚未掌握全部信息,那么它是无法执行一条命令的。在上面例子中(我们后面还会更具体地介绍这些命令在做什么),<code>if</code> 命令的首行没有包含足够完整的信息,bash 不知道如果测试通过了或是如果测试失败了,接下来要做什么。因此,bash 显示了一个特殊的提示符:<code>></code>。这个提示符其实就是在说:<q>你给我的命令尚未完结</q>。我们后面又为这条命令输入了更多行的字符,直到抵达 <code>fi</code>。当我们结束那一行的输入,bash 终于知道你已完整给出了条件结果案例。Bash 就会立即开始执行整个命令块中的代码,从 <code>if</code> 到 <code>fi</code>。我们很快就会看到 bash 语法中定义的不同种类的命令,前面例子中的 <code>if</code> 命令被称作<dfn> 复合命令(Compound Command)</dfn>,因为它将一组基本的指令复合成为一个更大的逻辑组块。</p>
<p>在以上示例中,我们都是把命令发送给一个交互式的 bash。正如前面解释过的,bash 也能以非交互式的模式从文件或流中读取命令,而无需向你索要。在非交互模式下,bash 没有命令提示符。除此之外,它的操作与交互模式基本相同。例如,我们就可以把上面例子中的代码复制到一个文件中:</p>
<pre lang="bash">
<kbd>read -p "Your name? " name
if [[ $name = $USER ]]; then
echo "Hello, me."
else
echo "Hello, $name."
fi</kbd>
</pre>
<p>至于你如何命名这个文件,并不重要。假设你将上面这些代码保存在 <code>hello.txt</code> 文件中,现在我们就可以使用 bash 直接运行文件中的命令,无需再向我们索要:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>bash hello.txt</kbd><em>启动一个新的 “bash” 进程</em>
Your name? <kbd>Maarten Billemont</kbd>
Hello, Maarten Billemont.<em>当文件中没有余下未执行的代码后,新启动的 “bash” 进程就结束</em>
<span class="prompt">$ </span><em>bash 命令执行结束后,交互式 bash 再次返回</em>
</pre>
<p>注意上面这个例子中有两个 bash 进程。开始时,我们先启动了常用的交互式 bash shell。然后我们让这个 bash 进程执行一条命令,从而启动了一个新的 bash 进程。第二个进程将会以非交互式的方式执行它在 <code>hello.txt</code> 文件中读取到的所有命令。执行结束后(即文件中没有余下未执行的命令),非交互式的 bash 进程终止,交互式的 bash 进程完成了它对 <kbd>bash hello.txt</kbd> 命令的执行,进而提示符再次出现,等待运行你的下一条命令。</p>
<p>从包含一列命令的文档到一份真正意义上的<dfn>bash 脚本(script)</dfn>,只需再迈出一小步。使用你喜欢的编辑器,再次打开 <code>hello.txt</code> 文档,在它的最上端添加一个 <dfn>hashbang</dfn> 作为脚本的首行:<kbd>#!/usr/bin/env bash</kbd></p>
<pre lang="bash">
<kbd><ins>#!/usr/bin/env bash</ins></kbd>
read -p "Your name? " name
if [[ $name = $USER ]]; then
echo "Hello, me."
else
echo "Hello, $name."
fi
</pre>
<p>祝贺你!你已创建了自己的第一份 bash 脚本。什么是 bash 脚本?就是其中含有 bash 代码的文档,系统内核可以像执行电脑中其他程序那样执行它。本质上,它就是一个程序,不过需要 bash 解释器将 bash 语言翻译为系统内核可以理解的指令。这就是为什么我们要在文档首行插入“hashbang”:它告诉系统内核需要用哪种解释器来理解并翻译这份文档中的语言,以及在哪里可以找到这种解释器。我们之所以称呼为“hashbang”是因为,bash 脚本总是从一个“hash”开始,后面紧跟着一个“bang” <code>!</code>。之后你的 hashbang 必须指定一条绝对路径,指向能理解、翻译文档中语言的程序,并且可以接收一个参数。我们这里的hashbang有些特殊:我们指向的程序 <code>/usr/bin/env</code>,其实并不是一个可以理解 bash 语言的程序。它实际是一个可以找到并启动其他程序的程序。在这个例子中,我们通过使用一个参数(argument)告诉这个程序去找到 <code>bash</code> 程序,并用它来解释翻译我们脚本中的语言。那为什么要使用这样一个叫做 <code>env</code> 的中间程序呢?全部的原因都在于这个名字前面的东西:路径(path)。我们很有把握地知道 <code>env</code> 这个程序生活在 <code>/usr/bin</code> 路径下,但是考虑到整个操作系统以及配置的庞杂,我们很可能不清楚 <code>bash</code> 这个程序被安装在哪里。这就是为什么我们会使用 <code>env</code> 这个程序来帮助我们找到它。有些复杂是吧!那么在加上 hashbang 之前与之后,我们的文档有什么不同吗?</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>chmod +x hello.txt</kbd><em>将 hello.txt 标识为可被执行(e<strong>x</strong>ecutable)的程序</em>
<span class="prompt">$ </span><kbd>./hello.txt</kbd><em>让 bash 启动 hello.txt 程序</em>
</pre>
<p>在内核允许将某个文档作为程序执行之前,绝大多数的系统都要求你首先将这个文档标识为 <dfn>可执行(executable)</dfn>。如此操作之后,我们就可以像启动其他程序那样启动 <code>hello.txt</code>。内核会读取这份文档,找到 hashbang,通过它找到 bash 解释器,最终使用 bash 解释器运行文档中的指令。现在你就有了自己的第一个 bash 程序!</p>
<footer>
bash以行的方式读取命令。一旦它读取完构成完整命令的全部行代码,就会开始执行命令。通常,命令都只有一行的长度。交互模式下的 bash 从提示符开始读取你的行命令。非交互式 bash 进程则从文档或流中读取命令。 以 <dfn>hashbang</dfn> 为首行起始(且获得 <dfn>可执行</dfn> 许可)的文档,就会像你电脑中的其他程序那样被系统内核启动。
</footer>
</section>
<section>
<h1>学习说“bash”</h1>
<p>如果你有认真阅读前面部分的内容,那应该对 bash 是什么、在系统的哪个位置、如何工作,以及如何使用它,都已有清楚的认识。</p>
<p>
现在是时候开始学说“bash”了。接下来我们会介绍 bash shell 语言的语法,因此这份指南会变得更有技术性(technical)。但是不要担心,保持专注,你不会掉队的。如果觉得不安或不踏实,可以重读前面的内容再看后续的部分,以防彻底迷失。我们会尽可能覆盖新概念相关的所有“如何”(how's)或“为什么”(why's)的问题。如果仍有任何不清楚的地方,也欢迎你联系我们,这样我们就能为你以及之后其他学生改进这篇指南。我们的联系信息在指南的篇首。</p>
<h2>关于意图与模糊性</h2>
<p>相比和人类对话,与计算机对话最大的不同在于,计算机程序通常都不擅长把你的需求放在一个背景下去揣摩你的意图是什么。那些尝试去做,且真的能够基于模糊的输入、费尽周折搞清楚预期结果的程序,通常会被称为“聪明”。不幸的是,这种情况下的“聪明”,与我们对“聪明”人的期待是不同的:基于我们含糊不清的输入,计算机程序作出的推测常与实际相差甚远,并经常导致糟糕甚至灾难性的结果。</p>
<p>麻烦地是,我们人类却习惯于模糊不清地讲话:我们依赖于接收者能理解我们所提需求的背景(context),清楚我们最期待的行动是什么。当我们向伴侣要盐的时候,并不是真地要他们给我们一勺盐:而是期待他们理解我们的实际意图是让他们往饭菜中加一点盐,期待当他们把饭菜端给我们的时候,上面至少洒了一点点盐。
</p>
<p>在开始与计算机程序对话之前,首先要认识到我们日常语言以及需求中的模糊性,进而在之后学习消除这种模糊性。如果你对此缺少经验,这很可能就是你前进过程中最大的挑战。按字面意思思考是需要练习的。下面这个技巧应该有用:想象我们是在和一个三岁大的小朋友讲话,每一次都要像第一次那样,教他们做你想让他们做的事。当只是说<q>把那本动物书拿过来</q> 还不够的时候,我们需要分步骤教他们:<q>向四周看看,看到你身后的书了吗?</q>,<q>棒!你能找到那本封面上有狮子和牛的书吗?</q>,<q>就是那本,把它拿给我!</q>,<q>好孩子,把它拿给爸爸!过来这边。</q>,<q>嗨,你真棒,把书给我,坐下吧,我们一起来阅读。</q>。一定程度上,写 bash 脚本就像是在教你的系统执行某项任务。区别在于,你三岁大的孩子自己就能在新的需求中辨识出过去习得的经验,但你的系统不会,每次你都需要明确指定它重复运行之前编写的任务描述代码。</p>
<p>一些语言解释器(类似 bash,解释器是一种可以理解语言的程序),试图通过严格地限定自己的语法来规避以上问题。背后的理念就是去除你语言中的模糊不清来避免系统不小心做错什么。解释器强制性地将表达的准确性限定到一定程度。这是一种相对成功的策略,通常会产出 bug 最少的程序。</p>
<p>可惜的是,bash 不是一个严格的解释器。<br>
实际上,bash 的宽容在很大程度上导致大多数编写 bash 脚本的人能力都不合格,无论是新手还是专业技术人员。结果就与世纪之交网站开发的状态非常相似:许多网页的代码都写得极为糟糕,以至于严重限制他们在任一种标准浏览器内的正常渲染,这就倒逼浏览器使用各种所谓“聪明”的手段,努力以网页开发者期待的效果去渲染,而不是严格遵从他们实际写下的代码。类似地,你即将遇到的大多数bash 脚本都是 <em>有bug的(buggy)</em>。有些是轻微的,但经常能到这种程度:仅仅将它用在一个名字稍不太寻常的文件上,就可能对你的系统造成不可逆的破坏。</p>
<p><strong>不要做这样的人。</strong><br>
这份指南的存在是要教你写出好的 bash 代码。它将赋能你准确传达自己的真实意图,并让计算机解决你的问题。既然 bash 是一种松懈的解释器,<em>纪律的责任就在你身上</em>。如果你不打算尊重这个前提,我建议你现在就停止阅读,另找一种严格的解释器。世界上已经有太多糟糕的 bash 代码,这份指南拒绝帮助人输出更多。</p>
<footer>
Bash 是一种宽松的语言解释器,这就意味着它允许你写模糊不清的命令。它的语法不会阻止你编写命令,使计算机做出非你本意要它做的事。因此,责任全都在你,要充分学习 bash 的语法,识别陷阱,练习过程中严于律己,尽可能地减少代码中的 bug。
</footer>
<h2>Bash 命令的基本语法</h2>
<p>在最高水平上,bash 有几类不同的命令。我们接下来会解释每一种类型,给出一个简单的示例,然后在后续部分更深入地介绍每一类命令。现在不要太担心这些命令的语法,当我们之后专注到每种命令类型时,他们就会变得清晰。当前你只需要对 bash 命令的多种类型、规模以及不同语法,形成初步整体性的理解。</p>
<dl>
<dt>简单命令(Simple Commands)</dt>
<dd>
<p>这是最常见的命令类型。命令中会指定要执行的命令名称,后面跟着可选的 <dfn>参数(arguments)</dfn>,<dfn>环境变量(environment variables)</dfn>, <dfn>文件描述符重定向(file descriptor redirections)</dfn>。
</p>
<pre class="syntax">
[ <var>var</var><strong>=</strong><var>value</var> ... ] <var>name</var> [ <var>arg</var> ... ] [ <var>redirection</var> ... ]
<samp><u title="名称(name)">echo</u> <u title="参数 #1(arg #1)">"Hello world."</u></samp>
<samp><u title="变量 #1(var #1)">IFS=,</u> <u title="名称(name)">read</u> <u title="参数 #1(arg #1)">-a</u> <u title="参数 #2(arg #2)">fields</u> <u title="重定向 #1(redirection #1)">< file</u></samp>
</pre>
<aside><p>因为这是我们第一次介绍语法,就先说明一下在这篇指南中我们如何标识一个语法组块中的不同元素。语法中的每一个单词你之后会用自己实际的命令代码替换,如上面示例命令中的名称 <var>name</var> 以 <var>这种方式</var> 标识。语法中你之后必须原样输入的字符会加粗显示,如 <code><var>var</var><strong>=</strong><var>value</var></code>中的 (<code>=</code>) 。</p>
<p>这篇指南非常注重通过实践教学,因此会在每个新概念之后附上一些例子。例子中通常会包括<u title="简短的文字注释会帮助你理解元素的性质">下划线标注</u>的部分,帮助你理解关键内容。当你把鼠标悬停在下划线标注的内容上时,会出现一个简短的解释。试试看这种操作,看看每个注释都说了什么。</p>
<pre>
<samp><u title="这是对例子的注释">例子</u>就像这个样子</samp>
<samp>这是例子的第二个示例</samp></pre></p>
<p>在语法块中,我们使用方括号(<code>[ ]</code>)包住的部分是可选性的。你会看到第一个例子中的命令内就没有 <code><var>var</var><strong>=</strong><var>value</var></code> 这个部分,但是第二个例子中有。如果你需要就用它,不需要就省略。最后,语法中根据你的需要可以重复使用多次的元素,用三个点(<code>...</code>)标识。例如,上面的语法就允许你根据需要选择性地标识任意多的 <var>重定向</var></p></aside>
<p>在命令的名称之前,你可以 <em>选择性</em> 赋值一些 <var>变量(var)</var>。这些变量仅适用于这一次命令的执行环境。关于变量和环境,后面我们还会深入介绍。</p>
<p>命令的 <var>名称(name)</var> 是第一个单词(在选择性的变量赋值之后)。Bash 会找到这个名称对应的命令并启动它。我们后面还会介绍被命名的命令都有哪些种类,以及 bash 如何找到他们。</p>
<p>命令的名称后面可以选择性地接一列 <var>参数(arg)</var> ,我们很快就会学习什么是参数,以及他们的语法。</p>
<p>最后,命令中也可以有一组施加于它的 <var>重定向(redirection)</var> 设置。还记得我们前面对文件描述符的解释吗,重定向就是改变文件描述符插件指向的操作。他们会改变与命令进程相连接的流(stream)。在后面章节中我们会学习到重定向的作用。
</p>
</dd>
<dt>管道(Pipelines)</dt>
<dd>
<p>相比使用基本语法,bash 有很多“语法糖(syntax sugar)”,使一般任务执行起来更为轻松。管道就是一种你经常会用到的语法糖。通过将第一个进程的标准输出与第二个进程的标准输入相连,它可以很方便地将两个命令连接在一起。这是终端命令最常用的与其他命令对话并传递信息的方法。</p>
<pre class="syntax">
[<strong>time</strong> [<strong>-p</strong>]] [ <strong>!</strong> ] <var>command</var> [ [<strong>|</strong>|<strong>|&</strong>] <var>command2</var> ... ]
<samp><u title="command #1">echo Hello</u> | <u title="command #2">rev</u></samp>
<samp>! <u title="command #1">rm greeting.txt</u></samp>
</pre>
<p>我们基本不用 <code>time</code> 关键词,但是对于了解执行命令所需的时间,使用它还是很方便的。</p>
<p>感叹号 <code>!</code> 这个关键词初看可能有些奇怪,和 time 关键词类似,它和连接命令也没多大关系。当我们讨论条件及测试命令是否执行成功时,会再介绍它的作用。</p>
<p>语法中的第一个命令 <code>command</code> 与第二个命令 <code>command2</code> 可以是任何类型。bash 会分别为他们创建一个 <dfn>subshell</dfn>,并将第一个命令的标准输出文件描述符设置为指向第二个命令的标准输入文件描述符。这两个命令会同时运行,而 bash 会等待他们全部执行结束。我们在后面的章节中会解释“subshell”究竟指什么。</p>
<p>在两个命令之间,有这样一个符号 <code>|</code>。这个也被称作“管道”符,它会告诉 bash 将第一个命令的输出与第二个命令的输入相连。此外,在两个命令之间也可以使用 <code>|&</code> 符号,意思是除了标准输出外,把第一个命令的标准错误输出也与第二个命令的输入相连。但最好不要这样做,因为标准错误文件描述符通常用来传递消息给用户。如果我们把这些消息传给第二个命令而不是终端显示器,需确保第二个命令可以处理接收到的这些消息。</p>
</dd>
<dt>列表(Lists)</dt>
<dd>
<p>列表就是一组命令序列。本质上,脚本就是一个命令列表:一个接一个的命令。列表中的命令用控制运算符分开,而控制运算符会示意 bash 应该如何执行它前面的命令。</p>
<pre class="syntax">
<var>command</var> <var>control-operator</var> [ <var>command2</var> <var>control-operator</var> ... ]
<samp>cd music<u title="序列(sequential)控制运算符">;</u> mplayer *.mp3</samp>
<samp>rm hello.txt <u title="条件(conditional)控制运算符“OR”">||</u> echo "Couldn't delete hello.txt." >&2</samp>
</pre>
<p>列表语法中的命令可以是这部分介绍的其他任何命令类型。</p>
<p>命令之后的 <dfn>控制运算符</dfn> 会告诉 bash 应该如何执行这条命令。最简单的控制运算符就是换行,等价于 <code>;</code>,用来告诉 bash 运行这条命令,等待结束,然后执行列表中的下一命令。第二个例子使用了 <code>||</code> 这个控制运算符,它用来告诉 bash 正常运行在它前面的命令,但是运行结束后,<em>只有当前面那条命令运行失败</em> 才需要继续运行后面第二条命令。如果前面那条命令运行成功,<code>||</code> 会让 bash 跳过它后面的那条命令。这对于命令运行失败后显示错误信息非常有用。在后面章节中,我们会深入了解所有的控制运算符。</p>
<p>注意,因为bash脚本本质上是一个由多行命令组成的列表,所以它是一个有效的命令列表,即在所有命令之间使用换行作为控制运算符。</p>
</dd>
<dt>复合命令(Compound Commands)</dt>
<dd>
<p>复合命令指的是命令中包含特殊语法。他们可以做很多不同的事,但在命令列表中整体作为一条命令行事。最明显的例子就是命令块:组块本身就像单独一个命令,但是内部还含有一些构成命令(sub commands)。复合命令的种类也有很多,我们后面还会深度介绍他们。</p>
<pre class="syntax">
<strong>if</strong> <var>list</var> [ <strong>;</strong>|<strong><newline></strong> ] <strong>then</strong> <var>list</var> [ <strong>;</strong>|<strong><newline></strong> ] <strong>fi</strong>
<strong>{</strong> <var>list</var> <strong>;</strong> <strong>}</strong>
<samp><u title="复合命令“if”的起始">if</u> ! rm hello.txt; then echo "Couldn't delete hello.txt." >&2; exit 1; <u title="复合命令“if”的终止">fi</u></samp>
<samp>rm hello.txt || <u title="复合命令“组”的起始">{</u> echo "Couldn't delete hello.txt." >&2; exit 1; <u title="复合命令“组”的终止">}</u></samp>
</pre>
<p>上面这两个例子中的命令完成的是完全相同的操作。第一个例子是一个复合命令,第二个例子是一个命令列表中含有一个复合命令。前面已简单提及 <code>||</code> 运算符:除非它前面的命令运行失败,否则它右边的命令会被 bash 跳过不执行。这个例子很好地展示了复合命令的一个重要特点:他们行事就像命令列表中的单独一项命令。在第二个例子中,复合命令从左侧大括号 <code>{</code> 开始,一直到右侧大括号 <code>}</code> 结束,括号内部的所有代码被整体当作一个命令执行。也就是说,在第二个例子中,我们有一个包含两条命令的命令列表:<code>rm</code> 命令后面跟着 <code>{ ... }</code> 这条复合命令。如果去掉大括号,就是包含 <em>三个</em> 命令的命令列表:<code>rm</code> 命令后面跟着 <code>echo</code> 命令,再然后是 <code>exit</code> 命令。有或是没有大括号主要会影响 <code>||</code> 运算符,它需要决定如果前面 <code>rm</code> 命令运行成功接下来要做什么。如果 <code>rm</code> 成功,<code>||</code> 会使 bash 跳过它后面的命令。假设没有大括号,它后面就只有 <code>echo</code> 这一条命令需要跳过;而大括号将 <code>echo</code> 与 <code>exit</code> 命令合并为一个复合命令,在 <code>rm</code> 运行成功后,<code>||</code> 运算符会使 bash 同时跳过这两个命令。</p>
</dd>
<dt>协作进程</dt>
<dd>
<p>协作进程不过就是更多的语法糖:它使你可以轻松地异步(asynchronously)运行命令(不需要等待命令结束,也可以说是“在后台运行”),还能设置新的文件描述符插件来直接连接新命令的输入与输出。你不会经常用到协作进程,但是当你做一些高级复杂的任务时,使用他们会非常方便。</p>
<pre class="syntax">
<strong>coproc</strong> [ <var>name</var> ] <var>command</var> [ <var>redirection</var> ... ]
<samp>coproc auth { tail -n1 -f /var/log/auth.log; }
read latestAuth <&"${auth[0]}"
echo "Latest authentication attempt: $latestAuth"</samp>
</pre>
<p>上面这个例子中,启动了一个异步命令 <code>tail</code>。当它在后台运行时,其他的脚本命令持续运行。首先,脚本会从协作进程 <code>auth</code> 的输出中读取一行结果(也就是 <code>tail</code> 命令的第一行输出)。接下来我们编写一条消息,显示从协作进程中读取的最近一次认证尝试。这个脚本可以持续运行,每一次它都会从协作管道中读取信息,<code>tail</code> 命令会使它换行。</p>
</dd>
<dt>函数(Functions)</dt>
<dd>
<p>当你在 bash 中声明一个函数时,本质上你在创建一个临时的新命令,在后面的脚本中还可以再次召唤它。函数是一种非常好的方式,用来将一列命令结合成组并自定义命名,以便你在脚本中重复执行某个任务。</p>
<pre class="syntax">
<var>name</var> <strong>()</strong> <var>compound-command</var> [ <var>redirection</var> ]
<samp>exists() { [[ -x $(type -P "$1" 2>/dev/null) ]]; }
exists gpg || echo "Please install GPG." <&2</samp>
</pre>
<p>首先需为你的函数指定一个 <code>名称(name)</code>,这就是你新命令的名称,之后只需用它写一句简单命令就可以运行。</p>
<p>在命令的名称之后是一对括号 <code>()</code>。有些语言会使用括号来声明函数接受的参数,但 <strong>bash 不这样做</strong>。这对括号的内部应该始终为空。他们只是用来标注你在声明一个函数而已。</p>
<p>之后跟着的是你每次运行这个函数时将会被执行的复合命令。</p>
<p>如果运行函数期间要改变脚本的文件描述符,可以选择性指定函数的自定义文件重定向。</p>
</dd>
</dl>
<footer>
bash 命令会让 bash 执行特定单元的任务。这些单元任务不能再被拆分,也就是说 bash 需要知道完整的命令单元才能执行它。对于不同类型的操作有不同种类的命令。一些命令将其他命令组成块或者测试他们的执行结果。许多命令类型其实都是语法糖:即他们的效果可以不同方式实现,但他们的存在会使任务的执行简便很多。
</footer>
</section>
<section>
<h1>简单命令(simple commands):所有 bash 命令的基础</h1>
<p>哇,一下子好多内容是吧。前面讲的绝大多数东西肯定都已从你脑袋里溜走,但是没有关系。我们接下来将重返最简单的内容,帮助你基于透彻的理解逐渐构建自己的知识。重要的是记住 bash 有不同种类的命令,而且绝大多数的语法实际都很相似:大多数命令都包括 <dfn>重定向</dfn>,<dfn>控制运算符</dfn>,某种程度上也都接受子命令(subcommand)。后续我们还会解释这些概念,当下先确保我们已很好地理解 <dfn>简单命令</dfn>。</p>
<p>你是否充分理解简单命令非常关键,因为这是你之后在 bash 中做任何事的基础。在前面的介绍中,你或许已经注意到,所有其他 bash 命令都由至少一条简单命令构成。他们仅仅是把简单命令拿来并对其做了一些特殊操作罢了。</p>
<h2>命令名称与运行程序</h2>
<pre class="syntax">
[ <var>var</var><strong>=</strong><var>value</var> ... ] <mark><var>name</var></mark> [ <var>arg</var> ... ] [ <var>redirection</var> ... ]
</pre>
<p>再来看看简单命令的定义。我们会一步步地拆解它,因为虽然看起来很短,其实涉及很多内容。<br>我们首先聚焦命令的名称。名称(name)会告诉 bash 你这条命令是想让它做什么事。因此,为了理解并按你的意图行事,bash 会先执行<em>搜索(search)</em> 以找出具体要执行的任务。按顺序,bash 依据命令的 <code>名称(name)</code>会做如下搜索:</p>
<dl>
<dt><dfn>函数(function)</dfn></dt>
<dd>函数就是预先已被声明并命名的命令块。在前文你已大致看到我们如何声明一个函数。所有已声明的函数都被放在一个列表内,而bash会搜索这个列表来查看其中是否有和你要执行的命令同名的函数。</dd>
<dt><dfn>内建命令(builtin)</dfn></dt>
<dd>内建命令(builtin)是 bash 内部内置的微小程序。他们是编写在 bash 内部的小的操作,bash 不需要启动特别的程序去运行他们。我们之后会深入介绍 bash 提供的内建命令,以及他们的名称和作用。</dd>
<dt><dfn>程序(program)</dfn>也被称作<dfn>外部命令(also called an external command)</dfn></dt>
<dd>你的系统内安装了非常多的程序,有些负责小的任务,有些则执行大的任务。有些运行在终端内,有些运行的时候不可见,还有一些运行在你的图形界面内。Bash 会通过你系统配置的 <code>PATH</code> 查找这些程序。
</dd>
</dl>
<p>如果 bash 根据你的命令名称找不到可运行的程序,那么就会产生错误,bash 会把这个错误以如下消息形式报告给你:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>buy beer</kbd>
bash: buy: command not found
</pre>
<p>在此简单提一下 <dfn>别名(alias)</dfn>。在 bash 执行搜索之前,它首先会查看你是否曾将该命令的名称设置为别名。如果设置过,它会在执行前用别名对应的值替换命令名称。别名基本没什么用,仅仅在交互模式下略有些作用,而且差不多完全可以用函数替代。因此绝大多数情况下,你应该避免使用他们。</p>
<footer>
运行命令时,bash 会使用你的命令名称去搜索该如何执行它。按顺序,bash 会依次确认是否有一个 <dfn>函数(function)</dfn> 或 <dfn>内建命令(builtin)</dfn> 与你的命令名称一致。如果没有,它就会尝试把它作为一个程序去运行。如果 bash 没有任何办法运行你的命令,它就会输出一条报错消息。
</footer>
<h2>程序的<code>路径(PATH)</code></h2>
<p>我们电脑里安装了各种程序,且不同程序安装在不同的位置。一些程序是我们操作系统预装的,一些是我们的发行版添加的,还有一些是我们自己或系统管理员安装的。在一个标准的 UNIX 系统内,<a href="http://refspecs.linuxfoundation.org/fhs.shtml">有一些安装程序的标准位置</a>。有的程序安装在 <code>/bin</code> 内,有的在 <code>/usr/bin</code>,有的在 <code>/sbin</code>,等等。如果我们要完全记住所有这些程序的安装位置简直就太费劲了,特别是他们可能还会因不同的系统而变化。于是<dfn>环境变量</dfn> <code>PATH</code> 前来拯救我们了。你的 <code>PATH</code> 变量内包含了一组可以用来搜索程序的路径。</p>
<pre>
<span class="prompt">$ </span><kbd>ping 127.0.0.1</kbd>
<strong>PATH=</strong>/bin<strong>:</strong>/sbin<strong>:</strong>/usr/bin<strong>:</strong>/usr/sbin
│ │
│ ╰──▶ <mark>/sbin</mark>/ping ? <strong>找到啦!</strong>
╰──▶ <mark>/bin</mark>/ping ? 没找到
</pre>
<p>每当你要启动一个程序而 bash 不知道它在哪里时,就会去查看 PATH 变量下存储的这些路径。例如,假设你要启动安装在 <code>/sbin/ping</code> 的 <code>ping</code> 程序,如果你的 <code>PATH</code> 变量被设置为 <code>/bin:/sbin:/usr/bin:/usr/sbin</code>,那么 bash 首先会尝试启动 <code>/bin/ping</code>,结果不存在。接下来它就会尝试 <code>/sbin/ping</code>,如此就找到了 <code>ping</code> 程序,然后 bash 会记住这个位置以便你之后要再次运行 <code>ping</code>,剩下的就是为你运行找到的程序了。</p>
<p>如果你好奇 bash 根据命令名称到底在哪里找到要执行的程序的话,可以使用内建命令 <code>type</code> 查看:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>type ping</kbd>
ping is /sbin/ping
<span class="prompt">$ </span><kbd>type -a echo</kbd><em> -a 会告诉 type 向我们返回所有可能性</em>
echo is a shell builtin<em>如果我们运行 'echo',bash 就会执行第一种可能</em>
echo is /bin/echo<em>但内建变量 'echo' 之外,还有一个程序也叫 echo!</em>
</pre>
<p>还记得前面说过 bash 有一些内建命令吗?其中一种就是 <code>echo</code> 程序。如果你在 bash 中运行 <kbd>echo</kbd> 命令,甚至在 bash 尝试 <code>PATH</code> 搜索之前,它就会根据名字注意到这是一个内建命令,进而使用它。<code>type</code> 是一种非常好用的将查询过程视觉化的方法。注意:bash 运行内建命令的速度远远快于启动外部程序。但如果你需要的是 bash 之外的 <code>echo</code> 功能,你也可以运行外部 <code>echo</code> 程序。</p>
<p>有时你可能需要运行一个安装位置不在 <code>PATH</code> 路径下的程序,这种情况下,除命令名称外,你还需手动指明程序所在的路径,以便 bash 找到这个程序:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>/sbin/ping -c 1 127.0.0.1</kbd>
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
<span class="prompt">$ </span><kbd>./hello.txt</kbd><em>还记得我们的 hello.txt 脚本吗?</em>
Your name? <em>路径 “.” 表示“我们当前的路径”</em>
</pre>
<aside>bash 只会对命令名称中不含有 <code>PATH</code> 符号的命令进行 <code>PATH</code> 搜索。包含 <kbd title="slash">/</kbd> 符号的命令名称会被理解为执行程序的直接路径名。</aside>
<p>你还可以添加更多的路径到 <code>PATH</code> 中。通常的做法是加入 <code>/usr/local/bin</code> 和 <code>~/bin</code>(<code>~</code> 表示你这个用户的主目录(home directory))。记住<code>PATH</code> 是一个 <dfn>环境变量</dfn>,你可以按照下面的方法更新它:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>PATH=~/bin:/usr/local/bin:/bin:/usr/bin</kbd>
<span class="prompt">$ </span>
</pre>
<p>以上操作会改变你当前 bash shell 内的变量。一旦你关闭退出这个 shell,这次修改就会失效。在后面的章节中,我们会深入介绍环境变量的工作原理以及如何设置他们。</p>
<footer>
当 bash 运行程序时,它会使用命令名称先执行搜索。它会在你 <code>PATH</code> <dfn>环境变量</dfn> 内存储的路径中依次搜索,直到找到一个与你命令名称同名的程序。如果要运行的程序没有安装在 <code>PATH</code> 存储的路径中,则需要使用程序的路径名作为你命令的名称。
</footer>
<h3 id="path_ex">练习时间!</h3>
<h4>PATH.1. 运行 <code>ls</code> 程序。</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls</kbd></samp></pre>
<h4>PATH.2. 找出bash会从哪里找到 <code>ls</code> 程序</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>type ls</kbd><em>这里使用“type”或“command”都可以,“which”不对
ls 是 /bin/ls</em></samp></pre>
<h4>PATH.3. 显示你系统的 <var>PATH</var>.</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>echo "$PATH"</kbd>
/bin:/sbin:/usr/bin:/usr/sbin</samp></pre>
<h4>PATH.4. 在你的主目录下创建一个脚本,把它添加到你的 <var>PATH</var> 中,然后像一般命令那样运行它</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ex</kbd><em>这里可以用你自己喜欢的编辑器替代</em>
: <kbd>i</kbd>
<kbd>#!/usr/bin/env bash
echo "Hello world."
.</kbd>
: <kbd>w myscript</kbd>
"myscript" [New] 2L, 40C written
: <kbd>q</kbd>
<span class="prompt">$ </span><kbd>chmod +x myscript</kbd>
<span class="prompt">$ </span><kbd>PATH=$PATH:~</kbd>
<span class="prompt">$ </span><kbd>myscript</kbd>
Hello world.</samp></pre>
<h2>命令参数与引用文字</h2>
<pre class="syntax">
[ <var>var</var>=<var>value</var> ... ] <var>name</var> <mark>[ <var>arg</var> ... ]</mark> [ <var>redirection</var> ... ]
</pre>
<p>鉴于你已理解 bash 如何查找并运行你的命令,现在我们来学习如何将指令传达给命令,这些指令会告诉命令具体需要做什么。我们可能会运行 <code>rm</code> 命令来删除某个文件,使用 <code>cp</code> 命令复制文件,使用 <code>echo</code> 命令输出一个字符串,或是 <code>read</code> 命令读取一行文本。但是如果没有更多的细节信息,这些命令基本就很难做什么。我们需要告诉 <code>rm</code> 命令删除哪个文件,告诉 <code>cp</code> 复制什么文件以及复制后放在哪里。<code>echo</code> 需要知道你想让它输出什么信息,而 <code>read</code> 需被告知它读取的文本要放在哪里。因此我们使用参数来提供这些信息。</p>
<aside>请你一定注意以下信息: <strong>Bash shell 脚本中的绝大多数 bug 都是他们的作者没有正确理解命令参数直接导致的。</strong> 罪魁祸首通常都是:依赖直觉行事,而非遵照对规则的理解。</aside>
<p>在上面的语法中可以看到,参数紧随命令的 <var>名称(name)</var> 之后,他们是被空格隔开的单词。<strong>在 bash 语境下,当我们提到单词的时候,并不是指通常语言中的单词概念。</strong> 在 bash 中,单词被定义为 <dfn>可以被当作一个独立单元对待的一串字符序列</dfn>。<dfn>单词</dfn> 也被称为 <dfn>token</dfn>。一个 bash 单词中可以包含 <em>许多</em> 语言性的词汇,事实上,甚至可以包含散文。为了表述的清晰,这篇指南在接下来会统一全部使用 <dfn>参数</dfn> 概念来避免 <dfn>单词</dfn> 含义的模糊性。无论是称为单词还是参数,重要的是他们对于 shell 来说是一个独立单元:可以是文件名,变量名,程序名或人名:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>rm <mark>hello.txt</mark></kbd>
<span class="prompt">$ </span><kbd>mplayer <mark>'05 Between Angels and Insects.ogg'</mark> <mark>'07 Wake Up.ogg'</mark></kbd>
</pre>
<p>在上面的例子中,单词被高亮出来了。注意看他们并不是语言性的单词,但是是有意义的单元,都是文件名称。为了分隔多个参数,我们在其间使用空白,可以是空格或制表位(tab)。通常你会在参数之间使用一个空格。</p>
<aside>对于 bash shell 来说,<dfn>空白</dfn> 同样也是语法。它表示的是:<q>将前面的部分与下一部分隔断</q>。bash 将其称为:<dfn>单词分割(word splitting)</dfn>。</aside>
<p>一个问题由此产生:上面例子中,在 <code>05</code> 的后面我们使用了一个空格,将它与 <code>Between</code> 分开。Shell 怎么知道你的文件名称是 <code>05 Between Angels and Insects.ogg</code> 而非 <code>05</code> 呢?我们如何告诉 shell <code>05</code> 后面的空格是 <dfn>字面意义(literal)</dfn> 的而不是分割单词的 <dfn>语法</dfn>?我们的意图是要保全文件名称的整体性,也就是说,<em>名称内的空格不能将他们拆分为多个参数</em>。我们需要一种方式来告诉 shell,它应该按字面意义对待某些单词,即按他们原本整体的样子使用,忽略任何语法意义。如果我们可以使这些空格成为字面性的,他们就不会让 bash 分割 <code>05</code> 和 <code>Between</code>,bash 只会把空格当作普通的字符。</p>
<p>bash 中有两种方法使字符成为字面性的:<dfn>引用(quoting)</dfn> 和 <dfn>转义(excaping)</dfn>。引用指的是用双引号 <kbd title="双引号">"</kbd> 或单引号 <kbd title="单引号">'</kbd> 前后包裹我们想要使之保留字面意义的文本。转义是指在保留字面意义的字符前放置单个字符 <kbd title="反斜杠">\</kbd>。上面例子中使用了单引号将整个文件名称字面化(literal),但不包括文件名之间的那个空格。我们强烈建议你使用引用而非转义,这样代码会更清晰可读。更重要的是,转义的使用会使人极难区分你代码中的哪些部分是字面性的,哪些不是。此外,如果后续想要编辑字面性的文本内容同时不产生错误,也非常困难。如果使用转义而非引用,上面的例子就会成为这样:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>mplayer <mark>05\ Between\ Angels\ and\ Insects.ogg</mark> <mark>07\ Wake\ Up.ogg</mark></kbd>
</pre>
<p>作为 bash 用户,引用是你需要掌握的最重要的技能之一。它的重要性怎么强调都不为过。引用特别好的一点是,即使有时并非必需,引用你的数据基本不会出错。下面这两种方式都完全有效:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls -l <mark>hello.txt</mark></kbd>
-rw-r--r-- 1 lhunath staff 131 29 Apr 17:07 hello.txt
<span class="prompt">$ </span><kbd>ls -l <mark>'hello.txt'</mark></kbd>
-rw-r--r-- 1 lhunath staff 131 29 Apr 17:07 hello.txt
<span class="prompt">$ </span><kbd>ls -l <mark>'05 Between Angels and Insects.ogg'</mark> <mark>'07 Wake Up.ogg'</mark></kbd>
</pre>
<p>因此,如果有任何犹疑,引用你的数据。此外,绝对不要试图通过移除引号来使代码工作。</p>
<p>对于任何含有扩展(例如 <code>$variable</code> 或 <code>$(command)</code>)的参数,你应该使用 <mark>"<dfn>双引号</dfn>"</mark>;其他任何参数,使用 <mark>'<dfn>单引号</dfn>'</mark> 。单引号内的一切字符都会是字面性的,双引号则允许某些 bash 语法,如扩展,依然生效:</p>
<pre lang="bash">
echo <mark>"Good morning, $USER."</mark><em>双引号允许 bash 扩展 <code>$USER</code></em>
echo <mark>'You have won SECOND PRIZE in a beauty contest.'</mark> \<em>单引号甚至阻止 <code>$</code> 语法触发扩展</em>
<mark>'Collect $10'</mark>
</pre>
<p>千万不要放松警惕!下面这样是绝对错误的:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls -l <mark>05</mark> <mark>Between</mark> <mark>Angels</mark> <mark>and</mark> <mark>Insects.ogg</mark></kbd>
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
</pre>
<p>你的 shell 中不会有这些黄色高亮标识。试着养成习惯,自己在头脑中标识他们,从而避免犯错。你绝不会是第一个因一个游离或未加引用的空格字符而不小心毁掉主目录下全部文件的人。</p>
<p>你会发现,针对引用,养成一种实用主义的意识是很好的习惯:只要瞥一眼 bash 代码,未加引用的参数应该能立马跃入你眼前,而你在继续向下做任何事之前,感到有鼓强烈的冲动需先改正他们。人们寻求帮助的绝大多数 bash 问题,至少十分之九的核心问题都在引用上。看到就引用其实非常简单,而一个严于律己的引用规范用者会少去非常多麻烦。</p>
<aside class="rule">
<p>关于引用的黄金规则其实很简单:<br>
<q>如果你的参数中有空格或符号,那么你 <strong>必须</strong> 引用它。<br>
如果没有,引用则是可选的,但是安全起见,还是最好引用它。</q></p>
<p><em>要求</em> 无引用的参数 <strong>极其罕见</strong>,主要是在 <code>[[</code> 测试中以及 <code>${..+..}</code> 扩展周围。其他任何情况下,都不要试图通过移除或省略参数中的引用来使代码工作;如果这样做,你更可能引发严重且难以发现的 bug </p>
</aside>
<aside class="warn">
<p>引用缺失导致的危险有很多,作为一个非常简单的示例,请看下面如果你 <em>不小心</em> 在输入中添加一个空格会发生什么:</p>
<pre lang="bash" class="bad">
<span class="prompt">$ </span><kbd>read -p 'Which user would you like to remove from your system? ' username</kbd>
Which user would you like to remove from your system? <kbd> lhunath</kbd>
<span class="prompt">$ </span><kbd>rm -vr /home/$username</kbd>
removed '/home/lhunath/somefile'
removed directory: '/home/lhunath'
removed '/home/bob/bobsfiles'
removed directory: '/home/bob'
removed '/home/victor/victorsfiles'
removed directory: '/home/victor'
removed directory: '/home'
rm: cannot remove 'lhunath': No such file or directory
</pre>
<p>这里的问题是,在输入中,你不小心在要删除的用户名前加了一个<code>空格</code>,<code>rm</code> 命令因此扩展为<code>rm -vr <mark>/home/</mark> <mark>lhunath</mark></code>,由此同时危及了不相关的 Victor 和 Bob。<code>rm</code> 命令这下子首先删除了整个 <mark>/home/</mark> 路径,然后将移除<mark>lhunath</mark> 文件。如果在 <code>rm</code> 命令中正确使用了引用,那么最坏的结果也只是返回一条错误消息,而不会有任何损失:</p>
<pre lang="bash" class="good">
<span class="prompt">$ </span><kbd>rm -vr "/home/$username"</kbd>
rm: cannot remove '/home/ lhunath': No such file or directory
</pre>
</aside>
<footer>
为了告诉命令做什么,我们会传递 <dfn>参数</dfn> 给它。bash 中,参数就是 <dfn>tokens</dfn>,也被称作 <dfn>单词(words)</dfn>,相互之间用空白隔开。为了将空白也作为参数的值纳入,你需要对参数做 <dfn>引用</dfn> 或是 <dfn>转义</dfn> 其内的空白。如果没有这样做,bash 就会根据空白把你的一个参数分割为多个参数。引用参数还可以防止其他符号被错误解释为 bash 代码,例如 <code>'$10 USD'</code>(变量扩展),<code>"*** NOTICE ***"</code>(文件名扩展)等。
</footer>
<h2>使用重定向管理命令的输入与输出</h2>
<pre class="syntax">
[ <var>var</var>=<var>value</var> ... ] <var>name</var> [ <var>arg</var> ... ] <mark>[ <var>redirection</var> ... ]</mark>
</pre>
<p>我们已简单介绍过 <dfn>文件描述符</dfn> 的概念,以及如何使用他们来连接进程。现在我们来看看如何在 bash 中实现这些操作。</p>
<p>回顾一下,进程使用文件描述符与流连接。每一个进程通常都有三种标准文件描述符: <dfn>标准输入(FD 0)</dfn>,<dfn>标准输出(FD 1)</dfn> 和 <dfn>标准错误输出(FD 2)</dfn>。当 bash 启动一个程序,它首先会为程序设置一组文件描述符。这组文件描述符与 bash 自己的完全一样,即这个新的进程“ <strong>继承</strong> ”了 bash 的文件描述符。当你打开终端进入一个新的 bash shell,终端会将 bash 的输入和输出与自己相连。这样一来你键盘上输入的字符才进入到 bash中,而来自 bash 的消息最终会显示在你的终端窗口内。每次 bash 启动一个程序,它就会为这个程序设置一组与自己相同的文件描述符。如此一来,bash 命令的消息最终也会传到你的终端内,你键盘上的输入也会传送到程序中去(即命令的输出和输入与你的终端相连)。</p>
<pre>
╭──────────╮
Keyboard ╾──╼┥0 bash 1┝╾─┬─╼ Display
│ 2┝╾─┘
╰──────────╯
<span class="prompt">$ </span><kbd>ls -l a b</kbd><em>想象我们有一个文件 “a”,但没有文件 “b“</em>
ls: b: No such file or directory<em>错误消息会发送至 FD 2</em>
-rw-r--r-- 1 lhunath staff 0 30 Apr 14:43 a<em>结果会发送至 FD 1</em>
╭──────────╮
Keyboard ╾┬─╼┥0 bash 1┝╾─┬─╼ Display
│ │ 2┝╾─┤
│ ╰─────┬────╯ │
│ ╎ │
│ ╭─────┴────╮ │
└─╼┥0 ls 1┝╾─┤
│ 2┝╾─┘
╰──────────╯
</pre>
<p>当 <code>bash</code> 启动 <code>ls</code> 进程,它首先查看自己的文件描述符,然后再为 <code>ls</code> 进程创建相同的文件描述符,并连接至与自己完全相同的流:即与 <code>显示(Display)</code> 相连的 FD1、FD2,以及与 <code>键盘(keyboard)</code> 相连的 FD 0。结果,<code>ls</code> 的错误消息(输出至 FD 2)以及它常规的结果输出(传递至 FD 1)最终都会显示在你的终端窗口内。</p>
<p>如果我们想要控制决定命令连接到哪里,就需要用到 <dfn>重定向(redirection)</dfn>,使用这一操作会改变文件描述符的来源或终点。使用重定向我们可以将 <code>ls</code> 的结果写入一个文件内,而不再传至终端显示:</p>
<pre>
╭──────────╮
Keyboard ╾──╼┥0 bash 1┝╾─┬─╼ Display
│ 2┝╾─┘
╰──────────╯
<span class="prompt">$ </span><kbd>ls -l a b <mark>>myfiles.ls</mark></kbd><em>我们将 FD 1 重定向至文档 “myfiles.ls”</em>
ls: b: No such file or directory<em>错误消息发送至FD 2</em>
╭──────────╮
Keyboard ╾┬─╼┥0 bash 1┝╾─┬─╼ Display
│ │ 2┝╾─┤
│ ╰─────┬────╯ │
│ ╎ │
│ ╭─────┴────╮ │
└─╼┥0 ls 1┝╾─╌─╼ myfiles.ls
│ 2┝╾─┘
╰──────────╯
<span class="prompt">$ </span><kbd>cat myfiles.ls</kbd><em>cat 命令可以向我们展示文件内容</em>
-rw-r--r-- 1 lhunath staff 0 30 Apr 14:43 a<em>结果现存在 myfiles.ls</em>
</pre>
<p>通过将命令的标准输出重定向至一个文件,你刚完成了一次文件重定向操作。标准输出的重定向是通过使用 <code>></code> 控制运算符实现的。可以将它想象为一个箭头,把输出从命令传送至文件。这是目前最常见也是最有用的重定向方式。</p>
<p>此外,重定向还常被用来隐藏错误消息。你会注意到我们重定向之后的 <code>ls</code> 命令仍然显示错误消息,通常这是好事。但有的时候,我们可能会觉得脚本中一些命令产生的报错消息对于用户来说是不重要的,应该被隐藏。为此,我们可以再次使用文件重定向,以相似的方式重定向标准错误输出,使 <code>ls</code> 的结果消失:</p>
<pre>
╭──────────╮
Keyboard ╾──╼┥0 bash 1┝╾─┬─╼ Display
│ 2┝╾─┘
╰──────────╯
<span class="prompt">$ </span><kbd>ls -l a b <mark>>myfiles.ls</mark> <mark>2>/dev/null</mark></kbd><em>我们将 FD 1 重定向至 “myfiles.ls”</em>
<em>将 FD 2 重定向至文档 "/dev/null"</em>
╭──────────╮
Keyboard ╾┬─╼┥0 bash 1┝╾─┬─╼ Display
│ │ 2┝╾─┘
│ ╰─────┬────╯
│ ╎
│ ╭─────┴────╮
└─╼┥0 ls 1┝╾───╼ myfiles.ls
│ 2┝╾───╼ /dev/null
╰──────────╯
<span class="prompt">$ </span><kbd>cat myfiles.ls</kbd><em>cat 命令会向我们展示文档内容</em>
-rw-r--r-- 1 lhunath staff 0 30 Apr 14:43 a<em>结果现存在 myfiles.ls 中</em>
<span class="prompt">$ </span><kbd>cat /dev/null</kbd><em>/dev/null 文档是空的?</em>
<span class="prompt">$ </span>
</pre>
<p>注意看,通过在 <code>></code> 控制运算符的前面注明 FD 编号,你可以重定向任何 FD。我们使用 <code>2></code> 将 FD 2 重定向至 <code>/dev/null</code>,使用 <code>></code> 将 FD 1 仍旧重定向至 <code>myfiles.ls</code>。如果你省略了编号,输出重定向的默认项是 FD 1(标准输出)。</p>
<p>我们的 <code>ls</code> 命令不再显示错误消息,结果也被保存在 <code>myfiles.ls</code>。那么错误消息到哪里去了呢?我们已经将它写入文件 <code>/dev/null</code>。但是当我们显示该文件内容时,并没有看到错误消息。难道出了什么错?</p>
<p>解开这个小小谜团的线索在于路径名称。文件 <code>null</code> 位于 <code>/dev</code> 路径下:这是一个存放 <dfn>设备文件</dfn> 的特殊路径。设备文件是一种特殊文件,他们用来代表系统内的设备。当我们从中读取或向内写入数据时,是通过内核与他们直接交流。那个 <code>null</code> 设备是一个总是为空的特殊设备。你向其中写入的任何东西都会遗失,从中也不能读取任何信息。因此,对于丢弃信息来说它是一个非常好用的设备。我们将不想要的错误消息流向这个 <code>null</code> 设备,他们就遗失不见了。</p>
<p>如果我们想把通常显示在终端窗口内的所有输出,包括命令执行结果以及错误消息,都保存在 <code>myfiles.ls</code> 呢?直觉可能会是这样:</p>
<pre lang="bash" class="bad">
<span class="prompt">$ </span><kbd>ls -l a b <mark>>myfiles.ls</mark> <mark>2>myfiles.ls</mark></kbd><em>将两个文件描述符都重定向至 myfiles.ls?</em>
╭──────────╮
Keyboard ╾┬─╼┥0 bash 1┝╾─┬─╼ Display
│ │ 2┝╾─┘
│ ╰─────┬────╯
│ ╎
│ ╭─────┴────╮
└─╼┥0 ls 1┝╾───╼ myfiles.ls
│ 2┝╾───╼ myfiles.ls
╰──────────╯
<span class="prompt">$ </span><kbd>cat myfiles.ls</kbd><em>取决于两条流如何抵达并交汇,内容很可能是错乱的</em>
-rw-r--r-- 1 lhunath stls: b: No such file or directoryaff 0 30 Apr 14:43 a
</pre>
<p>如果你这样想就 <strong>错了!</strong>为什么不对呢?乍一看 <code>myfiles.ls</code> 似乎没问题,但实际可能会很危险。如果你足够幸运,会看到文档内的输出结果与你期待的不太一致,内容可能有些混乱、无序,甚至还有可能完全正确。但问题是,你不能预测也不能保证命令的结果。</p>
<p>这里到底发生了什么?问题出在两个文件描述符现在都将他们的流连接至这个文档。因为流内部的工作方式,这种操作是有问题的,不过这个话题已超出本指南的讨论范围,总之就是当两条流被融合到一个文件后,其结果会是两条流任意随机地混合。</p>
<p>为了解决这个问题,你需要将输出与错误消息都发送到同一条流内,进而你需要知道如何 <dfn>复制文件描述符</dfn>:</p>
<pre lang="bash" class="good">
<span class="prompt">$ </span><kbd>ls -l a b <mark>>myfiles.ls</mark> <mark>2>&1</mark></kbd><em>使FD 2 写入FD 1 所写之处</em>
╭──────────╮
Keyboard ╾┬─╼┥0 bash 1┝╾─┬─╼ Display
│ │ 2┝╾─┘
│ ╰─────┬────╯
│ ╎
│ ╭─────┴────╮
└─╼┥0 ls 1┝╾─┬─╼ myfiles.ls
│ 2┝╾─┘
╰──────────╯
<span class="prompt">$ </span><kbd>cat myfiles.ls</kbd>
ls: b: No such file or directory
-rw-r--r-- 1 lhunath staff 0 30 Apr 14:43 a
</pre>
<p>复制文件描述符,就是将一个文件描述符的流连至另一个文件描述符。如此一来,两个文件描述符就与同一条流相连。我们会使用 <code>>&</code> 控制运算符,在它前面是我们想要改变的文件描述符,后面跟着我们想要复制进而连入同一条流的文件描述符。之后你会经常用到这个控制运算符,并且在绝大多数应用场景中都是像上面例子那样将 FD 2 复制为 FD 1。你可以把语法 <code>2>&1</code> 理解为 <q>使 FD <code>2</code> 写入(<code>></code>)FD(<code>&</code>)<code>1</code> 正在写入的地方</q>。</p>
<p>到此,我们已见到不少重定向操作,甚至已经会结合使用他们。在你自由驰骋之前,还有一条重要规则需要理解:重定向是从左至右评估执行的,和我们阅读的顺序是一致的。这看起来似乎没什么特别的,但是忽略这个规则已导致你前面许多人犯过如下错误:</p>
<pre lang="bash" class="bad">
$ <kbd>ls -l a b <mark>2>&1</mark> <mark>>myfiles.ls</mark></kbd><em>使 FD 2 连至 FD 1,FD 1 连至 myfiles.ls?</em>
</pre>
<p>写下如上代码的人可能认为 FD 2 的输出连至 FD 1,而 FD 1 又会输出至 <code>myfiles.ls</code>,进而错误消息最终会在这个文档中。他们这种推理的逻辑错误在于认为 <code>2>&1</code> 会将 FD 2 的输出发送至 FD 1。<strong>但并非如此。</strong> 它会将 FD 2 的输出发送至 FD 1 连接的 <strong><em>流</em></strong>,此时可能是 <strong>终端</strong> 而非那个文档,因为 FD 1 尚未被重定向。上面命令的结果可能会让人沮丧,因为看起来似乎是标准错误的重定向没有生效,但实际上,你仅仅是把标准错误重定向到终端(标准输出的目的地),而这正是之前它已被指向的地方。</p>
<p>如果我们修正重定向的顺序:</p>
<pre lang="bash" class="good">
$ <kbd>ls -l a b <mark>>myfiles.ls</mark> <mark>2>&1</mark></kbd><em>使 FD 1 输出至 myfiles.ls,同时将 FD 2 设置为相同目的地</em>
</pre>
<p>现在我们将 FD 1 的输出流至 <code>myfiles.ls</code>,然后将 FD 2 指向 FD 1 当前使用的流,即 <code>myfiles.ls</code>。两个文件描述符就都以 <code>myfiles.ls</code> 为目标,命令 <code>ls</code> 的任何输出,无论是写入 FD 2 还是 FD 1,最终都会存至该文档。</p>
<p>此外还有很多其他重定向控制运算符,但都不如我们以上学习的这些有用。对于人们来说,<em>已</em> 被证明肯定有用的是,学习像阅读英文那样阅读命令的重定向。接下来我会逐个列举 bash 的重定向命令运算符,每个都附上一段简短的描述,以及你可以用来将操作命令翻译为日常英语的一句话。</p>
<dl>
<dt><dfn>文件重定向(File redirection)</dfn></dt>
<dd>
<pre class="syntax">
[<var>x</var>]<strong>></strong><var>file</var>, [<var>x</var>]<strong><</strong><var>file</var>
<samp>echo Hello <mark>>~/world</mark></samp>
<samp>rm file <mark>2>/dev/null</mark></samp>
<samp>read line <mark><file</mark></samp>
</pre>
使 FD <var>x</var> 写入或读取 <var>文件(file)</var>。
<p>为写入或读取,打开通向 <var>文件(file)</var> 的流,并连接至文件描述符 <var>x</var>。如果省略 <var>x</var>,默认设置是 FD 1(标准输出)写入,FD 0(标准输入)读取。</p>
</dd>
<dt><dfn>文件描述符复制(File descriptor copying)</dfn></dt>
<dd>
<pre class="syntax">
[<var>x</var>]<strong>>&</strong><var>y</var>, [<var>x</var>]<strong><&</strong><var>y</var>
<samp>ping 127.0.0.1 >results <mark>2>&1</mark></samp>
<samp>exec <mark>3>&1</mark> >mylog; echo moo; exec <mark>1>&3</mark> 3>&-</samp>
</pre>
使 FD <var>x</var> 写入或读取 FD <var>y</var> 的流 。
<p>将 FD <var>x</var> 连接至FD <var>y</var> 的流。第二个例子很复杂:为了理解它你需要知道 <code>exec</code> 可以用来改变 bash 自身的文件描述符(而不是一个新命令的),而且,如果你使用尚不存在的 <var>x</var>,bash 会使用这个编号为你创建一个新的文件描述符。</p>
</dd>
<dt><dfn>追加文件重定向(Appending file redirection)</dfn></dt>
<dd>
<pre class="syntax">
[<var>x</var>]<strong>>></strong><var>file</var>
<samp>echo Hello >~/world
echo World <mark>>>~/world</mark></samp>
</pre>
使 FD <var>x</var> 追加至 <var>文件(file)</var>末尾。
<p>在追加模式下,连接 <var>文件(file)</var> 且供写入用的流会打开,连向文件描述符 <var>x</var>。使用常规文件重定向控制运算符 <code>></code> 会首先清空文档内之前存储的全部内容,然后写入本次重定向输出过去的内容。在追加模式下(<code>>></code>),文档内原有内容仍会保留,流只会把本次输出内容添加在原有内容的末尾。</p>
</dd>
<dt><dfn>重定向标准输出与标准错误输出(Redirecting standard output and standard error)</dfn></dt>
<dd>
<pre class="syntax">
<strong>&></strong><var>file</var>
<samp>ping 127.0.0.1 <mark>&>results</mark></samp>
</pre>
将 FD 1(标准输出)与 FD 2(标准错误输出)都写入 <var>文件(file)</var>。
<p>这是和 <code>><var>file</var> 2>&1</code> 效果相同,但是更精简方便的控制运算符。同样的,可以使用双箭头 <code>&>><var>file</var></code> 实现追加写入效果。</p>
</dd>
<dt><dfn>Here 文档(Here Documents)/dfn></dt>
<dd>
<pre class="syntax">
<strong><<</strong>[<strong>-</strong>]<var>delimiter</var>
<var>here-document</var>
<var>delimiter</var>
<samp>cat <mark><<.</mark> <em>我们选择 <code>.</code> 作为终止定界符</em>
<mark>Hello world.</mark>
<mark>Since I started learning bash, you suddenly seem so much bigger than you were before.</mark>
<mark>.</mark><em>我们前面选择了 <code>.</code> 这个字符标识here文档的结束</em></samp>
</pre>
使 FD 0(标准输入)读取 <var>定界符(delimiter)</var> 之间的字符串。
<p>Here 文档是将大块文本内容喂给命令作输入的非常好用的方式。他们起始于你选用的定界符之后,终止于 bash 遇到一行 <em>只</em> 含有该定界符的命令。一定要记得你的终止定界符前不能有缩进,因为如此一来这一行中就不仅有定界符了(还有空白)。</p>
<p>在你的起始定界符前可以放置 <code>-</code>,这样 bash 就会忽略你添加在 here 文档前的所有制表符(tab)。这样,你就可以缩进 here 文档,但作为输入的字符串不会显示缩进。同样,终止定界符前也因此可以使用 tab 缩进。</p>
<p>最后要说的是,也可以在 here 文档的字符串中使用 <dfn>变量扩展(variable expansions)</dfn>,这样你就可以在文档中置入变量数据。关于变量和扩展,后面我们还会学习更多,这里你只需要知道如果要回避使用扩展,你需要在首次声明 <code>'<var>定界符</var>'</code> 时为它加上引号。</p>
</dd>
<dt><dfn>Here 字符串(Here Strings)</dfn></dt>
<dd>
<pre class="syntax">
<strong><<<</strong><var>string</var>
<samp>cat <mark><<<"Hello world.
Since I started learning bash, you suddenly seem so much bigger than you were before."</mark></samp>
</pre>
使 FD 0(标准输入)从 <var>字符串</var> 中读取。
<p>Here 字符串与 here 文档非常相似但是更精简,因此更推荐使用。</p>
</dd>
<dt><dfn>关闭文件描述符(Closing file descriptors)</dfn></dt>
<dd>
<pre class="syntax">
<var>x</var><strong>>&-</strong>, <var>x</var><strong><&-</strong>
<samp>exec 3>&1 >mylog; echo moo; exec 1>&3 <mark>3>&-</mark></samp>
</pre>
关闭 FD <var>x</var>。
<p>断开文件描述符 <var>x</var> 与流的连接,并将它从进程中移除。除非被再次创建,否则这个文件描述符不能再被使用。当 <var>x</var> 省略的时候,<code>>&-</code> 默认会关闭标准输出,而 <code><&-</code> 默认会关闭标准输入。你基本不会用到这个运算控制符。</p>
</dd>
<dt><dfn>移动文件描述符(Moving file descriptors)</dfn></dt>
<dd>
<pre class="syntax">
[<var>x</var>]<strong>>&</strong><var>y</var><strong>-</strong>, [<var>x</var>]<strong><&</strong><var>y</var><strong>-</strong>
<samp>exec <mark>3>&1-</mark> >mylog; echo moo; exec <mark>>&3-</mark></samp>
</pre>
用 FD <var>y</var> 替换 FD <var>x</var>。
<p>文件描述符 <var>y</var> 复制到 <var>x</var> 然后关闭 <var>y</var>,即用 <var>y</var> 替换 <var>x</var>。也是 <code>[<var>x</var>]>&<var>y</var> <var>y</var>>&-</code> 的简便操作。同样的,你基本也不会用到它。</p>
</dd>
<dt><dfn>使用文件描述符读取或写入(Reading and writing with a file descriptor)</dfn></dt>
<dd>
<pre class="syntax">
[<var>x</var>]<strong><></strong><var>file</var>
<samp>exec <mark>5<>/dev/tcp/ifconfig.me/80</mark>
echo "GET /ip HTTP/1.1
Host: ifconfig.me
" >&5
cat <&5</samp>
</pre>
为读取和写入 <var>文档(file)</var>,打开 FD <var>x</var>。
<p>文件描述符 <var>x</var> 被打开,并通过流与可以读取和写入字节的文档相连。通常为实现这一目的,你会使用两个文件描述符。但在一些罕见情况下,像是要将流与读/写设备如网络接口(network socket)相连时,这种方式会很有用。上面例子就将数行 HTTP 写入 host 在 80 端口(标准 HTTP 端口)的 <code>ifconfig.me</code>,并读取网络返回的数据,使用的都是 <code>exec</code> 为此配置的相同的文件描述符 <code>5</code>。</p>
</dd>
</dl>
<p>作为对重定向最后的说明,我想要指出的是对于简单命令,重定向控制运算符可以出现在命令中的任何位置,也就是说,他们不一定是在命令的末尾出现。虽然出于一致性以及在长命令中避免出现意外或遗漏运算符的考虑,将重定向控制运算符放在命令末尾是一个好主意,但是有些情况下,一些人习惯将他们放在其他位置。特别是当有一串 <code>echo</code> 或 <code>printf</code> 命令时,为了可读性,会习惯将重定向控制运算符放在命令名称之后:</p>
<pre lang="bash">
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."
</pre>
<p> </p>
<footer>
默认地,新的命令会继承 shell 当前的文件描述符。我们可以使用 <dfn>重定向</dfn> 改变命令输入的来源以及输出的走向。文档重定向(如<code>2>errors.log</code>)允许我们将文件描述符的流引向文档。我们可以复制文件描述符(如<code>2>&1</code>)使他们共享同一个流。此外还有很多其他更高级的重定向运算符。
</footer>
<h3 id="redir_ex">练习时间!</h3>
<h4>REDIR.1. 执行一条命令,在标准输出处生成一条消息</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls /bin/bash</kbd>
/bin/bash*</samp></pre>
<h4>REDIR.2. 执行一条命令,在标准错误输出处生成一条消息</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls /bob/bash</kbd>
ls: /bob/bash: No such file or directory</samp></pre>
<h4>REDIR.3. 执行一条命令,同时在标准输出与标准错误输出处各生成一条消息</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls /bin/bash /bob/bash</kbd>
ls: /bob/bash: No such file or directory
/bin/bash*</samp></pre>
<h4>REDIR.4. 将上一条命令的标准错误消息发送至一个名为 <code>errors.log</code> 的文档,并在终端内显示该文档的内容</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls /bin/bash /bob/bash 2>errors.log</kbd>
/bin/bash*
<span class="prompt">$ </span><kbd>cat errors.log</kbd>
ls: /bob/bash: No such file or directory</samp></pre>
<h4>REDIR.5. 将上一条命令的标准输出与标准错误输出消息追加至名为 <code>errors.log</code> 的文档,然后再次在终端内显示该文档的内容</h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>ls /bin/bash /bob/bash >>errors.log 2>&1</kbd>
<span class="prompt">$ </span><kbd>cat errors.log</kbd>
ls: /bob/bash: No such file or directory
ls: /bob/bash: No such file or directory
/bin/bash*</samp></pre>
<h4>REDIR.6. 使用 here 字符串在终端内显示字符 <kbd>Hello world.</kbd></h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>cat <<< 'Hello world.'</kbd>
Hello world.</samp></pre>
<h4>REDIR.7. 修改后面的命令,使得消息可以被正确保存在 <code>log</code> 文档内,之后关闭 FD 3:<kbd>exec 3>&2 2>log; echo 'Hello!'; exec 2>&3</kbd></h4>
<pre lang="bash" class="exercise"><samp>$ <kbd>exec 3>&1 >log; echo 'Hello!'; exec 1>&3 3>&-</kbd></samp></pre>
</section>