-
Notifications
You must be signed in to change notification settings - Fork 1
/
expansions.html
1314 lines (1110 loc) · 92.6 KB
/
expansions.html
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
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
id: variables
layout: chapter
chapter: 3
title: 变量与扩展
subtitle: 我要如何存储和使用数据?
status: alpha
description: >-
Bash 参数与变量;环境变量,特殊参数与数组参数;扩展参数,扩展运算符,命令替换以及进程替换;路径名扩展,波浪号扩展以及花括号扩展。
published: true
---
<section>
<h1>什么是扩展?</h1>
<p>我们已知道如何使用 bash 编写和管理简单命令,这些命令使我们直抵系统内众多强有力的功能。我们已学习命令如何让 bash 为程序新建进程从而运行他们。我们已理解何为命令参数,以及如何将信息传递给命令,从而让他们完成我们需要做的事。</p>
<p>基于以上所有这些知识,我们已开始体验到 shell 成事的力量。这种感觉就好像我们现在正使用一种全新的语言直接与系统进行交流,命令就是任务,而参数是关于这些任务该如何被执行的具体指令。</p>
<p>当前,我们面临的一个主要局限是,以参数的形式、如此明确具体地向命令传递信息其实非常受限。如果我们需要拼写出每个待操作文件的名称、每一个要在屏幕上显示或被程序操纵的字节,这就意味着,如果我们对于要做的事没有绝对清晰的认识,我们是无法编写程序的。因此我们需要找到一种方式,使命令更加动态(dynamic),将他们转化成关于执行某种操作的类似模板的东西,这样我们就可以时不时地复用(re-use)他们。</p>
<p>假设我们现在想要删除 <code>下载(Downloads)</code> 目录中的所有文件。基于我们已掌握的知识,可以先查看该目录下当前都有什么文件,然后再一一删除他们:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>cd ~/Downloads</kbd>
<span class="prompt">$ </span><kbd>ls</kbd>
05 Between Angels and Insects.ogg
07 Wake Up.ogg
<span class="prompt">$ </span><kbd>rm -v '05 Between Angels and Insects.ogg' '07 Wake Up.ogg'</kbd>
removed '05 Between Angels and Insects.ogg'
removed '07 Wake Up.ogg'
<span class="prompt">$ </span><kbd>ls</kbd>
<span class="prompt">$ </span>
</pre>
<p>真棒。我们的 <code>ls</code> 命令没有再返回任何结果,说明当前目录已为空。<br>
但是,如果我们不需要全知一切岂不是很好?毕竟,我们的 <em>意图</em> 只是要清空 <code>下载(Downloads)</code> 目录罢了。为了实现这一目的,当前我们需要手动进入目录下,找出所有文件,下达 <code>rm</code> 命令,通过列举所有文件名进而清空目录。现在让我们来改进一下这个流程,使代码再 <em>动态(dynamic)</em> 一些。我们想要实现的是获得一种可重复使用的、执行某任务(job)的模板。该任务模板描述出一种实现我们意图的方式,且这种方式不受我们当前实际所处的具体情境的限制。</p>
<p>为了做到这一点,就需要把代码中涉及具体情境的部分全部移除。上面例子中,与之对应的就是我们想要删除的所有文件的具体名称。每当我们要清空下载目录时,并非都是要删除这两个文件。我们想要做的只是删除 <em><code>下载(Downloads)</code> 目录下的所有文件</em>,不管他们的名称到底是什么。上面例子中的方法之所以行得通,是因为有中间一步,作为人类的我们,要去查看目录中所有文件的名称,并把他们写作 <code>rm</code> 命令的参数。我们怎样将这个过程自动化实现呢?</p>
<h2>路径名扩展(Pathname Expansion)</h2>
<p>在 bash 为我们提供的众多扩展形式中,答案就随第一种而来。欢迎来到 <dfn>路径名扩展</dfn>:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>cd ~/Downloads</kbd>
<span class="prompt">$ </span><kbd>rm -v <mark>*</mark></kbd>
removed '05 Between Angels and Insects.ogg'
removed '07 Wake Up.ogg'
<span class="prompt">$ </span><kbd>ls</kbd>
<span class="prompt">$ </span>
</pre>
<p>那些我们想要删除的文件名称呢,发生了什么?我们已用一种模式替换了他们,这种模式会使 bash 自动 <em>为我们扩展路径名</em>。<dfn>扩展(expansion)</dfn> 是用具体情境相关的代码替换我们命令中某部分代码的操作。在这个例子中,我们想用下载目录内每一个文件的路径名替换命令中的 <code>*</code>。用路径名替换模式因此就被称为路径名扩展。</p>
<p>在上面例子中,bash 会注意到在命令行内它期待看到参数的位置上,你现在放上了一种路径名模式。于是它会采用这种路径名模式,继续去文件系统中寻找所有它能找到的、与这个模式匹配的路径名。结果就是,<code>*</code> 这种模式与当前路径下所有的文件名都匹配。因此,bash 会用当前路径下的每一个文件名替换我们命令行中的模式。我们自己就什么都不用做啦!一旦 bash 用 <code>'05 Between Angels and Insects.ogg' '07 Wake Up.ogg'</code> 替换掉我们的 <code>*</code>,它就会召唤 <code>rm</code> 命令使用完整的参数 <code>-v '05 Between Angels and Insects.ogg' '07 Wake Up.ogg'</code> 工作。最终结果就是我们的下载目录如愿被清空。棒极了。</p>
<aside>
<p>一定要理解,虽然代码中 <code>rm</code> 命令看似有一个明显的 <code>*</code> 参数,但我们 <em>并不是真的把 <code>*</code> 传递给 <code>rm</code> </em>。实际上, <code>rm</code> 命令甚至根本都不会看到路径名扩展模式。Bash 会对模式进行评估与扩展替换,之后才会启动命令 <code>rm</code>。据 <code>rm</code> 所知,它仅仅接收到一个 <code>-v</code> 参数,后面跟着路径下每一个文件的全称。扩展工作都是 <em>bash</em> 自己完成的,且永远在实际运行命令 <em>之前</em> !</p>
</aside>
<p>Bash 可以为我们执行各种路径名扩展,我们仅需要在想要扩展路径名的位置上,写下一个句法性的 glob 模式。Glob 是 bash shell 支持的一种模式类型的名字。下面是 bash shell 支持的多种基础性 glob 模式:</p>
<table>
<tr>
<th>Glob</th>
<th>意义</th>
</tr>
<tr>
<th><code class="syntax"><strong>*</strong></code></th>
<td>星号匹配任何类型的文本,甚至包括空。</td>
</tr>
<tr>
<th><code class="syntax"><strong>?</strong></code></th>
<td>问号匹配任何单个字符。</td>
</tr>
<tr>
<th><code class="syntax"><strong>[</strong><var>characters</var><strong>]</strong></code></th>
<td>方括号内的一组字符匹配内含其中的任一单个字符。</td>
</tr>
<tr>
<th><code class="syntax"><strong>[[:</strong><var>classname</var><strong>:]]</strong></code></th>
<td>当方括号内直接含有一组冒号的时候,你无需再一一列举写下所有字符,可以直接指定这一类字符的类名。<br>
Bash 知道很多种字符类。例如,如果你使用 <code>[[:<var>alnum</var>:]]</code> 模式,bash 就只会与字母数字型的字符做匹配。Bash 支持的字符类包括:<br>
<var>alnum</var>,<var>alpha</var>,<var>ascii</var>,<var>blank</var>,<var>cntrl</var>,<var>digit</var>,<var>graph</var>,<var>lower</var>,<var>print</var>,<var>punct</var>,<var>space</var>,<var>upper</var>,<var>word</var>,<var>xdigit</var></td>
</tr>
</table>
<p>结合使用上述 glob 模式,我们就可以描述各种可能的路径名组合。我们也可以将 glob 模式与表示字面含义的字符结合使用,来告诉 bash 某部分模式内要严格包含什么文本:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls</kbd><em>没有参数,<code>ls</code> 就列出某路径下的全部内容。</em>
myscript.txt
mybudget.xsl
hello.txt
05 Between Angels and Insects.ogg
07 Wake Up.ogg
<span class="prompt">$ </span><kbd>ls *</kbd><em>虽然效果相同,但这个命令实际上在 <code>ls</code> 的参数中</em>
myscript.txt<em>列举了路径下每一个文件的名称</em>
mybudget.xsl
hello.txt
05 Between Angels and Insects.ogg
07 Wake Up.ogg
<span class="prompt">$ </span><kbd>ls *.txt</kbd><em>当模式中包含了字面性字符串 <code>.txt</code> 后,仍与模式匹配的是</em>
myscript.txt<em>那些以任意文本起始但以字面性字符串 <code>.txt</code> 结束的路径名</em>
hello.txt
<span class="prompt">$ </span><kbd>ls 0<mark class="green">?</mark>' '<mark class="blue">*</mark>.ogg</kbd><em>这里我们将模式结合使用,要找的是这样的路径名,以 <code>0</code> 开始,</em>
0<mark class="green">5</mark> <mark class="blue">Between Angels and Insects</mark>.ogg<em>后面跟一个任意的字符,之后是一个 <strong>字面性的</strong> 空格, 最终以 <code>.ogg</code> 结束</em>
0<mark class="green">7</mark> <mark class="blue">Wake Up</mark>.ogg
<span class="prompt">$ </span><kbd>ls <mark>[0-9]</mark>*</kbd><em>在一个字符集合中,我们可以使用 <code>-</code> 表示字符的范围</em>
<mark>0</mark>5 Between Angels and Insects.ogg<em>这个模式匹配的路径名需以 <code>0</code> 至 <code>9</code> 中的任一字符起始,后面可跟任意文本</em>
<mark>0</mark>7 Wake Up.ogg
<span class="prompt">$ </span><kbd>ls <mark>[[:digit:]][[:digit:]]</mark>*</kbd><em>字符类是真的好,因为他们替我们说话:在此他们完全表达出了我们的意图</em>
<mark>05</mark> Between Angels and Insects.ogg<em>即我们想要任何以两位数字起始的路径名</em>
<mark>07</mark> Wake Up.ogg
<span class="prompt">$ </span><kbd>ls [[:digit:]][[:digit:]]</kbd><em>你的模式一定要完整!我们没有任何仅为两位数字的路径名</em>
<span class="prompt">$ </span>
</pre>
<p>另外需要理解的是这些 glob 永远不会跳进子路径(subdirectory)中去。他们只会匹配当前路径下的文件名。如果我们想让某个 glob 去查看另一目录下的路径名,就需要明确告诉它这个路径名:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls ~/Downloads/*.txt</kbd><em>列举 <code>~/Downloads</code> 目录下所有以 <code>.txt</code> 结尾的路径名</em>
/Users/lhunath/Downloads/myscript.txt
/Users/lhunath/Downloads/hello.txt
<span class="prompt">$ </span><kbd>ls ~/*/hello.txt</kbd><em>Glob 甚至可以在许多路径下搜索!这里 bash 就会搜索</em>
/Users/lhunath/Documents/hello.txt<em>主目录下 <strong>所有的路径</strong>,寻找一个叫做 <code>hello.txt</code> 的文件</em>
/Users/lhunath/Downloads/hello.txt
</pre>
<p>路径名扩展是一种强有力的工具,使我们无需在参数中具体声明确切的路径名称,还可以搜遍文件系统查找我们需要的文件。</p>
<p>最后,bash 还内置对一些高级 glob 模式的支持。这些 glob 被称作:<dfn>扩展(extended) glob</dfn>。默认状态下,对他们的支持是被关闭的,但是我们很容易就可以通过命令开启他们:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>shopt -s extglob</kbd>
</pre>
<p>一旦开启扩展 glob,前面列表中的 glob 模式运算符就得到了下面这些新增运算符的补充:</p>
<table>
<tr>
<th>扩展 Glob</th>
<th>意义</th>
</tr>
<tr>
<th><code class="syntax"><strong>+(</strong><var>pattern</var>[ <strong>|</strong> <var>pattern</var> ... ]<strong>)</strong></code></th>
<td>列表内的任一种模式出现一次或多次,即为匹配。读作:<q>至少有一个 ...</q>。</td>
</tr>
<tr>
<th><code class="syntax"><strong>*(</strong><var>pattern</var>[ <strong>|</strong> <var>pattern</var> ... ]<strong>)</strong></code></th>
<td>列表内的任一种模式出现一次,或 <em>没出现</em>,或出现多次,都为匹配。读作:<q>无论多少次...</q> </td>
</tr>
<tr>
<th><code class="syntax"><strong>?(</strong><var>pattern</var>[ <strong>|</strong> <var>pattern</var> ... ]<strong>)</strong></code></th>
<td>列表内的任一种模式出现一次或没出现,即为匹配。读作:<q>或许有一个...</q></td>
</tr>
<tr>
<th><code class="syntax"><strong>@(</strong><var>pattern</var>[ <strong>|</strong> <var>pattern</var> ... ]<strong>)</strong></code></th>
<td>列表内的任一种模式出现仅一次,即为匹配。读作:<q>一个 ...</q></td>
</tr>
<tr>
<th><code class="syntax"><strong>!(</strong><var>pattern</var>[ <strong>|</strong> <var>pattern</var> ... ]<strong>)</strong></code></th>
<td>只有当列表内任一种模式都没有出现,才算匹配。读作:<q>没有一个 ...</q></td>
</tr>
</table>
<p>这些运算符乍看起来可能会让人有些困惑,但他们其实是给模式增加逻辑性的一种非常好的方式:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls <mark>+([:digit:])</mark>' '*.ogg</kbd><em>文件名以一位或多位数值起始</em>
<mark>05</mark> Between Angels and Insects.ogg
<mark>07</mark> Wake Up.ogg
<span class="prompt">$ </span><kbd>ls *.jp<mark>?(e)</mark>g</kbd><em>文件名以 <code>.jpg</code> 或 <code>.jpeg</code> 结束</em>
img_88751.jp<mark></mark>g
igpd_45qr.jp<mark>e</mark>g
<span class="prompt">$ </span><kbd>ls *.<mark>@(jpg|jpeg)</mark></kbd><em>效果与前面相同,但这样表达可能更清楚!</em>
img_88751.<mark>jpg</mark>
igpd_45qr.<mark>jpeg</mark>
<span class="prompt">$ </span><kbd>ls !(my*).txt</kbd><em>所有 <strong>不是</strong>以 <code>my</code> 起始的 <code>.txt</code> 文件 </em>
hello.txt
<span class="prompt">$ </span><kbd>ls !(my)*.txt</kbd><em>你能猜到为什么这个会匹配到 <code>myscript.txt</code> 文件吗?</em>
myscript.txt
hello.txt
</pre>
<p>扩展 glob 模式有时会极为有用,但同时也可能使人困惑或误解。让我们专注来看最后一个例子:为什么 <code>!(my)*.txt</code> 会扩展出路径名 <code>myscript.txt</code> ? 难道 <code>!(my)</code> 匹配的不该是 <em>不</em> 包括 <code>my</code> 的路径名吗?没错,的确是这样!但是,bash 确实又扩展出了一个以 <code>my</code> 起始的路径名!</p>
<p>这里的解释是 bash 会开心地将 <code>m</code> 起始(并非 <code>my</code> )、甚至是空格起始的文件名判断为与 blob 的这部分模式匹配。这就意味着,如果这个文件名仍要匹配扩展,那么路径名的剩余部分就要匹配 <em> 模式的余下部分 </em>。余下部分也的确匹配,因为 <code>!(my)</code> 模式之后紧跟的是 <code>*</code> glob, 如此就能匹配剩下全部的文件名。所以在这个例子中,<code>!(my)</code> 部分匹配文件名首字母 <code>m</code> 字符, <code>*</code> 匹配 <code>yscript</code> 部分,模式的后缀 <code>.txt</code> 匹配路径名的后缀 <code>.txt</code>。 因此,模式匹配文件名,所以文件名被扩展!如果我们将 <code>*</code> 放在 <code>!()</code> 模式的括号内,这个路径名将不再与之匹配:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>ls <mark class="green">!(my)</mark><mark class="blue">*</mark>.txt</kbd>
<mark class="green">m</mark><mark class="blue">yscript</mark>.txt
<mark class="green">hello</mark>.txt
<span class="prompt">$ </span><kbd>ls <mark>!(my*)</mark>.txt</kbd>
<mark>hello</mark>.txt
</pre>
<h2>波浪号扩展(Tilde Expansion)</h2>
<p>有一种不同类型的扩展我们已在这份指南里悄悄地使用,但尚未明确解释它。它就是 <dfn>波浪号扩展</dfn>,也就是用当前用户的主目录路径替换路径名中的波浪号( <code>~</code> ):</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>echo 'I live in: ' ~</kbd><em>注意扩展一定不能被引用,否则他们就会成为 <strong>字面性</strong> 的字符!</em>
I live in: /Users/lhunath
</pre>
<p>相比路径名扩展,波浪号扩展在 bash 中稍有些特殊,因为它在解析阶段很早期的时候发生。这只是一个很小的细节,但理解波浪号扩展与路径名扩展有所不同是很重要的。波浪号扩展中,我们并不会执行搜索,试图将文件名与 glob 模式作匹配,我们仅仅是用确切的路径名替换波浪号。</p>
<p>除了简单的波浪号,我们也可以通过将其他用户的用户名紧放在波浪号之后,来扩展他们的主目录:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>echo 'My boss lives in: ' ~root</kbd>
My boss lives in: /var/root
</pre>
<h2>命令替换(Command Substitution)</h2>
<p>现在我们对 <dfn>扩展</dfn> 的含义已有很好的认识:即使用具体情境相关的信息作为标识(token)的值来替换命令中的标识。到目前为止,无论是路径名扩展还是波浪号扩展,我们都只是扩展了路径名。</p>
<p>但扩展能做的事如此之多,几乎任何种类的数据都可以通过扩展传递至我们命令的参数中。<dfn>命令替换</dfn> 就是一种极为流行的将数据扩展至命令参数的方法。借助 <dfn>命令替换</dfn>,我们实际是在命令中写入命令,然后让 bash 把内层命令扩展成它的 <em>输出结果</em>,再把这个输出结果作为参数传给外层主命令:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>echo 'Hello world.' > hello.txt</kbd>
<span class="prompt">$ </span><kbd>cat hello.txt</kbd>
Hello world.
<span class="prompt">$ </span><kbd>echo "The file <hello.txt> contains: <mark>$(cat hello.txt)</mark>"</kbd>
The file <hello.txt> contains: <mark>Hello world.</mark>
</pre>
<p>我们刚做了什么?<br>
初始很简单:我们首先创建了一个叫做 <code>hello.txt</code> 的文件,写入字符串 <code>Hello world.</code>,然后使用 <code>cat</code> 命令将文档的内容输出显示。我们可以看到,文档包含刚写入的字符串。</p>
<p>之后有趣的事发生了:我们在此想做的是向用户输出一条消息,用一句话清楚解释文档中存有什么字符串。为了实现这一点,我们想让文档内容作为 <code>echo</code> 出的这句话的“一部分”。然而,当我们为要输出的这句话写代码的时候,并不知道文件里有什么内容,那如何在脚本中完整打出那句话呢?答案就是使用扩展:因为我们知道如何通过 <code>cat</code> 命令得到文档内容,将可以将 <code>cat</code> 命令的输出结果 <em>扩展至</em> 要 <code>echo</code> 的那句话中。Bash 首先会运行 <code>cat hello.txt</code>,拿到它的输出结果(即字符串 <code>Hello world.</code> ),然后把 <dfn>命令替换</dfn> 语法(即 <code>$(cat ...)</code> 部分)扩展至输出结果。只有经过这样的扩展,bash 才会尝试运行 <code>echo</code> 命令。你能猜到,在我们的 <dfn>命令替换</dfn> 扩展之后,echo 命令的参数变成了什么吗?答案就是:<br>
<code>echo "The file <hello.txt> contains: Hello world."</code></p>
<p>以上是我们学习到的第一种值的扩展(value expansion)。值的扩展允许我们讲数据扩展至命令的参数。他们非常有用,你之后会反复使用到。对于值的扩展,bash 有非常一致的语法:他们全都以符号 <code>$</code> 起始。</p>
<p><dfn>命令替换</dfn> 本质上是 <em>扩展 bash subshell 执行的某条命令的值</em> 。因此,它的语法是由值扩展前缀符号 <code>$</code> 加上待扩展的 subshell 的部分 <code>(...)</code> 构成。Subshell 本质上是一个小的新开启的 bash 进程,用来运行某条命令,而主 bash shell 会等待它的运行结果。在后面的章节中,我们会学习更多关于 subshell 的内容。当前只需要知道 bash 的扩展语法既一致又缜密,这对学习它当然非常有利!</p>
<aside class="rule">
<p>观察敏锐的读者应该已经注意到这份指南倾向于使用单引号来引用字符串,但是在最近的例子中则切换使用双引号来引用包含扩展语法的句子。这是有意为之的:所有的值扩展(即所有以 <code>$</code> 为前缀的语法)只有在 <em>双引号</em> 引用的参数中才会被扩展。单引号会将美元语法符号转成纯字面性的符号,使得 bash 输出美元符号而非扩展它的值!因此一定要使用双引号引用包含值扩展的参数。</p>
<p>
<q>值扩展(<code>$...</code>)必须 <strong>永远</strong> 使用双引号</q>
</p>
</aside>
<aside class="warn">
<p>永远不要未加引用使用值扩展。如果你这样做,bash 会以单词分割的方式将值的部分拆解,删除其中所有的空格,对其中全部单词执行隐藏的变量名扩展!</p>
</aside>
<p>作为结束语,我需简单提及已被废弃的反引号 <code>`...`</code>语法。旧式 bourne shell 使用这种语法表示 <dfn>命令替换</dfn>,而非更现代的 <code>$(...)</code> 语法。虽然 bash 以及所有当代 POSIX shell 同时支持这两种语法,但还是强烈建议你 <em>停止</em> 使用反引号(<code>`</code>)语法,而且每当你看到这种用法时,都请将他们转为值扩展的等价语法。虽然他们在功能上等价,但后引号语法有一些严重的缺陷:</p>
<ul>
<li>后引号语法看起来 <em>非常</em> 像引用。这已引起使用者普遍的困惑。甚至在训练有素的使用者看来,有时也很难不忘记后引号扩展 <em>仍需要双引号将其包裹以确保安全</em>,就像其他所有值扩展那样。</li>
<li>后引号语法破坏了值扩展语法的一致性。<code>$(...)</code> 语法的美就在于非常清楚地声明我们要这里扩展出一个值,就像其他所有美元符号格式的值扩展那样。这种清晰是后引号语法所缺少的。</li>
<li>后引号语法使引用与嵌套的使用显得荒谬。它需要迷宫般地使用转义反斜杠,如此一来基本不可能解析,而且相当确定你会犯错:<code>echo "`echo \"\`echo \\"hello\\"\`\"`"</code> vs. <code>echo "$(echo "$(echo "hello")")"</code></li>
</ul>
</section>
<section>
<h1>我如何储存和再利用数据?</h1>
<p>我们现在知道如何使用 bash 编写和管理简单命令,这些命令使我们直抵系统内许多强大的功能。我们也已学习命令如何使 bash 为程序新建进程从而运行他们。我们甚至还学习了操纵进程的基本输入与输出,从而读取或写入任意文件。</p>
<p>你们之中那些极为留心的人肯定也已注意到,我们如何通过像 here 文档与 here 字符串这样的建构,向进程中传输任意数据。</p>
<p>现在,我们面临的最大限制是不能灵活地操纵数据。没错,借助文件重定向,我们是可以把数据写入文档之后再读取出来,也可以通过 here 文档与 here 字符串传入静态的、事先定义好的数据。但是我们渴望更多。</p>
<p>是时候解锁下一水平的惊奇:bash 参数。</p>
<h2>什么是 bash 参数?</h2>
<p>简单说,bash 参数就是内存中的一些位置,你可以暂时用来储存一些之后会用到的信息。</p>
<p>和文档类似,我们将信息写入参数并在之后需要提取的时候从中读出。但是,因为我们读写信息使用的是系统内存而非硬盘,所以速度会更快。相比重定向文档的输入输出,使用参数容易很多,语法也更强大。</p>
<p>Bash 提供了几种不同类型的参数:位置参数(positional parameters),特殊参数(special parameters)和 shell 变量(shell variables)。最后一种是最有趣的类型,前两种则主要帮助我们访问 bash 提供的特定信息。我们接下来会通过变量介绍 bash 参数的使用与实践,然后再解释位置和特殊参数的不同之处。</p>
<h2>Shell 变量</h2>
<p>Shell 变量本质上是被命名的 bash 参数。你可以利用变量储存值,之后还可以修改或读取这个值以再次使用。</p>
<p>使用变量非常简单。你可以通过变量赋值将信息储存其中,之后任何时间都可以再通过参数扩展访问存储的信息:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>name=lhunath</kbd><em>将 <code>lhunath</code> 赋值给变量 <code>name</code></em>
<span class="prompt">$ </span><kbd>echo "Hello, $name. How are you?"</kbd><em>将变量 <code>name</code> 的值扩展到 echo 参数中</em>
Hello, lhunath. How are you?
</pre>
<p>如你所见,我们通过赋值创建了一个叫作 <kbd>name</kbd> 的变量,并存入一个值。之后,通过在变量名的前面加一个前缀符号 <kbd title="dollar">$</kbd>,我们又将变量值扩展置入到 echo 的参数中。</p>
<h3>赋值(Assignment)</h3>
<p>赋值使用等号 <kbd title="equals">=</kbd> 运算符。你必须要理解等号运算符的前后不能有任何语法空格。虽然其他语言可能允许这样,但 bash 不允许。还记得在前面章节中我们已强调过吗,空格在 bash 里有特殊的含义:他们会将命令分割成参数。如果我们在等号 <code>=</code> 前后放置空格,他们会误导 bash 将命令分割成命令名和参数,以为你想执行某个程序而不是赋值给变量:</p>
<pre lang="bash" class="bad">
<span class="prompt">$ </span><kbd>name <mark>=</mark> <mark>lhunath</mark></kbd><em>运行命令 <code>name</code>,参数是 <code>=</code> 和 <code>lhunath</code>.</em>
-bash: name: command not found
</pre>
<p>为了修改以上代码,只需移除 <code>=</code> 运算符前后导致单词分割的空格即可。如果我们赋给某变量的值的确以字面性的空格字符起始,就需要使用引用来告诉 bash 这些空格是字面性的,不要启动单词分割:</p>
<pre lang="bash" class="good">
<span class="prompt">$ </span><kbd>name=lhunath</kbd>
<span class="prompt">$ </span><kbd>item=' 4. Milk'</kbd><em>使用引用将空格转成字面性的字符</em>
</pre>
<p>我们甚至可以将赋值的语法与其他值的扩展结合使用:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>contents="$(cat hello.txt)"</kbd>
</pre>
<p>这里我们执行了一个 <dfn>命令替换</dfn>,将 <code>hello.txt</code> 文档中的内容扩展到我们的赋值语法中,进而将其赋值给 <var>contents</var> 变量</p>
<h3>参数扩展(Parameter Expansion)</h3>
<p>赋值给变量很棒但不是立即就有用。也正是这些变量值在之后可以被任意调用才使得参数如此有趣。复用参数值是通过扩展实现的。<dfn>参数扩展</dfn> 有效地将数据从你的参数中取出,并置入命令的数据中。正如我们在前面简单看到的,扩展参数是通过在他们的名称前使用前缀符号 <code>$</code>。每当你在 bash 中看到这个符号,什么东西很可能正在被扩展。可能是某个参数,或是某个命令的输出结果,又或是某个数学运算的结果。我们后面还会学习更多其他扩展。</p>
<aside class="rule">
<p>你或许已注意到一种趋势,但还是值得反复强调:<br>
<q>参数扩展(以及其他所有值的扩展)<strong>总是(always)</strong> 应该被双引号引用。</q></p>
</aside>
<p>此外,参数扩展允许你使用大括号(<kbd>{</kbd> 和 <kbd>}</kbd>)包裹扩展。这些大括号是用来告诉 bash 你参数名称的起始和终止。他们的使用通常是可选的,因为 bash 自己基本都可以识别出参数名称。不过有些时候使用他们是必要的:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>name=Britta time=23.73</kbd><em>我们想要扩展 <code>time</code> 并加上 <code>s</code> 表示秒</em>
<span class="prompt">$ </span><kbd>echo "$name's current record is $times."</kbd><em>但是 bash 将其错误地理解为名称 <code>times</code>,而这个变量是空的</em>
Britta's current record is .
<span class="prompt">$ </span><kbd>echo "$name's current record is ${time}s."</kbd><em>括号会明确告诉 bash 变量名称在哪里终止</em>
Britta's current record is 23.73s.
</pre>
<p>参数扩展对于在命令指令中插入用户或程序数据非常好用,但它手里其实还藏有另一张王牌:参数扩展运算符。扩展某个参数的时候,还可以对扩展出的值施加一个运算符,用来以某种方式改变当前这个扩展值。记住,这个运算符只会改变本次扩展时的值,并不会影响变量中原始存储的值。</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>name=Britta time=23.73</kbd>
<span class="prompt">$ </span><kbd>echo "$name's current record is ${time%.*} seconds and ${time#*.} hundredths."</kbd>
Britta's current record is 23 seconds and 73 hundredths.
<span class="prompt">$ </span><kbd>echo "PATH currently contains: ${PATH//:/, }"</kbd>
PATH currently contains: /Users/lhunath/.bin, /usr/local/bin, /usr/bin, /bin, /usr/libexec
</pre>
<p>在扩展结果之前,以上例子分别使用 <code>%</code>、<code>#</code> 和 <code>//</code> 三种运算符对参数值施加了多种操作。参数本身并没有改变,运算符只影响扩展到相应位置的值。你会注意到我们在这里也可以使用 glob 模式,就像在路径名扩展中那样,来匹配参数中的值。</p>
<p>在第一个例子中,扩展之前,我们使用 <code>%</code> 移除变量 <code>time</code> 值内的 <code title="dot">.</code> 以及它之后的数字。结果只剩下 <code title="dot">.</code> 以前的部分,也就是秒数。第二个例子类似,我们使用 <code>#</code> 移除 <code>time</code> 值中从开始到 <code title="dot">.</code>的部分。最后,我们使用 <code>//</code> 运算符( 运算符 <code>/</code> 的特殊形式),以逗号 <code>,</code> 替代 <code>PATH</code> 值中全部的冒号 <code>:</code>。结果就是一列逗号分隔的路径名,相比原来冒号分隔的 <code>PATH</code>,更方便人们阅读。
<table>
<thead>
<tr>
<th colspan="3"><kbd>url='https://guide.bash.academy/variables.html'</kbd></th>
</tr>
</thead>
<tr>
<th>运算符</th>
<th>示例</th>
<th>结果</th>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>#</strong><var>pattern</var><strong>}</strong></code>
</th>
<td><kbd>"${url#<mark>*/</mark>}"</kbd></td>
<td rowspan="2">
<pre><mark>https:/</mark>/guide.bash.academy/variables.html
↓
/guide.bash.academy/variables.html</pre>
</td>
</tr>
<tr>
<td colspan="2">删除从值的起始算起,符合 <var>模式(pattern)</var> 的 <em>最短</em> 字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>##</strong><var>pattern</var><strong>}</strong></code>
</th>
<td><kbd>"${url##<mark>*/</mark>}"</kbd></td>
<td rowspan="2">
<pre><mark>https://guide.bash.academy/</mark>variables.html
↓
variables.html</pre>
</td>
</tr>
<tr>
<td colspan="2">删除从值的起始算起,符合 <var>模式(pattern)</var> 的 <em>最长</em> 字符串</td></td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>%</strong><var>pattern</var><strong>}</strong></code>
</th>
<td><kbd>"${url%<mark>/*</mark>}"</kbd></td>
<td>
<pre>https://guide.bash.academy<mark>/variables.html</mark>
↓
https://guide.bash.academy</pre>
</td>
</tr>
<tr>
<td colspan="2">删除到值的终止为止,符合 <var>模式(pattern)</var> 的 <em>最短</em> 字符串</td></td></td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>%%</strong><var>pattern</var><strong>}</strong></code>
</th>
<td><kbd>"${url%%<mark>/*</mark>}"</kbd></td>
<td>
<pre>https:<mark>//guide.bash.academy/variables.html</mark>
↓
https:</pre>
</td>
</tr>
<tr>
<td colspan="2">删除到值的终止为止,符合 <var>模式(pattern)</var> 的 <em>最长</em> 字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>/</strong><var>pattern</var><strong>/</strong><var>replacement</var><strong>}</strong></code>
</th>
<td><kbd>"${url/<mark>.</mark>/<mark>-</mark>}"</kbd></td>
<td>
<pre>https://guide<mark>.</mark>bash.academy/variables.html
↓
https://guide-bash.academy/variables.html</pre>
</td>
</tr>
<tr>
<td colspan="2">用替换值替换 <em></em> 符合 <var>模式(pattern)</var> 的第一个字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>//</strong><var>pattern</var><strong>/</strong><var>replacement</var><strong>}</strong></code>
</th>
<td><kbd>"${url//<mark>.</mark>/<mark>-</mark>}"</kbd></td>
<td>
<pre>https://guide<mark>.</mark>bash<mark>.</mark>academy/variables<mark>.</mark>html
↓
https://guide-bash-academy/variables-html</pre>
</td>
</tr>
<tr>
<td colspan="2">用替换值替换 <em></em> 符合 <var>模式(pattern)的</var> 每一个字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>/#</strong><var>pattern</var><strong>/</strong><var>replacement</var><strong>}</strong></code>
</th>
<td><kbd>"${url/#<mark>*:</mark>/<mark>http:</mark>}"</kbd></td>
<td>
<pre><mark>https:</mark>//guide.bash.academy/variables.html
↓
http://guide.bash.academy/variables.html</pre>
</td>
</tr>
<tr>
<td colspan="2">用替换值替换 <em>从值的起始算起</em> 符合 <var>模式(pattern)的</var> 字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>/%</strong><var>pattern</var><strong>/</strong><var>replacement</var><strong>}</strong></code>
</th>
<td><kbd>"${url/%<mark>.html</mark>/<mark>.jpg</mark>}"</kbd></td>
<td>
<pre>https://guide.bash.academy/variables<mark>.html</mark>
↓
https://guide.bash.academy/variables.jpg</pre>
</td>
</tr>
<tr>
<td colspan="2">用替换值替换 <em>到值的终止为止</em> 符合 <var>模式(pattern)的</var> 字符串</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${#</strong><var>parameter</var><strong>}</strong></code>
</th>
<td><kbd>"${#url}"</kbd></td>
<td>
<pre>https://guide.bash.academy/variables.html
↓
40</pre>
</td>
</tr>
<tr>
<td colspan="2">扩展值的长度(字节数)</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var><strong>:</strong><var>start</var>[<strong>:</strong><var>length</var>]<strong>}</strong></code>
</th>
<td><kbd>"${url:<mark>7</mark>}"</kbd></td>
<td>
<pre>https://<mark>guide.bash.academy/variables.html</mark>
↓
guide.bash.academy/variables.html</pre>
</td>
</tr>
<tr>
<td colspan="2">从 <var>start</var> 开始,<var>length</var> 字节长,扩展出值的一部分。你甚至可以通过使用负数,从值的末尾 <var>start</var> 算起</td>
</tr>
<tr>
<th>
<code class="syntax"><strong>${</strong><var>parameter</var>[<strong>^</strong>|<strong>^^</strong>|<strong>,</strong>|<strong>,,</strong>][<var>pattern</var>]<strong>}</strong></code>
</th>
<td><kbd>"${url^^<mark>[ht]</mark>}"</kbd></td>
<td>
<pre><mark>htt</mark>p://guide.bas<mark>h</mark>.academy/variables.<mark>ht</mark>ml
↓
HTTps://guide.basH.academy/variables.HTml</pre>
</td>
</tr>
<tr>
<td colspan="2">扩展转换后的值,将符合 <var>模式(pattern)的</var> 的第一个或全部字符转换成小写或大写形式。你也可以忽略模式转换全部字符</td>
</tr>
</table>
<p> </p>
<footer>
Shell 变量是你可以自由赋值的参数。赋值通过语法 <code>var=value</code> 实现。参数可以通过扩展将数据置入命令的参数。参数扩展通过在变量名称前使用前缀 <code>$</code> 符号实现。有时你会需要在变量名称的周围加上大括号 <code>{</code> 和 <code>}</code>,来明确告知 bash 参数名称从哪里起始到哪里终止 (例如:<code>"${time}s"</code>)。<br>
为保持一致性,防止因空格的存在而导致单词分割进而触发意料之外的路径名补全,参数扩展应该 <strong>总是被双引号引用</strong>。扩展参数的时候,你还可以使用一种特殊的参数扩展运算符,以需要的方式改变被扩展的值。
</footer>
<h2 id="expansion_ex">练习时间!</h2>
<h4>EXPAN.1. 将 <kbd>hello</kbd> 作为值赋给变量 <var>greeting</var></h4>
<pre lang="bash" class="exercise"><samp><kbd>greeting=hello</kbd></samp></pre>
<h4>EXPAN.2. 显示变量 <var>greeting</var> 的值</h4>
<pre lang="bash" class="exercise"><samp><kbd>echo "$greeting"</kbd>
hello</samp></pre>
<h4>EXPAN.3. 将字符串 <kbd> world</kbd> 赋在变量当前内容的后面</h4>
<pre lang="bash" class="exercise"><samp><kbd>greeting="$greeting world"</kbd></samp>
<samp><kbd>greeting+=" world"</kbd><em><code>+=</code> 将字符串附在当前值的末尾</em></samp></pre>
<h4>EXPAN.4. 显示变量 <var>greeting</var> 中最后一个单词</h4>
<pre lang="bash" class="exercise"><samp><kbd>echo "${greeting##* }"</kbd>
world</samp></pre>
<h4>EXPAN.5. 显示变量 <var>greeting</var> 的内容,第一个字母大写,以 (<code>.</code>) 结束</h4>
<pre lang="bash" class="exercise"><samp><kbd>echo "${greeting^}."</kbd>
Hello world.</samp></pre>
<h4>EXPAN.6. 用 <var> big </var> 替换变量内容中第一个空格符号</h4>
<pre lang="bash" class="exercise"><samp><kbd>greeting=${greeting/ / big }</kbd></samp></pre>
<h4>EXPAN.7. 将变量 <var>greeting</var> 的值重定向输出到一个文档中,用下划线 (<code>_</code>) 替换变量值中的全部空格并在最后加上 <code>.txt</code>,以此作为文档的名称</h4>
<pre lang="bash" class="exercise"><samp><kbd>echo "$greeting" > "${greeting// /_}.txt"</kbd></samp></pre>
<h4>EXPAN.8. 显示变量 <var>greeting</var> 的值,中间单词的全部字母大写</h4>
<pre lang="bash" class="exercise"><samp><kbd>middle=${greeting% *} middle=${middle#* }; echo "${greeting%% *} ${middle^^} ${greeting##* }"</kbd>
hello BIG world</samp></pre>
</section>
<section>
<h1>什么是环境,它是用来做什么的?</h1>
<p>变量可以保存在两种不同的空间内。这两种空间经常被混淆,进而导致许多误解。你已经熟悉了其中的一种:shell 变量。除此之外,变量还可以保存在进程环境中。接下来我们要介绍的就是环境变量,并会向你解释他们与 shell 变量的差别。</p>
<h2>环境变量(Environment Variables)</h2>
<p>不同于 shell 变量,环境变量存在于进程水平。这就意味着他们不是 bash shell 本身的特性,而是你系统内任一种程序进程的特性。如果我们将进程想象为你购买的一块土地,那么土地之上的建筑物就是运行在你进程中的代码。你可以在这块土地上盖一座 <code>bash</code> 房子,或是一座 <code>grep</code> 棚屋,又或是一座 <code>firefox</code> 高塔。环境变量是储存在你进程土地上的变量,而 shell 变量保存在你土地之上 bash 房子的内部。<br />
你可以将变量保存在环境中,也可以将他们存在 bash shell 内。环境是每一个进程共有的,而 shell 空间则只对 shell 进程开放。因此规则是这样的:<em>你应该把你的变量存放在 shell 空间中</em>,除非你明确需要环境变量的行为。</p>
<pre>
╭─── bash ─────────────────────────╮
│ ╭──────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ │ shell_var1=value │ │
│ │ shell_var2=value │ │
│ ╰──────────────────╯ │
│ ENV_VAR1=value │
│ ENV_VAR2=value │
╰──────────────────────────────────╯
</pre>
<p>当你在 shell 中运行一个新程序,bash 会为它创建新的进程,然后这个进程就会拥有它自己的环境。但是不同于 shell 进程,一般的进程没有 shell 变量,他们只有环境变量。更重要的是,当一个新的进程被创建,它的环境是通过 <strong>复制</strong> 创建它的进程的环境得到的:</p>
<pre>
╭─── bash ───────────────────────╮
│ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ │ greeting=hello │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
╰─┬──────────────────────────────╯
╎ ╭─── ls ─────────────────────────╮
└╌╌┥ │
│ ENVIRONMENT │
│ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
╰────────────────────────────────╯
</pre>
<p>有一种普遍存在的误解,即环境是所有进程共享的一个系统-全局性的变量池,而这种错觉通常是因为在子进程中看到了相同的变量。如果你在 bash shell 中创建一个自定义的环境变量,在此之后你新建的任何子进程都会继承到这个变量,因为它把这个变量一起从 shell 的环境变量中复制了过来。但是,因为环境是针对具体每个进程而言的,子进程改变或创建的变量不会对父进程的同名环境变量产生任何影响:</p>
<pre>
╭─── bash ───────────────────────╮
│ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ │ greeting=hello │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ NAME=<mark>Bob</mark> │
╰─┬──────────────────────────────╯
╎ ╭─── bash ───────────────────────╮
└╌╌┥ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ NAME=<mark>Bob</mark> │
╰────────────────────────────────╯
<span class="prompt">$ </span><kbd>NAME=John</kbd>
╭─── bash ───────────────────────╮
│ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ │ greeting=hello │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ NAME=<mark>Bob</mark> │
╰─┬──────────────────────────────╯
╎ ╭─── bash ───────────────────────╮
└╌╌┥ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ NAME=<mark>John</mark> │
╰────────────────────────────────╯
</pre>
<p>为什么人们会选择将特定变量存放在环境中,以上所示的区别也使得这个问题的答案更清晰了。虽然你绝大多数的变量都会是普通的 shell 变量,但还是有可能选择性输出一些 shell 变量到 shell 进程环境中。这样你就可以有效地把变量值输出到所创建的每一个子进程中,这些子进程进而又会把他们的环境变量再输出至更下一层的子进程中。你的系统会使用环境变量做各种事,其中主要是提供状态信息(state information)以及特定进程的默认配置。</p>
<p>例如,通常用来帮一个用户登陆进系统的 <code>login</code> 程序,会把用户信息输出至环境中( <var>USER</var> 包含你的用户名,<var>HOME</var> 包含你的主目录,<var>PATH</var> 包含标准命令搜索路径,等等)。在你登陆之后运行的所有进程,现在通过查看环境变量就会知道他们是在为哪个用户运行。</p>
<p>你也可以把自己创建的变量输出到环境中,这通常是用来配置你所运行的程序的行为。例如,你可以输出 <var>LANG</var> 并为它赋值,告诉程序他们应该使用什么语言与字符集。环境变量通常只对了解他们且明确支持他们的程序有用。有些环境变量的使用范围非常有限,例如 <var>LSCOLORS</var> 可以被 <code>ls</code> 等程序用来彩色输出你系统内的文档内容。</p>
<pre>
╭─── bash ───────────────────────╮
│ ╭────────────────╮ │
│ ENVIRONMENT │ SHELL │ │
│ │ greeting=hello │ │
│ ╰────────────────╯ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ LANG=en_CA │
│ PAGER=less │
│ LESS=-i -R │
╰─┬──────────────────────────────╯
╎ ╭─── rm ─────────────────────────╮<em><code>rm</code> 只使用 <var>LANG</var> </em>
├╌╌┥ │<em>决定错误消息的语言</em>
╎ │ ENVIRONMENT │
╎ │ │
╎ │ HOME=/home/lhunath │
╎ │ PATH=/bin:/usr/bin │
╎ │ <mark>LANG=en_CA</mark> │
╎ │ PAGER=less │
╎ │ LESS=-i -R │
╎ ╰────────────────────────────────╯
╎ ╭─── man ────────────────────────╮<em>除了 <var>LANG</var>, <code>man</code> 还使用 <var>PAGER</var> 决定</em>
└╌╌┥ │<em>使用什么程序将长指南分页</em>
│ ENVIRONMENT │
│ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ <mark>LANG=en_CA</mark> │
│ <mark>PAGER=less</mark> │
│ LESS=-i -R │
╰─┬──────────────────────────────╯
╎ ╭─── less ───────────────────────╮<em><code>less</code> 使用 <var>LESS</var> 变量</em>
└╌╌┥ │<em>为它自己提供一个初始配置</em>
│ ENVIRONMENT │
│ │
│ HOME=/home/lhunath │
│ PATH=/bin:/usr/bin │
│ <mark>LANG=en_CA</mark> │
│ PAGER=less │
│ <mark>LESS=-i -R</mark> │
╰────────────────────────────────╯
</pre>
<h2>Shell 初始化(Shell Initialization)</h2>
<p>当你启动一个交互式 bash 时,通过读取你系统内不同文档中存储的初始化命令,bash 会自行准备好。你可以使用这些文档来告诉 bash 要如何行动。其中有一个文档就是特别用来使你输出变量到环境中的,这个文件叫做 <code>.bash_profile</code>,它存在于你的主目录中。很有可能你现在还没有这个文档,如果真是这样的话,你可以自己创建一个,下一次 bash 就会找到它。</p>
<p>在你的 <code>~/.bash_profile</code> 文档末尾,应该有一条这个命令 <code>source ~/.bashrc</code>。这是因为当 <code>~/.bash_profile</code> 文档存在的时候,bash 的表现会有些奇怪,它会停止寻找标准的 shell 初始化配置 <code>~/.bashrc</code> 文档,<code>source</code> 命令则会修复这点反常。</p>
<p>注意,如果系统中没有 <code>~/.bash_profile</code> 文档,bash 就会尝试从 <code>~/.profile</code> 文档中读取信息,如果这个文档存在的话。这后一种文档是一种通用性的 shell 配置文件,其他 shell 也可以读取。你也可以选择将环境配置信息储存在这里,但是要注意在这个文档中,需使用POSIX sh 语法,而不能是任何具体的 bash shell 语法。POSIX sh 语法和 bash 语法非常相似,但这部分内容已超出本指南的讲解范围。</p>
<pre>
login<em><code>login</code> 程序使用户登陆至系统内</em>
│
╰─ <strong>-bash</strong><em><code>login</code> 命令启动用户登陆过的shell</em>
│
╰─ screen<em>用户从登陆后的 shell 中运行 <code>screen</code> 程序</em>
│
╰─ weechat<em><code>screen</code> 程序会创建多个窗口</em>
│ <em>且允许用户在窗口之间切换</em>
╰─ bash <em>第一个运行一个 IRC 客户端,另外两个则分别运行</em>
│ <em>未登录的 bash shell</em>
╰─ bash
</pre>
<p>这个进程树描绘了一个用户,使用 bash 作为登陆的 shell,同时复用终端创建了几个独立的“屏幕(screen)”,使他可以同时与多个正在运行的程序交互。登陆后,系统( <code>login</code> 程序)会决定用户的登陆 shell。例如,它可能会通过查看 <code>/etc/passwd</code> 确定。在这个例子中,用户登陆的 shell 被设置为 bash。<code>login</code> 接下来会运行 bash 并将它的名字设置为 <code>-bash</code>。这是 <code>login</code> 命令的标准执行流程,它会在登陆的 shell 名称前加一个前缀 <code>-</code>,表示这个 shell 会作为登陆 shell 运行。</p>
<p>当用户有了一个正在运行且登陆过的 bash shell,接着他启动了 <code>screen</code> 程序。当这个程序运行时,它会接管用户的整个终端并在其中模拟出多个终端,允许用户在他们之间切换。在每一个模拟终端中,screen 程序都会运行一个新的程序。在这个例子中,用户使用 screen 程序配置了一个模拟终端运行 IRC 客户端,配置另外两个终端运行交互式(但是非登陆)bash shell。下方视频演示的是具体的操作过程:</p>
<script type="text/javascript" src="https://asciinema.org/a/13948.js" id="asciicast-13948" async></script>
<p>让我们来看一下这个例子中的初始化过程具体是怎样的,以及环境变量从哪里来:</p>
<pre>
login
│ <ins>TERM=dumb</ins>
│ <ins>USER=lhunath</ins>
│ <ins>HOME=/home/lhunath</ins>
│ <ins>PATH=/usr/bin:/bin</ins>
│
╰─ -bash
│ TERM=dumb
│ USER=lhunath
│ HOME=/home/lhunath
│ <del>PATH=/usr/bin:/bin</del>
│ <ins>PWD=/home/lhunath</ins>
│ <ins>SHLVL=1</ins>
│╭──────────────╮ ╭────────────────────────╮╭──────────────────╮
┝┥ login shell? ┝─yes─┥ source ~/.bash_profile ┝┥ source ~/.bashrc │
│╰──────────────╯ ╰────────────────────────╯╰──────────────────╯
│ <ins>PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/libexec</ins>
│ <ins>EDITOR=vim</ins>
│ <ins>LANG=en_CA.UTF-8</ins>
│ <ins>LESS=-i -M -R -W -S</ins>
│ <ins>GREP_COLOR=31</ins>
│
╰─ screen
│ <del>TERM=dumb</del>
│ <ins>TERM=screen-bce</ins>
│ USER=lhunath
│ HOME=/home/lhunath
│ PATH=/usr/bin:/bin
│ PWD=/home/lhunath
│ SHLVL=1
│ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/libexec
│ EDITOR=vim
│ LANG=en_CA.UTF-8
│ LESS=-i -M -R -W -S
│ GREP_COLOR=31
│ <ins>WINDOW=0</ins>
│
╰─ weechat
│
╰─ bash
│ │╭──────────────╮
│ ╰┥ login shell? ┝
│ ╰──────┰───────╯
│ no
│ ╭──────┸───────╮ ╭──────────────────╮
│ │ interactive? ┝─yes─┥ source ~/.bashrc │
│ ╰──────────────╯ ╰──────────────────╯
╰─ bash
│╭──────────────╮
╰┥ login shell? ┝
╰──────┰───────╯
no
╭──────┸───────╮ ╭──────────────────╮
│ interactive? ┝─yes─┥ source ~/.bashrc │
╰──────────────╯ ╰──────────────────╯
</pre>
<p>如你所见,不同水平会输出各自的变量至环境中。每一个子进程会继承父进程的环境变量。反过来,子进程也可以覆盖一些变量的值或添加新的变量。</p>
<p>注意看第一个(登陆)bash 会同时读取并执行(点命令,source)<code>~/.bash_profile</code> 和 <code>~/.bashrc</code>,而下面的两个 bash 则只读取和执行 <code>~/.bashrc</code>。之所以会这样是因为只有第一个 bash 进程是作为“登陆 shell”的(它的名称前面有一个前缀 <code>-</code>)。下面两个 bash 进程只是普通的交互式 shell。他们之所以不需要读取并执行 <code>~/.bash_profile</code> 的原因现在看来就更为明显了:<code>~/.bash_profile</code> 的职责在于配置 bash 的环境,而下面两个 shell 已从登陆的 shell 那里继承了它的环境。</p>
</section>
<section>
<h1>我还可以使用参数做什么?</h1>
<p>正如我们在前面部分提到的,参数有位置参数、特殊参数和变量三种类型。变量本质上就是有名字的参数。现在我们将会进一步看看不同类型的参数,以及他们如何允许你从 shell 中获得具体信息、或是如何改变 shell 的特定行为。</p>
<h2>位置参数(Positional Parameters)</h2>
<p>如果说变量是有名字的参数,位置参数就是有数值的参数(更具体地说,正整数)。我们使用标准的参数扩展语法扩展这些参数:<code>$1</code>,<code>$3</code>。一定要注意,如果数字位数超过一位,bash 会要求你使用大括号将位置参数包裹:<code>${10}</code>,<code>${22}</code> (实际操作中,你基本不会遇到需要明确指定如此高位置参数的情况)。</p>
<p>位置参数会扩展出值,然后作为参数发送至父进程创建的子进程中。例如,当你使用如下命令启动 <code>grep</code> 进程:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>grep Name registrations.txt</kbd>
</pre>
<p>你实际上是在运行 <code>grep</code> 命令,它的参数是 <code>Name</code> 和 <code>registrations.txt</code>。如果 <code>grep</code> 是一个 bash 脚本,通过扩展位置参数 <code>$1</code> 和 <code>$2</code>就能分别获得脚本所需的第一个和第二个参数。比 <code>2</code> 更高的位置参数未定义。</p>
<p>另外你最好知道还有一个第 0 位参数,这个位置参数会扩展为进程的 <em>名称</em>。进程的名称由创建它的程序选择,因此第 0 位的参数可以包含任何信息,而且完全由脚本的父进程决定。绝大多数 shell 会使用启动进程的执行文件的绝对路径作为进程名称,或是用户执行的命令名。注意这并非必须,而且你不能据此对第 0 位参数的内容做出任何可靠的推测:因此无论出于任何意图或目的最好都避免猜测。</p>
<p>好在且极为方便的是:关于变量参数我们目前所学的绝大多数内容也同样适用于位置参数:我们可以扩展他们,也可以应用参数扩展运算符来改变结果值:</p>
<pre lang="bash">
<kbd>#!/usr/bin/env bash
echo "The Name Script"
echo "usage: names 'My Full Name'"; echo
first=${1%% *} last=${1##* } middle=${1#$first} middle=${middle%$last}
echo "Your first name is: $first"
echo "Your last name is: $last"
echo "Your middle names are: $middle"</kbd>
</pre>
<p>如果你把这个脚本保存在以 <code>names</code> 命名的文档中,然后按照用法传递一个参数给它并运行,你将会看到这个脚本分析你的名字,并告诉你其中哪些部分分别是名、姓和中间名。过程中,我们使用变量<var>first</var>,<var>last</var> 和 <var>middle</var> 储存这些信息片段,后面在 <code>echo</code> 语句中再将他们扩展使用。注意 <var>中间名</var> 的计算同时需要名字全称(可从第一位置参数获得)以及名字(已被计算并保存在变量 <var>first</var> 中)信息。</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>chmod +x names</kbd>
<span class="prompt">$ </span><kbd>./names 'Maarten Billemont'</kbd>
The Name Script
usage: names 'My Full Name'
Your first name is: Maarten
Your last name is: Billemont
Your middle names are:
<span class="prompt">$ </span><kbd>./names 'James Tiberius "Jim" Kirk'</kbd>
The Name Script
usage: names 'My Full Name'
Your first name is: James
Your last name is: Kirk
Your middle names are: Tiberius "Jim"
</pre>
<p>一定要理解,和绝大数变量不同,位置参数是只读参数。仔细想想,你很可能也会认同,我们是无法从脚本内部改变传递给脚本的参数的。因此下面就是一个语法错误:</p>
<pre lang="bash" class="bad">
<span class="prompt">$ </span><kbd>1='New First Argument'</kbd>
-bash: 1=New First Argument: command not found
</pre>
<p>虽然例子中返回的错误消息看起来略显混乱,它表示 bash 甚至都没有识别出这个语句是想要赋值给变量(因为参数 <code>1</code> 不是一个变量),相反 bash 以为你给了它一个想要运行的命令。</p>
<p>然而,我们可以使用一种内置命令去改变位置参数集合的值。虽然在那些缺少 bash 高级特性的古老 shell 中,这是很常用的一种操作,但是你之后在 bash 中基本不会用到。为了改变当前的位置参数集合,使用 <code>set</code> 命令并在 <code>--</code> 参数的后面指定新的位置参数:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>set -- 'New First Argument' Second Third 'Fourth Argument'</kbd>
<span class="prompt">$ </span><kbd>echo "1: $1, 2: $2, 4: $4"</kbd>
1: New First Argument, 2: Second, 4: Fourth Argument
</pre>
<p>除了改变位置参数集合,内置命令 <code>shift</code> 可以用来“推动”位置参数。当我们移动位置参数时,实质上是将他们向开始的方向推动,移走前面的位置参数,为后面的腾出位置来:</p>
<pre lang="bash">
<mark>New First Argument</mark> <mark>Second</mark> <mark>Third</mark> <mark>Fourth Argument</mark>
<span class="prompt">$ </span><kbd>shift 2</kbd><em>将位置参数移动 2 </em>
<mark>Third</mark> <mark>Fourth Argument</mark> <----<em>前面两个位置参数消失,原来第三个现处于第一个的位置,第四个现在第二个</em>
</pre>
<p>最后,当我们使用 <code>bash</code> 命令启动一个新的 bash shell 时,有一种方法可用来传递位置参数。这是一种非常有用的方法,可以将一列参数传至内联 bash 脚本中。之后,当你把内联 bash 代码与其他功用结合使用的时候,你就会用到这种方法,但现在我们只把它作为一种实验位置参数、而无需创建独立脚本去激活和传递参数(如我们在上面 <code>names</code> 例子中所做的那样)的方法。。下面示例的就是如何运行一个内联 bash 命令,并传递一个参数列表扩展位置参数:</p>
<pre lang="bash">
<span class="prompt">$ </span><kbd>bash -c 'echo "1: $1, 2: $2, 4: $4"' -- 'New First Argument' Second Third 'Fourth Argument'</kbd>
1: New First Argument, 2: Second, 4: Fourth Argument
</pre>
<p>我们运行 <code>bash</code> 命令,传递 <code>-c</code> 选项,后面跟着一个参数,其内含有一些 bash shell 代码。这样会告诉 bash,相比启动一个新的交互式 bash shell,你只是想让 shell 运行以上 bash 代码并结束。在 shell 代码之后,我们指明了用以扩展位置参数的参数。第一个参数是 <code>--</code>。虽然这个参数技术上是用来扩展第 0 位位置参数,但是为了兼容性,以及区分bash 参数和 shell 代码参数,最好总是使用 <code>--</code>。在这个参数之后,每一个参数像你预期的那样填充标准位置参数。</p>
<aside class="rule">
<p>注意,包含 bash 代码的参数是被 <strong><code>'单引号引用'</code></strong> 的:</p>
<q>当我们把 <em>代码</em> 放在 <em>字符串</em> 中的时候(例如将他们作为参数传递时),代码 <em>应该</em> 被单引号引用。</q>
<p>不要使用 <code>"双引号"</code> 包裹代码字符串。这很重要,因为在确保将引用数据字面化上,单引号远比双引号可靠。 </p>
</aside>
<p>如果在上面例子中我们使用双引号,接收我们 <code>bash</code> 命令的shell 会将位置参数<code>$1</code>、<code>$2</code> 以及 <code>$4</code>分别扩展,由此 <code>-c</code> 选项的参数就破裂掉了。</p>
<p>为了示例这一点,请对比我们上面完整正确的例子:</p>
<pre lang="bash" class="good">
<span class="prompt">$ </span><kbd>bash -vc <mark>'echo "1: $1, 2: $2, 4: $4"'</mark> -- \</kbd><em>我们将 -v 参数传递给 bash 好让它在显示结果之前先呈现要运行的代码</em>
<kbd>'New First Argument' Second Third 'Fourth Argument'</kbd><em>我们可以在行末使用反斜杠 \ 以令起一行继续</em>
echo "1: $1, 2: $2, 4: $4"<em>这是即将运行的代码</em>
1: New First Argument, 2: Second, 4: Fourth Argument<em>这是结果</em>
</pre>
<p>以及如果我们使用双引号而非单引号引用 <code>-c</code> 的参数,会发生什么:</p>
<pre lang="bash" class="bad">
<span class="prompt">$ </span><kbd>bash -vc <mark>"echo "1:</mark> <mark>$1,</mark> <mark>2:</mark> <mark>$2,</mark> <mark>4:</mark> <mark>$4""</mark> -- \</kbd><em>外部的双引号与内部的双引号冲突,导致混淆</em>
<kbd>'New First Argument' Second Third 'Fourth Argument'</kbd>
echo 1:<em>结果就是,-c 选项的参数不再是完整的 bash 代码,而只包含第一个单词</em>
1:
<span class="prompt">$ </span><kbd>bash -vc <mark>"echo \"1: $1, 2: $2, 4: $4\""</mark> -- \</kbd><em>就算我们修复了引用的模糊性,问题仍在,位置参数 $1、$2 和 $4 正被我们输入命令的 shell 解释</em>
<kbd>'New First Argument' Second Third 'Fourth Argument'</kbd><em>而不是参数被传入的 shell </em>
echo "1: , 2: , 4: "<em>因为位置参数 $1、$2、$4 在你交互式的 shell 中很可能为空,扩展的结果也就相应为空,因此 -c 就没有了参数</em>
1: , 2: , 4:
</pre>
<p>通过使用反斜杠转义所有特殊字符,包括双引号和美元符号,我们可以修复掉双引号内的全部问题。但是这会使 shell 代码看起来极为复杂且难读。维护如此被特殊转义后的 shell 代码简直就像噩梦,随时可能导致极难发现的错误:</p>
<pre lang="bash" class="good">
<span class="prompt">$ </span><kbd>bash -vc "echo \"1: \$1, 2: \$2, 4: \$4\"" -- \</kbd>
<kbd>'New First Argument' Second Third 'Fourth Argument'</kbd>
echo "1: $1, 2: $2, 4: $4"
1: New First Argument, 2: Second, 4: Fourth Argument
</pre>
<h2>特殊参数(Special Parameters)</h2>
<p>理解位置参数使得理解特殊参数容易很多:因为他们非常相似。以单独符号字符作为参数名的参数就是特殊参数,他们通常被用来向 bash shell 请求特定状态信息。下面是不同种类的特殊参数以及他们含有的信息:</p>
<table>
<tr>
<th>参数</th>
<th>示例</th>
<th>描述</th>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>*</var><strong>"</strong></code></th>
<td><kbd>echo "Arguments: $<mark>*</mark>"</kbd></td>
<td>
扩展 <strong><em>单个字符串</em></strong>,将所有位置参数连接合并成一个,相互之间被 <var>IFS</var> 中的第一个字符分隔(通常默认的是一个空格)<br>
<strong>注:</strong> 永远不要使用这个参数,除非你明确希望连接所用参数。你基本上总是可以使用 <code>@</code> 替代。
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>@</var><strong>"</strong></code></th>
<td><kbd>rm "$<mark>@</mark>"</kbd></td>
<td>
将位置参数作为一列参数分别扩展
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>#</var><strong>"</strong></code></th>
<td><kbd>echo "Count: $<mark>#</mark>"</kbd></td>
<td>
扩展出一个数值,对应可用位置参数的数量
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>?</var><strong>"</strong></code></th>
<td><kbd>(( $? == 0 )) || echo "Error: $<mark>?</mark>"</kbd></td>
<td>
扩展上一个(同步)命令的退出代码<br>
退出代码 0 表示命令执行成功,其他数字分别对应执行失败的原因
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>-</var><strong>"</strong></code></th>
<td><kbd>[[ $<mark>-</mark> = *i* ]]</kbd></td>
<td>
扩展出当前 shell 中活跃的选项标识集<br>
选项标识(option flags)会配置 shell 的行为,例子是在测试 <code>i</code> 标识是否存在,i 表示 shell 处于交互模式(有提示符),并非在运行脚本
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>$</var><strong>"</strong></code></th>
<td><kbd>echo "$<mark>$</mark>" > /var/run/myscript.pid</kbd></td>
<td>
扩展一个数值,对应正在解析代码的 shell 进程的唯一识别码
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>!</var><strong>"</strong></code></th>
<td><kbd>kill "$<mark>!</mark>"</kbd></td>
<td>
扩展一个数值,对应上一个后台异步运行的进程的唯一识别码<br>
例子表示终止后台进程
</td>
</tr>
<tr>
<th><code class="syntax"><strong>"$</strong><var>_</var><strong>"</strong></code></th>
<td><kbd>mkdir -p ~/workspace/projects/myscripts && cd "$<mark>_</mark>"</kbd></td>
<td>
扩展上一个命令的最后一个参数
</td>
</tr>
</table>
<p>正如位置参数,特殊参数也是只读性的:你只能使用他们扩展信息,而非储存信息。</p>
<h2>Shell 内部变量(Shell Internal Variables)</h2>
<p>你已知道 shell 变量是什么。你是否知道 bash shell 也为你创建了一些变量?这些变量被用在各种任务中,对于从 shell 中查询状态信息或是改变 shell 行为非常方便。</p>
<aside class="rule">
<p>Shell 内部变量就是名称全部大写的 Shell 变量。环境变量基本也都是这样。当我们准备新建自己的 shell 变量时,一定要确保没有误用了内部已有变量的名称,否则会导致各种危险难预期的行为。幸好,shell 变量的名称是区分字母大小写的,因此为了防止不小心覆盖 shell 内部变量或系统输出的同名变量,一般的规则是:</p>
<p><q>你自己创建的所有 shell 变量名称应该全部使用 <em>小写字母</em>。如果你创建了一个环境变量,则以全部 <em>大写字母</em> 为它命名。</q></p>
</aside>
<p>虽然 bash 实际定义了许多 shell 内部变量,但绝大多数都不是非常有用。有些即使有用也只是在一些具体的情境中,其中许多变量的使用需要你理解更多高级的 bash 概念。下面我将简单介绍一些当前阶段学起来比较有趣的 shell 内部变量。使用 <code>man bash</code> 可以得到完整的 shell 内部变量列表。</p>
<table>
<tr>
<th><var>BASH</var></th>
<td><code>/usr/local/bin/bash</code></td>
</tr>
<tr>
<td colspan="2">
这个变量包含启动你当前 bash 的命令的完整路径名</td>
</tr>
<tr>
<th><var>BASH_VERSION</var></th>
<td><code>4.4.0(1)-release</code></td>
</tr>