本节将介绍一些 bash 中的参数扩展。当然,您仍然拥有 Linux 或 UNIX 命令(如 sed 或 awk)的全部功能来执行更复杂的工作,但是您也应该了解如何使用 shell 扩展。
我们开始使用上述的选项分析和参数分析函数来构建一个脚本。清单 7 中给出了 testargs.sh 脚本。
清单 7. testargs.sh 脚本
#!/bin/bash
showopts () {
while getopts ":pq:" optname
do
case "$optname" in
"p")
echo "Option $optname is specified"
;;
"q")
echo "Option $optname has value $OPTARG"
;;
"?")
echo "Unknown option $OPTARG"
;;
":")
echo "No argument value for option $OPTARG"
;;
*)
# Should not occur
echo "Unknown error while processing options"
;;
esac
done
return $OPTIND
}
showargs () {
for p in "$@"
do
echo "[$p]"
done
}
optinfo=$(showopts "$@")
argstart=$?
arginfo=$(showargs "${@:$argstart}")
echo "Arguments are:"
echo "$arginfo"
echo "Options are:"
echo "$optinfo"
尝试运行几次这个脚本,看看它如何运作,然后再对它进行详细考察。清单 8 给出了一些样例输出。
清单 8. 运行 testargs.sh 脚本
[ian@pinguino ~]$ ./testargs.sh -p -q qoptval abc "def ghi"
Arguments are:
[abc]
[def ghi]
Options are:
Option p is specified
Option q has value qoptval
[ian@pinguino ~]$ ./testargs.sh -q qoptval -p -r abc "def ghi"
Arguments are:
[abc]
[def ghi]
Options are:
Option q has value qoptval
Option p is specified
Unknown option r
[ian@pinguino ~]$ ./testargs.sh "def ghi"
Arguments are:
[def ghi]
Options are:
注意这些参数与选项是如何分开的。showopts 函数像以前一样分析选项,但是使用 return 语句将 OPTIND 变量的值返回给调用语句。调用处理将这个值指派给变量 argstart。然后使用这个值来选择原始参数的子集,原始参数包括那些没有被作为选项处理的参数。这个过程使用参数扩展
${@:$argstart} 来完成。
记住在这个表达式的两侧使用引号使参数和嵌入空格保持在一起,如清单 2 后部所示。
如果您是使用脚本和函数的新手,请记住以下说明:
return 语句返回 showopts 函数的 exit 值,调用方使用 $? 来访问该函数。您也可以将函数的返回值和 test 或 while 之类的命令结合使用来控制分支和循环。
Bash 函数包括一些可选的词 —— “函数”,例如:
function showopts ()
这不是 POSIX 标准的一部分,不受 dash 之类的 shell 的支持,因此如果您要使用它,就不要将 shebang 行设为
#!/bin/sh
因为这会给您提供系统的默认 shell,而它可能不按您希望的方式运行。
函数输出,例如此处两个函数的 echo 语句所产生的输出,不会被打印出来,但是调用方可以访问该输出。如果没有将该输出指派给一个变量,或者没有在别的地方将它用作调用语句的一部分,则 shell 将会尝试执行输出而不是显示输出。
子集和子字符串
此扩展的一般形式为 ${PARAMETER:OFFSET:LENGTH},其中的 LENGTH 参数是可选参数。因此,如果您只希望选择脚本参数的特定子集,则可以使用完整版本来指定要选择多少个参数。例如,${@:4:3} 引用从参数 4 开始的 3 个参数,即参数 4、5 和 6。您可以使用此扩展来选择除那些使用 $1 到 $9 可立即访问的参数之外的单个参数。${@:15:1} 是一种直接访问参数 15 的方法。
您可以将此扩展与单个参数结合使用,也可以与 $* 或 $@ 表示的整个参数集结合使用。在本例中,参数被作为一个字符串及引用偏移量和长度的数字来处理。例如,如果变量 x 的值为 “some value”,则
${x:3:5}
的值就是 “e val”,如清单 9 所示。
清单 9. shell 参数值的子字符串
[ian@pinguino ~]$ x="some value"
[ian@pinguino ~]$ echo "${x:3:5}"
e val
长度
您已经知道 $# 表示参数的数量,而 ${PARAMETER:OFFSET:LENGTH} 扩展适用于单个参数以及 $* 和 $@,因此,可以使用一个类似的结构体 ${#PARAMETER} 来确定单个参数的长度也就不足为奇了。清单 10 中所示的简单的 testlength 函数阐明了这一点。自己去尝试使用它吧。
清单 10. 参数长度
[ian@pinguino ~]$ testlength () { for p in "$@"; do echo ${#p};done }
[ian@pinguino ~]$ testlength 1 abc "def ghi"
1
3
7
模式匹配
参数扩展还包括了一些模式匹配功能,该功能带有在文件名扩展或 globbing 中使用的通配符功能。注意:这不是 grep 使用的正则表达式匹配。
表 2. Shell 扩展模式匹配 扩展目的
${PARAMETER#WORD}shell 像文件名扩展中那样扩展 WORD,并从 PARAMETER 扩展后的值的开头删除最短的匹配模式(若存在匹配模式的话)。使用 ‘@’ 或 ‘$’ 即可删除列表中每个参数的模式。
${PARAMETER##WORD}导致从开头删除最长的匹配模式而不是最短的匹配模式。
${PARAMETER%WORD}shell 像文件名扩展中那样扩展 WORD,并从 PARAMETER 扩展后的值末尾删除最短的匹配模式(若存在匹配模式的话)。使用 ‘@’ 或 ‘$’ 即可删除列表中每个参数的模式。
${PARAMETER%%WORD}导致从末尾删除最长的匹配模式而不是最短的匹配模式。
${PARAMETER/PATTERN/STRING}shell 像文件名扩展中那样扩展 PATTERN,并替换 PARAMETER 扩展后的值中最长的匹配模式(若存在匹配模式的话)。为了在 PARAMETER 扩展后的值开头匹配模式,可以给 PATTERN 附上前缀 #,如果要在值末尾匹配模式,则附上前缀 %。如果 STRING 为空,则末尾的 / 可能被忽略,匹配将被删除。使用 ‘@’ 或 ‘$’ 即可对列表中的每个参数进行模式替换。
${PARAMETER//PATTERN/STRING}对所有的匹配(而不只是第一个匹配)执行替换。
清单 11 给出了模式匹配扩展的一些基本用法。
清单 11. 模式匹配示例
[ian@pinguino ~]$ x="a1 b1 c2 d2"
[ian@pinguino ~]$ echo ${x#*1}
b1 c2 d2
[ian@pinguino ~]$ echo ${x##*1}
c2 d2
[ian@pinguino ~]$ echo ${x%1*}
a1 b
[ian@pinguino ~]$ echo ${x%%1*}
a
[ian@pinguino ~]$ echo ${x/1/3}
a3 b1 c2 d2
[ian@pinguino ~]$ echo ${x//1/3}
a3 b3 c2 d2
[ian@pinguino ~]$ echo ${x//?1/z3}
z3 z3 c2 d2
整合
在介绍其余要点之前,先来观察一下参数处理的实际示例。我构建了 developerWorks 作者程序包(有关 Linux 系统使用 bash 脚本的信息,请参阅 参考资料)。我们将所需的各种文件存储在 developerworks/library 库的子目录中。该库的最新发行版是 5.7 版,因此,模式文件位于 developerworks/library/schema/5.7 中、XSL 文件位于 developerworks/library/xsl/5.7 中,而示例模板则位于 developerworks/library/schema/5.7/templates 中。很明显,一个提供版本(本例中为 5.7)的参数即可满足脚本构建指向所有这些文件的路径的需要。因此脚本获取的 -v 参数必须有值。稍后对这个参数执行验证,方法是构建路径然后使用 [ -d "$pathname" ] 检查它是否存在。
这种方法对产品构建而言非常有效,但是在开发期间,文件被存储在不同的目录中:
developerworks/library/schema/5.8/archive/test-5.8/merge-0430
developerworks/library/xsl/5.8/archive/test-5.8/merge-0430 和
developerworks/library/schema/5.8/archive/test-5.8/merge-0430/templates-0430
这些目录中的当前版本为 5.8,0430 则表示最新测试版本的日期。
为了处理这一情况,我添加了一个参数 -p,它包含了一段补充的路径信息 —archive/test-5.8/merge-0430。现在,我(或者别的什么人)可能忘记了前导斜杠或末尾斜杠,而一些 Windows 用户可能使用反斜杠而不是正斜杠,因此我决定在脚本中对此进行处理。另外,您还注意到指向模板目录的路径包含了两次日期,因此需要在运行时设法摘除日期 0430。
清单 12 给出了用来处理两个参数和根据这些需求清理部分路径的代码。-v 选项的值存储在 ssversion 变量中,清理后的 -p 变量存储在 pathsuffix 中,而日期(连同前导连字符)则存储在 datesuffix 中。注释解释了每一步执行的操作。即使在这样一小段脚本中,您也可以找到一些参数扩展,包括长度、子字符串、模式匹配和模式替换。
清单 12. 分析用于 developerWorks 作者程序包构建的参数
while getopts ":v:p:" optval "$@"
do
case $optval in
"v")
ssversion="$OPTARG"
;;
"p")
# Convert any backslashes to forward slashes
pathsuffix="${OPTARG//\\//}"
# Ensure this is a leading / and no trailing one
[ ${pathsuffix:0:1} != "/" ] && pathsuffix="/$pathsuffix"
pathsuffix=${pathsuffix%/}
# Strip off the last hyphen and what follows
dateprefix=${pathsuffix%-*}
# Use the length of what remains to get the hyphen and what follows
[ "$dateprefix" != "$pathsuffix" ] && datesuffix="${pathsuffix:${#dateprefix}}"
;;
*)
errormsg="Unknown parameter or option error with option - $OPTARG"
;;
esac
done
像 Linux 中的大多数内容一样,也许通常对编程而言,这并非解决此问题的惟一解决方案,但它确实展示了您了解的扩展的一种更实际的用法。
默认值
在上一节中您已经了解了如何为 ssversion 或 pathsuffix 之类的变量指派选项值。在这种情况下,稍后将检测到空值,产品构建时会出现空路径后缀,因此这是可以接受的。如果需要为尚未指定的参数指派默认值怎么办?表 3 所示的 shell 扩展将帮助您完成这个任务。
表 3. 默认值相关的 Shell 扩展 扩展目的
${PARAMETER:-WORD}如果 PARAMETER 没有设置或者为空,则 shell 扩展 WORD 并替换结果。PARAMETER 的值没有更改。
`${PARAMETER:=WORD}如果 PARAMETER 没有设置或者为空,则 shell 扩展 WORD 并将结果指派给 PARAMETER。这个值然后被替换。不能用这种方式指派位置参数或特殊参数的值。
${PARAMETER:?WORD}如果 PARAMETER 没有设置或者为空,shell 扩展 WORD 并将结果写入标准错误中。如果没有 WORD 则写入一条消息。如果 shell 不是交互式的,则表示存在这个扩展。
${PARAMETER:+WORD}如果 PARAMETER 没有设置或者为空,则不作替换。否则 shell 扩展 WORD 并替换结果。
清单 13 演示了这些扩展以及它们之间的区别。
清单 13. 替换空变量或未设置的变量。
[ian@pinguino ~]$ unset x;y="abc def"; echo "/${x:-’XYZ’}/${y:-’XYZ’}/$x/$y/"
/’XYZ’/abc def//abc def/
[ian@pinguino ~]$ unset x;y="abc def"; echo "/${x:=’XYZ’}/${y:=’XYZ’}/$x/$y/"
/’XYZ’/abc def/’XYZ’/abc def/
[[ian@pinguino ~]$ ( unset x;y="abc def"; echo "/${x:?’XYZ’}/${y:?’XYZ’}/$x/$y/" )\
> >so.txt 2>se.txt
[ian@pinguino ~]$ cat so.txt
[ian@pinguino ~]$ cat se.txt
-bash: x: XYZ
[[ian@pinguino ~]$ unset x;y="abc def"; echo "/${x:+’XYZ’}/${y:+’XYZ’}/$x/$y/"
//’XYZ’//abc def/
传递参数
关于参数传递有一些微妙之处,如果不小心,可能会犯错误。您已经了解了使用引号的重要性以及引号对使用 $* 和 $@ 的影响,但是考虑以下的例子。假设您想要一个脚本或函数来操作当前工作目录中的所有文件或目录。为了说明这个例子,考虑清单 14 所示的 ll-1.sh 和 ll-2.sh 脚本。
清单 14. 两个示例脚本
#!/bin/bash
# ll-1.sh
for f in "$@"
do
ll-2.sh "$f"
done
#!/bin/bash
ls -l "$@"
脚本 ll-1.sh 只是将它的每个参数依次传递给脚本 ll-2.sh 而 ll-2.sh 执行传递的参数的一个长目录清单。我的测试目录包含了两个空文件 “file1” 和 “file 2”。清单 15 显示了脚本的输出。
清单 15. 运行脚本 - 1
[ian@pinguino test]$ ll-1.sh *
-rw-rw-r-- 1 ian ian 0 May 16 15:15 file1
-rw-rw-r-- 1 ian ian 0 May 16 15:15 file 2
到目前为止,一切进展得还不错。但是如果您忘记使用 * 参数,则脚本不会执行任何操作。它不会像 ls 命令那样自动执行当前工作目录的内容。可以做一个简单的修正,当没有给 ll1-sh 提供数据时为 ll-1.sh 中的这个条件添加检查并使用 ls 命令的输出来生成 ll-2.sh 的输入。清单 16 给出了一个可能的解决方案。
清单 16. 修正后的 ll-1.sh
#!/bin/bash
# ll-1.sh - revision 1
for f in "$@"
do
ll-2.sh "$f"
done
[ $# -eq 0 ] && for f in "$(ls)"
do
ll-2.sh "$f"
done
注意:我们小心地将 ls 命令的结果用引号引用起来,确保可以正确地处理 “file 2”。清单 17 给出了运行带 * 和不带 * 的新 ll-1.sh 的结果。
清单 17. 运行脚本 - 2
[ian@pinguino test]$ ll-1.sh *
-rw-rw-r-- 1 ian ian 0 May 16 15:15 file1
-rw-rw-r-- 1 ian ian 0 May 16 15:15 file 2
[ian@pinguino test]$ ll-1.sh
ls: file1
file 2: No such file or directory
很奇怪吧?当您传递参数时,尤其当这些参数是命令的输出时,处理起来可能需要些技巧。错误消息提示文件名被换行符分隔,这就给我们提供了线索。有很多种方法可以处理这个问题,但是有一种简单的方法就是,使用清单 18 所示的内置 read。自己可以试用一下。
清单 17. 运行脚本 - 2
#!/bin/bash
# ll-1.sh - revision 2
for f in "$@"
do
ll-2.sh "$f"
done
[ $# -eq 0 ] && ls | while read f
do
ll-2.sh "$f"
done
这个例子的目的就是要说明:注意细节并使用一些不常见的输入来进行测试可以使脚本更加可靠。祝您编程顺利! .
TAG: bash 参数