需求分析
使用环境变量(例如MAILER和PAGER)的shell脚本都有一个隐藏的危险:有些设置指向的程序可能并不存在。如果你以前没有碰到过这种环境变量,那么应该将MAILER设置成你喜欢的电子邮件程序(例如/usr/bin/mailx),将PAGER设置成可以分屏浏览长文档的程序。假如你为了实现灵活性,打算使用PAGER设置代替系统默认的分页程序(通常是more或less程序)来显示脚本输出,你该怎样确保环境变量PAGER的值是一个有效的程序?
第一个脚本解决的问题正是如何测试能否在用户的环境变量PATH中找到指定的程序。该脚本也很好地演示了包括脚本函数和变量切分(variable slicing)在内的各种shell脚本编写技术。代码清单1-1显示了如何验证路径是否有效。
代码实现
#!/bin/bash
# inpath -- 验证指定程序是否有效,或者能否在PATH目录列表中找到
in_path(){
# 尝试在环境变量PATH中找到给定的命令。如果找到,返回0;如果没有找到,返回1。
# 注意:该函数会临时修改IFS(内部字段分隔符),不过在函数执行完毕时会将其恢复原状。
cmd=$1
ourpath=$2
result=1
oldIFS=$IFS
IFS=":"
for directory in $ourpath
do
if [ -x $directory/$cmd ];then
# 如果执行到此处,那么表明我们已经找到了该命令。
result=0
fi
done
IFS=$oldIFS
return $result
}
checkForCmdInPath() {
var=$1
if [ "$var" != "" ];then
if [ "${var:0:1}" = "/" ]; then
if [ ! -x $var ];then
return 1
fi
elif ! in_path $var "$PATH"; then
return 2
fi
fi
}
[ "${var:0:1}" = "/" ]
这个语法只是用来查看指定路径是否以斜线起始。只要确定传入脚本的路径包含起始斜线,就检查是否能在文件系统中找到该路径。- 如果路径开头是/,则假定给出的是绝对路径,然后使用bash操作符-x
[ ! -x $var ]
检查其是否存在。 - 否则,将该值交给函数inpath
elif ! in_path $var "$PATH"
,看看能否在默认的环境变量PATH的各个目录中找到。
工作原理
checkForCmdInPath能够正常工作的关键在于,区分只包含程序名的变量(例如echo)与包含程序完整路径和文件名的变量(例如/bin/echo)。它的做法是检查给定值的第一个字符是否为/。因此,我们需要把第一个字符与变量值的其余部分分离开。
注意:if [ "${var:0:1}" = "/" ]
处的变量切分语法${var:0:1}是一种可以在字符串中指定子串的简写法,从偏移处开始,按照给定长度截取(如果没有提供长度,则返回余下的全部字符串)。例如,表达式${var:10}将会从第11个字符开始返回变量$var余下的值,而${var:10:5}则返回第11个到第15个字符。
运行脚本
要想以独立程序的形式运行这个脚本,首先需要在脚本底部加上一小段代码。这段代码负责获取用户输入并将其传给相应的函数。
if [ $# -ne 1 ];then
echo "Usage:$0 command" >&2
exit 1
fi
checkForCmdInPath "$1"
case $? in
0) echo "$1 found in PATH" ;;
1) echo "$1 not found or not found or not executable" ;;
2) echo "$1 not found in PATH" ;;
esac
exit 0
- 添加上面的代码之后,就可以直接调用脚本了,如接下来的“运行结果”所示。使用脚本完成工作之后,记得要把这段代码删除或注释掉,不然随后将其作为库函数使用时就乱套了。
在测试该脚本的时候,我们使用3个程序名来调用inpath:一个存在的程序、一个虽然存在但没有列入PATH中的程序,以及一个不存在但包含完整的合格文件名和路径的程序。
sh inpath.sh echo # -> echo found in PATH
sh inpath.sh MrEcho # -> MrEcho not found in PATH
sh inpath.sh /usr/bin/MrEcho # ->/usr/bin/MrEcho not found or not found or not executable
- 脚本中最后添加的那段代码将函数in_path的结果转换成了更易于阅读的文字,所以现在我们可以很容易地看到每种情况都按照预期得以处理。
精益求精
如果你想在第一个脚本中就化身为代码忍者,可以将表达式${var:0:1}换成更为复杂的${var%${var#?}},后者是POSIX的变量切分写法。从外表上来看,这种写法嵌套了两个字符串切分。内部的${var#?}会提取变量var中除第一个字符之外的其余所有内容,其中#表示删除指定模式的第一处匹配,?是正则表达式,只匹配单个字符。
接下来,${var%pattern}会产生一个子串,其值为将指定模式从变量var中删除后所剩下的部分。在这个例子中,被删除的模式正是内部字符串切分的结果,所以最后剩下的就是整个字符串的第一个字符。
如果POSIX写法看起来太吓人,大多数shell(包括bash、ksh和zsh)也支持我们在该脚本中采用的${varname:start:size}这种形式。
当然,如果这两种方法你都不喜欢,还可以调用$(echo $var | cut -c1)。在bash编程中,解决问题的手段不止一种,可以通过不同的方式从系统中提取、转换或载入数据。重要的是要意识到并理解“殊途同归”并不意味着不同的方法之间存在优劣之分。
如果你想创建一种能够区分自己是独立运行还是被其他函数所调用的脚本,可以考虑在脚本开始部分加上一个条件测试:if [ "$BASH_SOURCE" = "$0" ]