参数替换
参数替换最简单的形式是在参数前加上美元符号,如$i
或$9
${parameter:-value}
这种写法的意思:如果parameter不为空,则使用它的值;否则,就使用 value。举例来说,在下列命令行中:
echo Using editor ${EDITOR:-/bin/vi}
如果变量EDITOR不为空,Shell
就使用该变量的值,否则使用/bin/vi。其效果等同于:
if [ -n "$EDITOR" ]
then
echo Using editor $EDITOR
else
echo Using editor /bin/vi
fi
重要的是要注意到这种写法并不会改变变量的值,因此,如果之前EDITOR
为空,执行完上面的语句之后,该变量依然为空。
${parameter:=value}
如果parameter
为空的话,不仅会使用value,而且还会将其分配给parameter
(注意其中的=)。你不能使用这种方法给位置参数赋值,也就是说,parameter
不能是数字。典型用法是测试某个导出变量是否已经设置,如果没有,则为其分配默认值:
${PHONEBOOK:=$HOME/phonebook}
这句的意思是如果PHONEBOOK已经分配了值,那么不做任何操作,否则将其设为$HOME/phonebook。
注意,上面的例子是不能单独作为命令的,因为执行完替换操作后,Shell
会尝试执行替换结果:
$ PHONEBOOK=
$ ${PHONEBOOK:=$HOME/phonebook}
sh: /users/steve/phonebook: cannot execute
要想将其作为一个单独的命令,需要使用空命令。如果写作:
: ${PHONEBOOK:=$HOME/phonebook}
Shell
仍旧会进行替换(求值),但是什么都不执行(空命令)。
$ PHONEBOOK=
$ : ${PHONEBOOK:=$HOME/phonebook}
$ echo $PHONEBOOK # 查看是否已赋值
/users/steve/phonebook
$ : ${PHONEBOOK:=foobar} # 应该不会改变
$ echo $PHONEBOOK
/users/steve/phonebook # 的确没有
${parameter:?value}
如果parameter
不为空,Shell
会替换它的值;否则,Shell
将value写入到标准错误,然后退出。如果忽略value,Shell
会输出默认的错误信息:
prog: parameter: parameter null or not set
例如:
$ PHONEBOOK=
$ : ${PHONEBOOK:?"No PHONEBOOK file"}
No PHONEBOOK file
$ : ${PHONEBOOK:?} 没有给出 value
sh: PHONEBOOK: parameter null or not set
$
你可以轻松地利用这种写法检查程序所需的变量是否已经设置且不为空:
: ${TOOLS:?} ${EXPTOOLS:?} ${TOOLBIN:?}
${parameter:+value}
在这种写法中,如果parameter
不为空,则替换成value
;否则,不进行任何替换。它的效果和’:-‘相反。
$ traceopt=T
$ echo options: ${traceopt:+"trace mode"}
options: trace mode
$ traceopt=
$ echo options: ${traceopt:+"trace mode"}
options:
value部分都可以使用命令替换,因为只有在需要这部分值的时候才会执行命令。不过这样也会变得更复杂。考虑下面的语句:
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种写法都不会修改变量的值。所影响到的只是命令行中使用到的内容。另外,模式匹配都是被锚定的。在%
和%%
写法中,变量值必须以指定的模式作为结尾,而在#
和##
写法中,变量值必须以指定的模式作为起始。
$ var=testcase
$ echo $var
testcase
$ echo ${var%e} # 从右侧删除 e
testcas
$ echo $var # 变量内容不变
testcase
$ echo ${var%s*e} # 从右侧删除最短的匹配
testca
$ echo ${var%%s*e} # 从右侧删除最长的匹配
te
$ echo ${var#?e} # 从左侧删除最短的匹配
stcase
$ echo ${var#*s} # 从左侧删除最短的匹配
tcase
$ echo ${var##*s} # 从左侧删除最长的匹配
e
$ echo ${var#test} # 从左侧删除 test
case
$ echo ${var#teas} # 没有匹配
testcase
这些写法有很多实际应用。例如,下面的测试会检查保存在变量file
中的文件名末尾是否为两个字符.o
:
if [ ${file%.o} != $file ] ; then
# 文件名以.o 结尾
...
fi
${#variable}
如何知道变量中保存了多少个字符?下面的写法可以帮你解决:
$ text='The Shell'
$ echo ${#text}
9
$0
变量
无论何时执行Shell
程序,Shell
都会自动将程序名保存在特殊变量$0
中。这种做法在很多情况下都能派上用场,比如说,假设你有一个可以通过多个不同的命令名访问的程序(利用文件系统的硬链接实现)。$0
能够让你以编程的方式找出究竟执行的是哪个命令。更为常见的用法是显示错误信息,因为$0
基于的是实际的程序文件名,而非程序中的硬编码。如果使用$0
来引用程序名,重命名程序会更新输出信息,无须修改程序:
#
# 在电话簿中查找联系人
#
if [ "$#" -ne 1 ] ; then
echo "Incorrect number of arguments"
echo "Usage: $0 name"
exit 1
fi
name=$1
grep "$name" $PHONEBOOK
if [ $? -ne 0 ] ; then
echo "I cou1dn't find $name in the phone book"
fi
有些UNIX系统会自动将$0
设置成包含目录的完整路径,这会导致产生一些乱七八糟的错误信息。可以使用$(basename $0)
或使用模式匹配来取出目录:
${0##*/}
set
命令
Shell
的set
命令有两个作用:
- 设置各种
Shell
选项 - 重新为位置参数
$1
、$2
…赋值。
-x
选项
set
命令能够针对程序中特定部分打开或关闭跟踪模式。
在程序中,下列语句:
set -x
可以打开跟踪模式,这意味着随后的命令会在执行完文件名替换、变量替换及命令替换,还有I/O重定向之后由Shell
打印到标准错误中。被跟踪的命令前面会有一个加号。
$ x=*
$ set -x 设置命令跟踪选项
$ echo $x
+ echo add greetings lu rem rolo
add greetings lu rem rolo
$ cmd=wc
+ cmd=wc
$ ls | $cmd -l
+ ls
+ wc -l
5
只需要执行带有+x
选项的set
命令就可以随时关闭跟踪模式:
$ set +x
+ set +x
$ ls | wc –l
5 返回正常模式
注意,跟踪选项不会沿用到子Shell
中。不过你可以通过在sh -x
后面跟上程序名来跟踪子Shell
的执行:
sh -x rolo
也可以在程序中插入一系列set -x
和set +x
命令来实现相同的效果。实际上,你在程序中完全可以根据自己的需要插入set -x
和set +x
命令来打开或关闭跟踪模式!
无参数的set
如果使用set
的时候不加任何参数,会输出一个按照字母顺序排列的变量列表,这些变量都是存在于当前环境中的局部变量或导出变量:
$ set # 显示所有的变量
CDPATH=:/users/steve:/usr/spool
EDITOR=/bin/vi
HOME=/users/steve
IFS=
LOGNAME=steve
使用set
为位置参数重新赋值
可以使用set
来更改位置参数的值。如果在命令行上将若干单词作为set
的参数,那么这些单词会被赋给对应的位置参数$1
、$2
…。位置参数之前的值也就被覆盖了。在Shell
程序中,下列命令:
set a b c
会将a赋给$1
,b赋给$2
,c赋给$3
。$#
的值会被设为3,以反映出参数个数。
$ set one two three four
$ echo $1:$2:$3:$4
one:two:three:four
$ echo $# 应该为 4
4
$ echo $* 现在保存的内容是什么?
one two three four
$ for arg; do echo $arg; done
one
two
three
four
set
命令执行完成之后,一切和预期中的一样:$#
、$*
和不包含列表的for
循环都反映出了位置参数值的变化。set
常用来”解析”从文件或终端中读入的数据。下面是一个叫做words的程序,该程序可以统计出输入的一行中所包含的单词数。
#
# 统计一行中的单词
#
read line
set $line
echo $#
$ words 运行程序
Here's a line for you to count.
7
--
选项
该选项告诉set
对于后续出现的连接符或参数形式的单词,均不视其为选项。
例如:
#
# 统计标准输入中的所有单词
#
count=0
while read line
do
set -- $line
count=$(( count + $# ))
done
echo $count
IFS
变量
有一个叫作IFS
的特殊Shell
变量,它代表的是内部字段分隔符。当Shell
解析read
命令输入、命令替换(反引号机制)输出以及执行变量替换时,会用到该变量。简单地说,IFS
包含了一组用作空白分隔符的字符。如果在命令行中输入,Shell
会将其视为普通的空白字符(也就是作为单词分隔符)。来看下列命令的输出:
$ echo "$IFS"
$
为了弄明白这里到底都有些什么字符,我们将echo
的输出通过管道传给带有-b
选项的od
命令:
$ echo "$IFS" | od –b
0000000 040 011 012 012
0000004
通过上述观察可知默认的IFS
包含:
- 空格
- 制表符
- 换行符
要想将IFS
修改成单个换行符,需要先输入一个起始引号,接着立刻按Enter键,然后在下一行中输入结束引号。引号之间不能输入任何其他字符,因为额外的字符都会被保存在IFS
中,随后由Shell
使用。
现在让我们把IFS
改成更容易识别的冒号:
。
$ IFS=:
$ read x y z
123:345:678
$ echo $x
123
$ echo $z
678
$ list="one:two:three"
$ for x in $list; do echo $x; done
one
two
three
$ var=a:b:c
$ echo "$var"
a:b:c
由于IFS
会影响到Shell
对于单词分隔符的解释方式,因此,如果你打算在自己的程序中修改它,通常明智的做法是先将旧的IFS
值保存在另一个变量中(如 OIFS),等执行完操作后再将其恢复。
readonly
命令
readonly
命令用于指定在程序随后的执行过程中,值都不会发生改变的那些变量。例如:
readonly PATH HOME
指明PATH和HOME变量为只读变量。如果之后试图给这两个变量赋值,就会导致Shell
发出错误信息:
$ PATH=/bin:/usr/bin:.:
$ readonly PATH
$ PATH=$PATH:/users/steve/bin
sh: PATH: is read-only
要想获得一份只读变量的列表,可以输入readonly -p
:
$ readonly -p
readonly PATH=/bin:/usr/bin:.:
变量的只读属性不会往下传给子Shell
。另外,只要在Shell
中将变量设为只读,就没有”后悔药”了。
unset
命令
有时候你可能想从环境中删除某个变量。可以输入unset
,后面跟上要删除的变量名:
$ x=100
$ echo $x
100
$ unset x 从环境中删除 x
$ echo $x
你不能对只读变量使用unset
。而且对于变量IFS
、MAILCHECK
、PATH
、PS1
和PS2
,也不能使用unset
。