摘自 shell脚本实战 第二版 第一章 遗失的代码库
使用环境变量(例如 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 # -x 判断文件是否可执行
result=0 # 如果执行到此处,那么表明我们已经找到了命令
fi
done
IFS=$oldIFS
return $result
}
checkForCmdInPath(){
var=$1
if [ "$var" != "" ];then
if [ "${var:0:1}" = "/" ];then # ${var:0:1} 从下标0开始截取1个长度字符
if [ ! -x $var ];then
return 1
fi
elif ! in_path $var "$PATH" ;then
return 2
fi
fi
}
if [ $# -ne 1 ];then # -ne 不等于 !=
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 executable" ;;
2) echo "$1 not found in PATH" ;;
esac
exit 0
$ ./inpath echo
echo found in PATH
$ ./inpath NotExistEcho
NotExistEcho not found in PATH
$ ./inpath /usr/bin/NotExistEcho
/usr/bin/NotExistEcho not found or not executable
如果你想在第一个脚本中就化身为代码忍者, 可以将表达式 ${var:0:1} 换成更为复杂的 KaTeX parse error: Expected '}', got 'EOF' at end of input: {var%{var#?}},后者是 POSIX 的变量切分写法。
从外表上来看,这种写法嵌套了两个字符串 切分。内部的${var#?}会提取变量 var 中除第一个字符之外的其余所有内容,其中#表示删除指 定模式的第一处匹配,?是正则表达式,只匹配单个字符。
$ name=coco.txt
$ echo ${name#*.}
txt
$ echo ${name%.*}
coco
用户总是无视操作指南,输入一些不一致、格式错误或语法有问题的数据。作为一名 shell 脚本开发人员,你得在这些错误引发问题之前将其找出并标记出来。
一种典型的情况涉及文件名和数据库键名。你的程序要提示用户输入的字符应该仅限字母数 字,只能包含大写字母、小写字母和数字,不能有标点符号、特殊字符和空格。用户输入的字符 串是否有效?这正是代码清单 1-3 要测试的
#!/bin/bash
# validAlphaNum -- 确保输入内容仅限于字母和数字
validAlphaNum(){
# 返回值:如果输入内容全都是字母和数字,那么返回0;否则,返回1
# 删除所有不符合要求的字符
validchars="$(echo $1 |sed -e 's/[^[:alnum:]]//g')" # -e 可以指定多个匹配规则
if [ "$validchars" = "$1" ];then
return 0
else
return 1
fi
}
# 主脚本开始 -- 如果要将该脚本包含到其他脚本之内,那么删除活注释掉本行一下的所有内容
# ==============
read -p "Enter input :" input
# 输入验证
if ! validAlphaNum "$input" ;then
echo "Your input must consist of only letters and numbers." >&2
exit 1
else
echo "Input is valid."
fi
exit 0
$ ./validalnum
Enter input :coco is handsome
Your input must consist of only letters and numbers.
$ ./validalnum
Enter input :cocoishandsome
Input is valid.
如果除了大写字母之外,还需要空格、逗号、点好,该怎么办
sed 's/[^[:upper:] ,.]//g'
shell 脚本开发存在的一个问题是各种不一致的数据格式。规范数据格式的难度可小可大。数 据格式算是其中最有挑战性的工作之一,这是因为指定日期的方法各种各样。哪怕是提示过特定 的格式,例如按照“月日年”,照样有可能得到不一致的输入:月份没有采用数字,而是用了 月份名称或月份名称缩写,甚至还有全部是大写字母的月份全称。有鉴于此,一个能够规范日期 的函数,尽管本身很基础,却能在后续的脚本编写工作中帮上大忙,尤其是在脚本#7 中。
#!/bin/bash
# normdate -- 将月份规范成3个字母,首字母大写
# 该脚本随后将作为#7的辅助函数
# 如果没有错误,那么以0值退出
monthNumToName(){
# 将变量month设置为相应的值
case $1 in
1 ) month="Jan" ;; 2 ) month="Feb" ;;
3 ) month="Mar" ;; 4 ) month="Apr" ;;
5 ) month="May" ;; 6 ) month="Jun" ;;
7 ) month="Jul" ;; 8 ) month="Aug" ;;
9 ) month="Sep" ;; 10) month="Oct" ;;
11) month="Nov" ;; 11) month="Dec" ;;
*) echo "$0: Unknown numeric month value $1" >&2
exit 1
esac
return 0
}
# 主脚本开始 -- 如果要将该脚本包含到其他脚本之内,那么删除或注释掉一下的所有内容
# ==================
# 输入验证
if [ $# -ne 3 ];then
echo "Usage: $0 month day year" >&2
echo "Formats are August 3 1962 and 8 3 1962" >&2
exit 1
fi
if [ $3 -le 999 ];then # le 小于等于
echo "$0: expected 4-digit year value." >&2
exit 1
fi
# 输入的月份是否为数字?
if [ -z $(echo $1|sed 's/[[:digit:]]//g') ];then # -z 检测是否为空
monthNumToName $1
else
# 规范前3个字母,首字母大写,其余小写
month="$(echo $1|cut -c1|tr '[:lower:]' '[:upper:]')" # -c 以字符为单位进行分割
month="$month$(echo $1|cut -c2-3|tr '[:upper:]' '[:lower:]')"
fi
echo $month $2 $3
exit 0
$ ./normdate 8 3 1000
Aug 3 1000
$ ./normdate 8 3 999
./normdate: expected 4-digit year value.
$ ./normdate AUGUST 03 1962
Aug 03 1962
在你兴奋于能够为该脚本添加大量扩展,使其变得更为复杂之前,先看看脚本#7,它用到了 normdate 来验证输入日期。
可以做出的一处改动是允许脚本接受形如 MM/DD/YYYY 或 MM-DD-YYYY 的日期格式, 将下面的代码添加到第一个条件语句之前
if [ $# -eq 1 ];then # 处理/或-格式
set -- $(echo $1|sed 's/[\/\-]/ /g') # 使用set 命令将用空格分割开的字符串赋值给 $1 $2 ...
fi
程序员常犯的一个错误是在向用户展示计算结果之前没有先格式化数据。如果不在脑海里从 右向左,每隔 3 位插入一个逗号,那么用户很难断定 43245435 有数百万之大。代码清单 1-7 中的 脚本可以很好地实现数字的格式化。
#!/bin/bash
# nicenumber -- 将给定的数字以逗号分割的形式显示出来
# 可接收两个选项:DD (decimal point delimiter,小数分割符)
# 和TD(thousands delimiter,千位分割符)。
# 美化数字显示,如果指定了第二个参数,则将输出回显在stdout
nicenumber(){
# 注意:我们假设'.'是脚本输入值的小数分隔符
# 除非用户使用选项-d指定了其他分割符,负责输出值中的小数分隔符也是'.'
integer=$(echo $1|cut -d. -f1) # 小数分隔符左侧。
decimal=$(echo $1|cut -d. -f2) # 小数分隔符右侧。
# 检查数字是否不为整数。
if [ "$decimal" != "$1" ];then
# 有小数部分,将其保存起来。
result="${DD:= '.'}$decimal" # ${DD:='.'} 如果DD 不会空则返回$DD 为空则返回 .
fi
thousands=$integer
while [ $thousands -gt 999 ];do
remainder=$(( $thousands % 1000 )) # 3 个最低有效数字。
# 我们需要变量remainder中包含3位数字。是否需要添加0?
while [ ${#remainder} -lt 3 ];do # ${#remainder} 获取该变量的长度 如果小于3加入前导数字0
remainder="0$remainder"
done
result="${TD:=','}${remainder}${result}" # 从右向左构建最终结果
thousands=$((thousands/1000)) # 如果有的话,千位分隔符左侧部分
done
nicenum="${thousands}${result}"
if [ ! -z $2 ];then
echo $nicenum
fi
}
DD="." # 小数分隔符,分割整数部分和小数部分
TD="," # 千位分隔符,隔3个数字出现一次
# 主脚本开始
# =============
# getopts配合case来进行操作时有两个隐含变量:一个是OPTARG,用来取当前选项的值,另外一个是OPTIND,代表下一个要处理的元素位置。
while getopts "d:t:" opt;do
case $opt in
d) DD="$OPTARG" ;;
t) TD="$OPTARG" ;;
esac
done
shift $(($OPTIND -1))
# 输入验证
if [ $# -eq 0 ];then
echo "Usage: $(basename $0) [-d c] [-t c] numeric_value"
echo " -d specifies the decimal point delimiter (default '.')"
echo " -t specifies the thousands delimiter (default ',')"
exit 0
fi
nicenumber $1 1
exit 0
$ ./nicenumber 123456789
123,456,789
$ ./nicenumber 123456780
123,456,780
$ ./nicenumber -d, -t. 123456780.12
123.456.780,12
由于不同国家采用的千位分隔符和小数分隔符各不相同,因此我们为该脚本加入了几个灵活 的调用选项。例如,德国和意大利可以使用-d "."和-t “,”,法国可以使用-d ","和-t " ",有 4
种官方语言的瑞士可以使用-d "."和-t “’”。这个例子很好地说明了在某些情况下灵活性要优于 硬编码,以便工具能够服务于尽可能多的用户群体。
另一方面,我们将输入值的小数分隔符硬编码为".",因此如果你认为带有小数部分的输入 值会采用其他的分隔符,那么可以修改和处的 cut 命令,这两处命令目前是指定"."作为小数 分隔符。
integer=$(echo $1 |cut "-d$DD" -f1) # 小数分割符左侧
decimal=$(echo $1 |cut "-d$TD" -f1) # 小数分割符右侧
上述代码没有问题,除非输入中的小数分隔符和指定用于输出的分隔符不一样,因为那样的 话,脚本会悄无声息地运行失败。一种更为复杂的解决方法是在这两行代码前面加入测试,确保 用户请求的小数分隔符和输入中的分隔符一样。可以采用脚本#2 中用过的技巧来实现这种测试: 移除所有的数字,看看还剩下什么
separator="$(echo $!|sed 's/[[:digit:]]//g')"
if [ ! -z "$separator" -a "$separator" != "$DD" ];then # -a 表示 and
echo "$0: Unknown decimal separator $separator encountered." >&2"
exit 1
fi
我们在脚本#2 中已经见到过,验证整数输入可谓是小菜一碟,但如果你也想接受负数的话, 可就没那么容易了。问题在于每个数值只能有一个负号,而且还必须出现在数值的最开始部分。 代码清单 1-9 中的脚本可以确保正确地格式化负数,另外还能检查其值是否位于用户指定的区 间内。
#!/bin/bash
validint(){
# 验证第一个参数并根据最小值$2和/或最大值$3(如果指定的话)进行测试
# 如果第一个参数的值不在指定区间内或者不全是数组组成,那么脚本执行失败
number="$1"; min="$2"; max="$3"
if [ -z $number ];then # 确保不会出现用户不输入任何参数,包括带引号的空字符串
echo "You didn't enter anything.Please enter a number." >&2
return 1
fi
# 第一个字符是否为负号?
if [ "${number%${number#?}}" = "-" ];then
testvalue="${number#?}" # 获取除第一个字符以外的所有字符进行测试。
else
testvalue="$number"
fi
# 删除变量number中的所有数字,以做测试只用
nodigits="$(echo $testvalue |sed 's/[[:digit:]]//g')"
# 检查非数字字符
if [ ! -z $nodigits ];then
echo "Invalid number format! Only digits, no commas, spaces, etc." >&2
return 1
fi
if [ ! -z $min ];then
# 输入值是否小于指定的最小值?
if [ "$number" -lt "$min" ];then
echo "Your value is too small: smallest acceptable value is $min." >&2
return 1
fi
fi
if [ ! -z $max ];then
# 输入值是否大雨指定的最大值?
if [ "$number" -gt "$max" ];then
echo "Your value is too big: largest acceptable value is $max." >&2
return 1
fi
fi
return 0
}
# 验证输入
if validint "$1" "$2" "$3" ;then
echo "Input is a valid integer within your constraints."
fi
$ ./validint 1234.3
Invalid number format! Only digits, no commas, spaces, etc.
$ ./validint 103 1 100
Your value is too big: largest acceptable value is 100.
$ ./validint -17 0 25
Your value is too small: smallest acceptable value is 0.
$ ./validint -17 -20 25
Input is a valid integer within your constraints.
注意,[ “KaTeX parse error: Expected '}', got 'EOF' at end of input: {number%{number#?}}” = “-” ] 处的测试检查用户输入值的第一个字符是否为负号:
if [ "${number%${number#?}}" = "-" ];then
如果第一个字符是负号,就将整数值的数字部分赋给变量 testvalue。然后将其中所有的数 字全部剔除,做进一步测试。
你可能会想用逻辑 AND(-a)连接两个表达式,这样就可以少用一个嵌套 if 语句了。例如, 下面的代码看起来应该没问题:
if [ ! -z $min -a "$number" -lt "$min" ];then
echo "Your value is too small: smallest acceptable value is $min." >&2
exit 1
fi
可惜事与愿违,因为即便 AND 的第一个条件被证明为假,你也无法保证第二个条件不会被 测试(这一点和其他大多数编程语言不同)。这意味着,如果你打算采用这种写法,那么在比较 的时候,可能会碰上由于无效或非预期值所造成的各种 bug。事情本不该如此,但是是你自己要 这么写的