使用正确的Shell

到目前为止,我们只是把命令放进文件,然后将其作为Shell程序运行,并没有真正谈及究竟是哪个Shell读取命令并执行程序。在默认情况下,因为Shell程序是由登录Shell运行的,所以这还不算什么大问题。其实所有主要的交互式Shell都允许你指定由哪个Shell来执行文件。如果文件中第一行的前两个字符是#!,那么该行余下的部分就指定了该文件的解释器,因此#!/bin/ksh指定了Korn Shell,而#!/bin/bash 则指定了Bash。
你也可以指定任何你想用的程序,所以以下面语句起始的Perl程序#!/usr/bin/perl 会强制Shell调用/usr/bin/perl来解释文件中的内容。无论用户使用的是什么登录Shell,为了确保使用Bourne Shell,在系统Shell程序中基本上都会使用下面的写法:

  1. #!/bin/sh

ENV文件

当你启动Shell时,它要先做的其中一件事就是查找环境变量ENV。如果找到了该变量且变量不为空,则执行由ENV所指定的文件,这和登录时要执行.profile很像。ENV指定的文件中包含用于设置Shell环境的命令。本章中提及的内容都可以放进该文件。如果你决定编写一个ENV文件,那么应该在.profile中设置并导出ENV变量:

  1. $ cat .profile
  2. ...
  3. export ENV=$HOME/.alias
  4. ...

注意,在上面使用了一种快捷的写法:不需要先给变量赋值,然后调用export,为了提高效率,这两步可以放在一行上完成。

命令行编辑

行编辑模式是Shell的特性之一,它模仿了两种流行的全屏编辑器,允许你使用相同的方法编辑命令行。POSIX标准Shell可以模仿 viBashKorn Shell还支持emacs的行编辑模式。如果你用过这两种全屏编辑器中的任何一种,就会发现Shell内建的行编辑器在功能上完全再现了viemacs。这是 Shell 最有帮助的特性之一。使用set命令配合-o mode选项可以启用行编辑模式,其中mode可以是viemacs

  1. $ set -o vi 启用 vi 模式

将这一句放入.profileENV文件中,使得Shell在启动时可以自动开启指定的行编辑模式。

命令历史

无论你用的是哪种Shell,它都会保留你之前输入过的所有命令的历史记录。每次你按下Enter键执行一条命令,该命令就会被添加到历史记录的末尾。
根据你的设置,命令历史记录甚至可以保存成文件,在另一次登录会话中恢复,这样你就能快速地访问上一次会话期间用过的命令。在默认情况下,历史记录以文件的形式保存在用户主目录下,文件名为.sh_history。你可以将该文件设为任何名字,只要保证和变量HISTFILE的内容一样就行了。这个变量可以在.profile文件中设置并导出。Shell能够保存的命令数量有限,最少能保存128条命令,不过大多数现代Shell能够保存500条甚至更多。每次登录后,Shell都会自动将历史记录文件截断到这个长度。你可以通过HISTSIZE变量控制历史记录文件的大小。如果默认大小不能满足你的要求,那么可以将HISTSIZE变量设成更大的值,如500或1000。赋给HISTSIZE的值可以在.profile文件中设置及导出:

  1. $ grep HISTSIZE .profile
  2. HISTSIZE=500
  3. export HISTSIZE

访问历史记录的其他方法

history命令

访问命令历史记录最简单的方法就是使用history命令。

  1. $ history
  2. 507 cd Shell
  3. 508 cd ch15
  4. 509 vi int
  5. 510 ps
  6. 511 echo $HISTSIZE
  7. 512 cat $ENV
  8. 513 cp int int.sv
  9. 514 history
  10. 515 exit
  11. 516 cd Shell
  12. 517 cd ch16
  13. 518 vi all
  14. 519 run -n5 all

左侧的数字是命令编号(编号为 1 的命令是历史记录中最靠前的命令,或者说是最早的命令)。
如果你不想被这么多的命令记录所”淹没”,可以将想看到的命令数作为参数:

  1. $ history 10
  2. 513 cp int int.sv
  3. 514 history
  4. 515 exit
  5. 516 cd Shell
  6. 517 cd ch16
  7. 518 vi all
  8. 519 run -n5 all
  9. 520 ps
  10. 521 lpr all.out
  11. 522 history 10

fc命令

fc命令可以为历史记录中的若干条命令启动一个编辑器或将历史命令列表写入终端中。在后一种形式中,可以使用-l选项来指定命令列表,这和history差不多,无非就是灵活性更大(你可以指定要显示的命令范围)。例如,下列命令:

  1. fc -l 510 515

会将编号在510~515之间的命令写入到标准输出,而命令:

  1. fc -n -l -20

会将最近的20条命令写入到标准输出,在输出命令时忽略命令编号(-n)。
假设你刚执行了一条挺长的命令,然后觉得把这条命令改成名为runx的脚本程序应该不错。你可以使用fc从历史记录中把这条命令挑出来,然后利用I/O重定向将其写入文件中:

  1. fc -n -l -1 > runx

命令中先是字母l,然后是用数字-1获得最近执行的那条命令。

执行上一条指令

!string可以搜索历史记录,!!可以重新执行上一条命令:

  1. $ !!
  2. cat docs/planB
  3. ...
  4. $ !d
  5. date
  6. Thu Oct 24 14:39:40 EST 2002

!string之间没有空格。

函数

局部变量

Bash和Korn Shell函数都可以拥有局部变量,这使得编写递归函数成为可能。局部变量使用typeset命令定义:

  1. typeset i j

如果已经存在同名变量,该同名变量的值在执行typeset时会被保存,当函数退出时恢复。
用过一段时间的Shell之后,你可能有了一些自己编写的函数,希望在交互式工作会话中使用。ENV文件是一个定义函数不错的地方,这样不管什么时候启动了新Shell,都可以直接使用这些函数。

自动载入函数

Korn Shell允许设置一个特殊变量FPATH,它类似于PATH变量。如果你试图执行一个尚未定义的函数,Korn Shell会在FPATH所保存的一系列以冒号分隔的目录中搜索匹配该函数名的文件。如果找到,便会在当前Shell中执行该文件,期望其中定义了指定的函数。

整数算术

Bash和Korn Shell都支持在不使用算术扩展的情况下求值算术表达式。其语法类似于$((...)),不过不需要使用$。因为不会执行扩展,所以这种写法本身可以作为命令使用:

  1. $ x=10
  2. $ ((x = x * 12))
  3. $ echo $x
  4. 120

它的真正价值在于可以将算术表达式用于ifwhileuntil命令中。如果比较结果为假,比较运算符会将退出状态码设为非0;如果结果为真,退出状态码为0。因此,下面的写法:

  1. (( i == 100 ))

会测试i是否等于100并设置相应的退出状态码。这非常适合用在if条件中:

  1. if (( i == 100 ))
  2. then
  3. ...
  4. fi

相较于test的一个优势在于在测试的同时还可以执行算术运算:

  1. if (( i / 10 != 0 ))
  2. then
  3. ...
  4. fi

如果i除以10不等于0,则返回真。
while循环也可以从这种写法中获益。例如:

  1. x=0
  2. while ((x++ < 100))
  3. do
  4. commands
  5. done

会执行 100 次 commands。

整数类型

Korn和Bash Shell 都支持整数类型。你可以使用带有-i选项的typeset声明整数类型的变量:

  1. typeset -i variables

其中,variables可以是任何合法的Shell变量名。在声明的同时可以初始化变量:

  1. typeset -i signal=1

这样做的主要好处在于:相较非整数值,((...))对整数执行算术运算的速度要更快。
但是只能将整数值或整数表达式赋给整数类型的变量。如果你试图为其分配非整数值,Shell会输出bad number

  1. $ typeset -i i
  2. $ i=hello
  3. ksh: i: bad number

Bash 会直接忽略不包含数字值的字符串,对于包含数字和其他字符的字符串,会产生错误信息:

  1. $ typeset -i i
  2. $ i=hello
  3. $ echo $i
  4. 0
  5. $ i=1hello
  6. bash: 1hello: value too great for base (error token is "1hello")
  7. $ i=10+15
  8. $ echo $i
  9. 25

不同基数的数字

Korn和Bash也允许你对不同基数的值进行算术运算。要想在Shell中使用其他基数的值,可以这样写:

  1. base#number

例如,要想表示基数为 8(八进制)的值100,必须写成:

  1. 8#100

在允许出现整数值的地方,都可以使用其他进制。例如,可以将八进制数100赋给整数类型变量 i:

  1. typeset -i i=8#100

注意,在Korn Shell中,赋给整数类型变量的第一个值的基数决定了该变量后续使用的默认基数。换句话说,如果赋给整数类型变量i的第一个值是八进制,那么之后每次引用i的时候,Korn Shell都会使用8#value的记法,按照八进制数来显示该变量的值。

  1. $ typeset i i=8#100
  2. $ echo $i
  3. 8#100
  4. $ i=50
  5. $ echo $i
  6. 8#62
  7. $ (( i = 16#a5 + 16#120 ))
  8. $ echo $i
  9. 8#705

因为赋给i的第一个值是八进制数(8#100),所以之后对于i的所有引用都会使用八进制。接下来,十进制数50被分配给i,然后在显示i的值时,我们看到的形式是8#62,这是十进制数50所对应的八进制。这里有一个细微之处:尽管i的值被设置成按照八进制显示,但是分配给该变量的值的默认基数仍旧是十进制,除非另行指定。换句话说,i=50并不等同于i=8#50,即便是Shell知道在引用i的时候要使用八进制。在上面的例子中,((...))用来将两个十六进制数a5和120相加。然后将结果以八进制形式显示出来。我们承认,这种做法很是晦涩难懂,在日常的Shell编程或交互式应用中不大可能碰到。
Bash对任何基数的值都可以使用base#number的语法,对八进制和十六进制值可以使用C语言的语法(八进制数前面加上数字 0,十六进制数前面加上 0x):

  1. $ typeset -i i=0100
  2. $ echo $i
  3. 64
  4. $ i=0x80
  5. $ echo $i
  6. 128
  7. $ i=2#1101001
  8. $ echo $i
  9. 105
  10. $ (( i = 16#a5 + 16#120 ))
  11. $ echo $i
  12. 453

和Korn Shell不同,Bash并不保持变量的基数,整数类型变量就按照十进制形式显示。你总是可以使用printf打印出八进制或十六进制格式的整数。

alias命令

别名是Shell提供的一种可以用于自定义命令的快捷记法。Shell保存了一个别名列表,在命令输入之后,会在执行其他替换操作之前首先搜索该列表。如果命令行的第一个单词是别名,将该别名替换成对应的文本。可以使用alias命令定义别名。其格式如下:

  1. alias name=string

其中,name是别名的名称,string是任意的字符串。例如:

  1. alias ll='ls -l'

ll作为ls -l的别名。如果现在用户输入ll命令,Shell会悄悄地将其替换为ls -l。更妙的是,你还可以在别名之后输入参数:

  1. ll *.c

在完成别名替换之后,该命令会变成:

  1. ls -l *.c

定义别名

在别名设置及使用的时候,Shell会执行正常的命令行处理,比较棘手的地方是引用。举例来说,我们知道Shell在变量PWD中保存了当前工作目录:

  1. $ cd /users/steve/letters
  2. $ echo $PWD
  3. /users/steve/letters

创建一个叫做dir的别名,该别名通过PWD变量以及参数替换,可以给出当前工作目录的基本名称(base name):

  1. alias dir="echo ${PWD##*/}"

这种写法看起来很合理。

  1. $ alias dir="echo ${PWD##*/}" 定义别名
  2. $ pwd 当前位置
  3. /users/steve
  4. $ dir 应用别名
  5. steve
  6. $ cd letters 更改目录
  7. $ dir 再次应用别名
  8. steve
  9. $ cd /usr/spool 再次更改目录
  10. $ dir
  11. steve

无论当前目录是什么,别名dir总是输出steve。这是因为当我们定义别名dir的时候,没有仔细处理好引用。回想一下,Shell会在双引号中执行参数替换,问题就在于Shell是在定义别名的时候对${PWD##*/}进行求值的。这意味着在别名dir的定义实际上相当于我们输入了:

  1. $ alias dir="echo steve"

解决方法是定义的时候使用单引号而不是双引号,将参数替换推迟到应用别名的时候:

  1. $ alias dir='echo ${PWD##*/}' 定义别名
  2. $ pwd 当前位置
  3. /users/steve
  4. $ dir 应用别名
  5. steve
  6. $ cd letters 更改目录
  7. $ dir 再次应用别名
  8. letters
  9. $ cd /usr/spool 再次更改目录
  10. $ dir
  11. spool

如果别名以空格结束,则会对其之后的单词执行别名替换。例如:

  1. alias nohup="/bin/nohup "
  2. nohup ll

在使用/bin/nohup替换了别名nohup之后,Shell还会检查字符串ll,查看有没有对应的别名。
把命令引用起来或是将其放在反斜线之后可以避免进行别名替换。
alias name会列出别名name对应的值,不带参数的alias命令会列出所有的别名。

删除别名

unalias命令可以删除别名。其格式为:

  1. unalias name

该命令会删除别名name
而下列命令:

  1. unalias -a

会删除所有的别名。
如果你定义了一些别名,并希望在登录会话期间使用,那么可以将其定义在ENV文件中,这样就能够一直使用了,这些别名定义不会进入子Shell

数组

Korn 和 Bash Shell 均提供了有限的数组功能。Bash数组对于数组元素的个数没有限制(仅受限于内存容量),而Korn Shell将数组元素个数限制在4096个。在这两种Shell中,数组元素都是以 0 作为起始。
数组元素可以通过下标访问,下标是一个值为整数的表达式,两侧由中括号包围。你并不需要声明Shell数组的大小,直接给元素赋值就行了。给元素赋值和给普通变量赋值没什么两样:

  1. $ arr[0]=hello
  2. $ arr[1]="some text"
  3. $ arr[2]=/users/steve/memos

要想从数组中检索某个元素,首先要写出数组名,然后是一个起始中括号,接着是元素下标以及另一个闭合中括号。之前所有这些内容必须再放入一对花括号中,然后在花括号前面加上美元符号。

  1. $ echo ${array[0]}
  2. hello
  3. $ echo ${array[1]}
  4. some text
  5. $ echo ${array[2]}
  6. /users/steve/memos
  7. $ echo $array
  8. hello

如果在执行替换的时候忘了写花括号,也许产生的结果未必是你想要的:

  1. $ echo $array[1]
  2. hello[1]

[*]可以作为下标,用来在命令行中生成数组的所有元素,元素之间用空格分隔。

  1. $ echo ${array[*]}
  2. hello some text /users/steve/memos

${#array[*]}可以用来获得 array 中的元素个数。

  1. $ echo ${#array[*]}
  2. 3

这个数字是数组元素的个数,并非保存元素的最大下标数。

  1. $ array[10]=foo
  2. $ echo ${array[*]} 显示所有元素
  3. hello some text /users/steve/memos foo
  4. $ echo ${#array[*]} 元素个数
  5. 4

包含非连续值的数组称为稀疏数组。
你可以使用typeset -i指定数组名来声明一个整数数组:

  1. typeset -i data

可以使用((...))对数组元素执行整数运算:

  1. $ typeset -i array
  2. $ array[0]=100
  3. $ array[1]=50
  4. $ (( array[2] = array[0] + array[1] ))
  5. $ echo ${array[2]}
  6. 150
  7. $ i=1
  8. $ echo ${array[i]}
  9. 50
  10. $ array[3]=array[0]+array[2]
  11. $ echo ${array[3]}
  12. 250

注意,在双括号中引用数组元素时不仅可以省略美元符号和花括号,而且,如果声明的是整数数组的话,就算不在双括号中也可以省略。另外,下标表达式中的变量前面不需要使用美元符号。

数组写法 含义
${array[i]} 替换为元素 i 的值
$array 替换为数组第一个元素的值(array[0])
${array[*]} 替换为所有数组元素的值
${#array[*]} 替换为元素个数
array[i]=val 将 val 保存到 array[i]

其他特性

cd命令的其他特性

cd命令看起来挺直截了当的,但它还有几个鲜为人知的技巧。例如,作为一种简写方式,参数-表示”上一个目录”:

  1. $ pwd
  2. /usr/src/cmd
  3. $ cd /usr/spool/uucp
  4. $ pwd
  5. /usr/spool/uucp
  6. $ cd - 切换到上一个目录
  7. /usr/src/cmd cd 命令打印出新目录名
  8. $ cd -
  9. /usr/spool/uucp

波浪符替换

如果命令行中有单词是以波浪符~起始,Shell会执行以下替换操作:如果波浪符是单词中唯一的字符或者紧挨着波浪符的是斜线/,会使用HOME变量的值来替换。

  1. $ echo ~
  2. /users/pat
  3. $ qrep Korn ~/Shell/chapter9/ksh
  4. The Korn Shell is a new Shell developed
  5. by David Korn at AT&T

如果从波浪符往后,一直到斜线的剩余单词是/etc/passwd中的用户登录名,那么波浪符和用户登录名会被该用户的HOME目录所替换。

  1. $ echo ~steve
  2. /users/steve
  3. $ echo ~pat
  4. /users/pat
  5. $ qrep Korn -pat/Shell/chapter9/ksh
  6. The Korn Shell is a new Shell developed
  7. by David Korn at AT&T
  8. for the Bourne Shell would also run under the Korn
  9. the one on System V, the Korn Shell provides you with
  10. idea of the compatibility of the Korn Shell with Bourne's,
  11. the Bourne and Korn Shells.
  12. The main features added to the Korn Shell are:

在Korn和Bash Shell中,如果~后面是+-,那么会分别使用变量PWDOLDPWD的值来替换。PWDOLDPWDcd所设置,其中保存的是当前目录和上一个目录的完整路径。POSIX标准Shell不支持~+~-

  1. $ pwd
  2. /usr/spool/uucppublic/steve
  3. $ cd
  4. $ pwd
  5. /users/pat
  6. $ echo ~+
  7. /users/pat
  8. $ echo ~-
  9. /usr/spool/uucppublic/steve

搜索次序

有必要弄清楚在命令行中输入命令名后 Shell 所采用的搜索次序:

  • Shell首先检查命令是否为保留字(如fordo)。
  • 如果不是保留字,也没有被引用,Shell接着检查别名列表,如果在其中找到匹配的别名,就执行替换操作。如果别名定义是以空格结尾,Shell还会尝试对下一个单词执行别名替换。针对替换后的最终结果,再次检查保留字列表,如果不是保留字,继续进行下一步。
  • 针对命令名检查函数列表,如果找到的话,执行其中的同名函数。
  • 检查是否为内建命令(如cdpwd)。
  • 最后,搜索PATH来定位命令。
  • 如果还没有找到,输出错误信息command not found