参数替换

参数替换最简单的形式是在参数前加上美元符号,如$i$9

${parameter:-value}

这种写法的意思:如果parameter不为空,则使用它的值;否则,就使用 value。举例来说,在下列命令行中:

  1. echo Using editor ${EDITOR:-/bin/vi}

如果变量EDITOR不为空,Shell就使用该变量的值,否则使用/bin/vi。其效果等同于:

  1. if [ -n "$EDITOR" ]
  2. then
  3. echo Using editor $EDITOR
  4. else
  5. echo Using editor /bin/vi
  6. fi

重要的是要注意到这种写法并不会改变变量的值,因此,如果之前EDITOR为空,执行完上面的语句之后,该变量依然为空。

${parameter:=value}

如果parameter为空的话,不仅会使用value,而且还会将其分配给parameter(注意其中的=)。你不能使用这种方法给位置参数赋值,也就是说,parameter不能是数字。典型用法是测试某个导出变量是否已经设置,如果没有,则为其分配默认值:

  1. ${PHONEBOOK:=$HOME/phonebook}

这句的意思是如果PHONEBOOK已经分配了值,那么不做任何操作,否则将其设为$HOME/phonebook。
注意,上面的例子是不能单独作为命令的,因为执行完替换操作后,Shell会尝试执行替换结果:

  1. $ PHONEBOOK=
  2. $ ${PHONEBOOK:=$HOME/phonebook}
  3. sh: /users/steve/phonebook: cannot execute

要想将其作为一个单独的命令,需要使用空命令。如果写作:

  1. : ${PHONEBOOK:=$HOME/phonebook}

Shell仍旧会进行替换(求值),但是什么都不执行(空命令)。

  1. $ PHONEBOOK=
  2. $ : ${PHONEBOOK:=$HOME/phonebook}
  3. $ echo $PHONEBOOK # 查看是否已赋值
  4. /users/steve/phonebook
  5. $ : ${PHONEBOOK:=foobar} # 应该不会改变
  6. $ echo $PHONEBOOK
  7. /users/steve/phonebook # 的确没有

${parameter:?value}

如果parameter不为空,Shell会替换它的值;否则,Shell将value写入到标准错误,然后退出。如果忽略value,Shell会输出默认的错误信息:

  1. prog: parameter: parameter null or not set

例如:

  1. $ PHONEBOOK=
  2. $ : ${PHONEBOOK:?"No PHONEBOOK file"}
  3. No PHONEBOOK file
  4. $ : ${PHONEBOOK:?} 没有给出 value
  5. sh: PHONEBOOK: parameter null or not set
  6. $

你可以轻松地利用这种写法检查程序所需的变量是否已经设置且不为空:

  1. : ${TOOLS:?} ${EXPTOOLS:?} ${TOOLBIN:?}

${parameter:+value}

在这种写法中,如果parameter不为空,则替换成value;否则,不进行任何替换。它的效果和’:-‘相反。

  1. $ traceopt=T
  2. $ echo options: ${traceopt:+"trace mode"}
  3. options: trace mode
  4. $ traceopt=
  5. $ echo options: ${traceopt:+"trace mode"}
  6. options:

value部分都可以使用命令替换,因为只有在需要这部分值的时候才会执行命令。不过这样也会变得更复杂。考虑下面的语句:

  1. WORKDIR=${DBDIR:-$(pwd)}

如果DBDIR不为空的话,将其值赋给WORKDIR,否则执行pwd命令并将命令结果赋给WORKDIR。仅当DBDIR为空时才执行pwd。

模式匹配

模式匹配接受两个参数:变量名(或者是参数数量)和模式。Shell会在指定变量的内容中匹配所提供的模式。如果能够匹配,则在命令行中使用该变量的值(不包括模式所匹配的那部分内容)。如果无法匹配,则使用变量的全部内容。在这两种情况下,都不会修改变量的内容。
在这里使用模式一词是因为 Shell 允许你使用和文件名替换以及case语句中相同的模式匹配字符:

  • *匹配零个或多个字符
  • ?匹配任意单个字符
  • [...]匹配指定字符组中任意单个字符
  • [!...]匹配不在字符组中的任意单个字符

POSIX Shell提供了 4 种能够执行模式匹配的参数替换形式。

  • ${variable%pattern} : Shell会检查variable是否以指定的pattern结束。如果是,则使用variable的内容并从其右侧删除pattern所能够匹配到的最短结果。
  • ${variable%%pattern}: Shell仍旧会检查variable是否以指定的pattern结束。但这次,它会从右侧删除pattern所能够匹配到的最长结果。
  • ${variable#pattern}: Shell在命令行中使用variable的内容并从其左侧删除pattern所能够匹配到的最短结果。
  • ${variable##pattern}: Shell在命令行中使用variable的内容并从其左侧删除pattern所能够匹配到的最长结果。

这4种写法都不会修改变量的值。所影响到的只是命令行中使用到的内容。另外,模式匹配都是被锚定的。在%%%写法中,变量值必须以指定的模式作为结尾,而在###写法中,变量值必须以指定的模式作为起始。

  1. $ var=testcase
  2. $ echo $var
  3. testcase
  4. $ echo ${var%e} # 从右侧删除 e
  5. testcas
  6. $ echo $var # 变量内容不变
  7. testcase
  8. $ echo ${var%s*e} # 从右侧删除最短的匹配
  9. testca
  10. $ echo ${var%%s*e} # 从右侧删除最长的匹配
  11. te
  12. $ echo ${var#?e} # 从左侧删除最短的匹配
  13. stcase
  14. $ echo ${var#*s} # 从左侧删除最短的匹配
  15. tcase
  16. $ echo ${var##*s} # 从左侧删除最长的匹配
  17. e
  18. $ echo ${var#test} # 从左侧删除 test
  19. case
  20. $ echo ${var#teas} # 没有匹配
  21. testcase

这些写法有很多实际应用。例如,下面的测试会检查保存在变量file中的文件名末尾是否为两个字符.o

  1. if [ ${file%.o} != $file ] ; then
  2. # 文件名以.o 结尾
  3. ...
  4. fi

${#variable}

如何知道变量中保存了多少个字符?下面的写法可以帮你解决:

  1. $ text='The Shell'
  2. $ echo ${#text}
  3. 9

$0变量

无论何时执行Shell程序,Shell都会自动将程序名保存在特殊变量$0中。这种做法在很多情况下都能派上用场,比如说,假设你有一个可以通过多个不同的命令名访问的程序(利用文件系统的硬链接实现)。$0能够让你以编程的方式找出究竟执行的是哪个命令。更为常见的用法是显示错误信息,因为$0基于的是实际的程序文件名,而非程序中的硬编码。如果使用$0来引用程序名,重命名程序会更新输出信息,无须修改程序:

  1. #
  2. # 在电话簿中查找联系人
  3. #
  4. if [ "$#" -ne 1 ] ; then
  5. echo "Incorrect number of arguments"
  6. echo "Usage: $0 name"
  7. exit 1
  8. fi
  9. name=$1
  10. grep "$name" $PHONEBOOK
  11. if [ $? -ne 0 ] ; then
  12. echo "I cou1dn't find $name in the phone book"
  13. fi

有些UNIX系统会自动将$0设置成包含目录的完整路径,这会导致产生一些乱七八糟的错误信息。可以使用$(basename $0)或使用模式匹配来取出目录:

  1. ${0##*/}

set命令

Shellset命令有两个作用:

  • 设置各种Shell选项
  • 重新为位置参数$1$2…赋值。

-x选项

set命令能够针对程序中特定部分打开或关闭跟踪模式。
在程序中,下列语句:

  1. set -x

可以打开跟踪模式,这意味着随后的命令会在执行完文件名替换、变量替换及命令替换,还有I/O重定向之后由Shell打印到标准错误中。被跟踪的命令前面会有一个加号。

  1. $ x=*
  2. $ set -x 设置命令跟踪选项
  3. $ echo $x
  4. + echo add greetings lu rem rolo
  5. add greetings lu rem rolo
  6. $ cmd=wc
  7. + cmd=wc
  8. $ ls | $cmd -l
  9. + ls
  10. + wc -l
  11. 5

只需要执行带有+x选项的set命令就可以随时关闭跟踪模式:

  1. $ set +x
  2. + set +x
  3. $ ls | wc l
  4. 5 返回正常模式

注意,跟踪选项不会沿用到子Shell中。不过你可以通过在sh -x后面跟上程序名来跟踪子Shell的执行:

  1. sh -x rolo

也可以在程序中插入一系列set -xset +x命令来实现相同的效果。实际上,你在程序中完全可以根据自己的需要插入set -xset +x 命令来打开或关闭跟踪模式!

无参数的set

如果使用set的时候不加任何参数,会输出一个按照字母顺序排列的变量列表,这些变量都是存在于当前环境中的局部变量或导出变量:

  1. $ set # 显示所有的变量
  2. CDPATH=:/users/steve:/usr/spool
  3. EDITOR=/bin/vi
  4. HOME=/users/steve
  5. IFS=
  6. LOGNAME=steve

使用set为位置参数重新赋值

可以使用set来更改位置参数的值。如果在命令行上将若干单词作为set的参数,那么这些单词会被赋给对应的位置参数$1$2…。位置参数之前的值也就被覆盖了。在Shell程序中,下列命令:

  1. set a b c

会将a赋给$1,b赋给$2,c赋给$3$#的值会被设为3,以反映出参数个数。

  1. $ set one two three four
  2. $ echo $1:$2:$3:$4
  3. one:two:three:four
  4. $ echo $# 应该为 4
  5. 4
  6. $ echo $* 现在保存的内容是什么?
  7. one two three four
  8. $ for arg; do echo $arg; done
  9. one
  10. two
  11. three
  12. four

set命令执行完成之后,一切和预期中的一样:$#$*和不包含列表的for循环都反映出了位置参数值的变化。
set常用来”解析”从文件或终端中读入的数据。下面是一个叫做words的程序,该程序可以统计出输入的一行中所包含的单词数。

  1. #
  2. # 统计一行中的单词
  3. #
  4. read line
  5. set $line
  6. echo $#
  7. $ words 运行程序
  8. Here's a line for you to count.
  9. 7

--选项

该选项告诉set对于后续出现的连接符或参数形式的单词,均不视其为选项。
例如:

  1. #
  2. # 统计标准输入中的所有单词
  3. #
  4. count=0
  5. while read line
  6. do
  7. set -- $line
  8. count=$(( count + $# ))
  9. done
  10. echo $count

IFS变量

有一个叫作IFS的特殊Shell变量,它代表的是内部字段分隔符。当Shell解析read命令输入、命令替换(反引号机制)输出以及执行变量替换时,会用到该变量。简单地说,IFS包含了一组用作空白分隔符的字符。如果在命令行中输入,Shell会将其视为普通的空白字符(也就是作为单词分隔符)。来看下列命令的输出:

  1. $ echo "$IFS"
  2. $

为了弄明白这里到底都有些什么字符,我们将echo的输出通过管道传给带有-b选项的od命令:

  1. $ echo "$IFS" | od b
  2. 0000000 040 011 012 012
  3. 0000004

通过上述观察可知默认的IFS包含:

  • 空格
  • 制表符
  • 换行符

要想将IFS修改成单个换行符,需要先输入一个起始引号,接着立刻按Enter键,然后在下一行中输入结束引号。引号之间不能输入任何其他字符,因为额外的字符都会被保存在IFS中,随后由Shell使用。
现在让我们把IFS改成更容易识别的冒号:

  1. $ IFS=:
  2. $ read x y z
  3. 123:345:678
  4. $ echo $x
  5. 123
  6. $ echo $z
  7. 678
  8. $ list="one:two:three"
  9. $ for x in $list; do echo $x; done
  10. one
  11. two
  12. three
  13. $ var=a:b:c
  14. $ echo "$var"
  15. a:b:c

由于IFS会影响到Shell对于单词分隔符的解释方式,因此,如果你打算在自己的程序中修改它,通常明智的做法是先将旧的IFS值保存在另一个变量中(如 OIFS),等执行完操作后再将其恢复。

readonly命令

readonly命令用于指定在程序随后的执行过程中,值都不会发生改变的那些变量。例如:

  1. readonly PATH HOME

指明PATH和HOME变量为只读变量。如果之后试图给这两个变量赋值,就会导致Shell发出错误信息:

  1. $ PATH=/bin:/usr/bin:.:
  2. $ readonly PATH
  3. $ PATH=$PATH:/users/steve/bin
  4. sh: PATH: is read-only

要想获得一份只读变量的列表,可以输入readonly -p

  1. $ readonly -p
  2. readonly PATH=/bin:/usr/bin:.:

变量的只读属性不会往下传给子Shell。另外,只要在Shell中将变量设为只读,就没有”后悔药”了。

unset命令

有时候你可能想从环境中删除某个变量。可以输入unset,后面跟上要删除的变量名:

  1. $ x=100
  2. $ echo $x
  3. 100
  4. $ unset x 从环境中删除 x
  5. $ echo $x

你不能对只读变量使用unset。而且对于变量IFSMAILCHECKPATHPS1PS2,也不能使用unset