使用bash编写Linux shell脚本--参数和子壳
为了成为一个灵活的工具,一个合格的脚本必须提供额外的信息来说明此脚本的作用,如何执行此脚本以及在哪儿执行此脚本。和命令一样脚本也使用参数。开关和参数提高了重用性同时也减少了成本,节省了时间。
定位的参数
有三种有效的方法可以使Linux脚本使用参数。第一种使用定位参数。脚本根据在命令行出现参数的位置调用参数。因为其他两种依赖于定位参数,所以先讨论这个。
Bash变量使用“$0”标示脚本的路径。不必是全路径名,但是它定义了执行脚本所在的路径。
$ printf “%s\n” “$0”
/bin/bash
在这个例子中,Bash会和开始命令/bin/bash。
当参数命令组合了basename命令时,只留下脚本的名字,其余的路径部分被删除了。
一些微缩版本使用Bash的字符串替换功能来避免执行外面的程序。
$ declare -rxSCRIPT=${0##*/}
$ printf “%s\n” “$SCRIPT”
Bash
通过使用“$0”来找到脚本的名字,在脚本被拷贝和重新命名之后,就不会出现错误文件名的潜在威胁了。SCRIPT总是保持这正确的脚本名。
变量“$#”包含有脚本或外壳会话参数的个数。如果没有参数,$#总是0。这个参数没有将脚本名包含在$0中。
$ printf “%d\n” $#
0
前面九个参数放置在变量$1~$9中。(九个之后的参数如果要访问使用大括号)。如果设置了nounset外壳选项,访问一个未定义的参数会产生一个错误,就像未定义变量名一样的错误。
$ printf “%s\n” $9
bash: $9: unboundvariable
变量“$@”或者是“?*”将所有参数作为一个字符串返回。
当使用定位参数时,Bash并不区分它们是参数还是开关,对于脚本来说在命令行的每一个项目作为独立的参数来对待。
考虑一下下面的脚本,显示在列表9.1中:
Listing 9.1 params.sh
#!/bin/bash
#
# params.sh: apositional parameter demonstration
printf “There are %dparameter(s)\n” “$#”
printf “The completelist is %s\n” “$@”
printf “The firstparameter is %s\n” “$1”
printf “The secondparameter is %s\n” “$2”
当运行此脚本并带上参数“-c”和“t2341”,它表示“$1”是“-c”,“$2”是“t2341”。
$ bash parms.sh -c t2341
There are 2 parameter(s)
The complete list is -ct2341
The first parameter is-c
The second parameter ist2341
虽然“$@”和“$*”都表示所有的参数,但是如果他们用双引号封装起来的含义是有所不同的。“$@”根据IFS变量的第一个字符进行分割,如果IFS为空则使用空格,如果IFS没有定义,则不使用任何东西。“$*”将一组参数作为一个单独的组。
“$@”总是使用空格进行分割,并将参数视为一个个单独的项目,即使它们使用双引号包起来也是这样。“$@”通常用于将整个开关集合传输给另一个命令(例如:ls $@)。
虽然定位参数是一个简单的方法来遍历开关和参数,它们并不是总是这样直接遍历参数列表的,有一个内置命令shift,它可以将参数“$1”给丢弃掉,将后面的参数前移一位。使用shift命令,你可以检查每一个参数,就像它们总是第一个参数一样。
列表9.2展示了如何使用shift的完整例子:
Listing 9.2 param2.sh
#!/bin/bash
#
# param2.sh
#
# This script expectsthe switch -c and a company name. --help (-h)
# is also allowed.
shopt -s -o nounset
declare -rxSCRIPT=${0##*/}
# Make sure there is atleast one parameter or accessing $1
# later will be anerror.
if [ $# -eq 0 ] ; then
printf “%s\n” “Type--help for help.”
exit 192
fi
# Process the parameters
while [ $# -gt 0 ] ; do
case “$1” in
-h | --help) # Show help
printf “%s\n” “usage:$SCRIPT [-h][--help] -c companyid”
exit 0
;;
-c ) shift
if [ $# -eq 0 ] ; then
printf “$SCRIPT:$LINENO:%s\n” “company for -c is missing” >&2
exit 192
fi
COMPANY=”$1”
;;
-* ) printf“$SCRIPT:$LINENO: %s\n” “switch $1 not supported” >&2
exit 192
;;
* ) printf“$SCRIPT:$LINENO: %s\n” “extra argument or missing switch” >&2
exit 192
;;
esac
shift
done
if [ -z “$COMPANY” ] ;then
printf “%s\n” “companyname missing” >&2
exit 192
fi
# <-- begin work here
exit 0
最后一个有关的参数是“$_”(美元符号加上下划线)。这个开关有两个作用,首先当外壳脚本首先开始时,它表示为外壳或外壳脚本的路径名,其次,在每个命令执行之后,当前命令被放置在环境变量中。
$ /bin/date
Fri Jun 29 14:39:58 EDT2001
$ printf “%s\n” “$_”
/bin/date
$ date
Fri Jun 29 14:40:04 EDT2001
$ printf “%s\n” “$_”
date
你可以使用“$_”来重复上一次的参数。
getopts命令
使用定位参数有两个限制,首先,他需要编程者自己测试错误并建立相应的消息。其次,shift命令会删除掉所有的参数,如果你想在以后再次访问他们,将是不可能的。
为了处理这些问题。Bash包含了一个内置命令getopts,它可以提取并检查开关而不会弄乱定位参数。意外出现的参数或缺少的参数会重新识别并报告错误。
使用getopts需要坐一些准备工作,首先,你必须定于一个想要使用开关的字符串。通常这个变量称之为OPTSTRING。如果开关需要一个参数,在该开关后加一个冒号。
例如param2.sh需要-h和-c加上公司标识的参数,OPTSTRING是“hc:”。
在选项列表后面还需要第二个参数,该参数保存外壳命令当前使用的参数。
每次getopts运行,命令行的第二个开关将会被检查是否包含在参数列表中,并将名字保存在变量SWITCH中。下一个要检查的参数的位置称之为 OPTING。如果它不存在,OPTING在第一个脚本参数检查之前自动设置为1。如果有参数,他被保存在变量OPTARG中。列表9.3展示一个脚步, 它会测试脚本的第一个参数。
Listing 9.3 getopts.sh
#!/bin/bash
#
# getopts.sh
declare SWITCH
getopts “hc:” SWITCH
printf “The first switchis SWITCH=%s OPTARG=%s OPTIND=%s\n” \
“$SWITCH” “$OPTARG”“$OPTIND”
在这个脚本中,未知的开关被分配一个问号给SWITCH变量,并显示一条错误信息。
$ bash getopts.sh -h
The first switch isSWITCH=h OPTARG= OPTIND=2
$ bash getopts.sh -c a4327
The first switch isSWITCH=c OPTARG=a4327 OPTIND=3
$ bash gettopts.sh -a
t.sh: illegal option --a
The first switch isSWITCH=? OPTARG= OPTIND=1
错误信息可以在开关列表的第一字符前加一个冒号进行隐藏,通过使用“:hc:”,使用错误开关-a时就不会显示错误了,但是该错误开关会被保存在OPTARG中,以便自定义错误信息用。
$ bash getopts.sh -a
The first switch isSWITCH=? OPTARG=a OPTIND=1
你也可以通过建立OPTERR变量并赋值为0来隐藏错误消息。它将被合法的开关字符串所覆盖掉。
开关通常使用while和case语句进行检查,请看列表9.4:
Listing 9.4 getopts_demo.sh
# getopts_demo.sh
#
# This script expectsthe switch -c and a company name. --help (-h)
# is also allowed.
shopt -s -o nounset
declare -rxSCRIPT=${0##*/}
declare -rOPTSTRING=”hc:”
declare SWITCH
declare COMPANY
# Make sure there is atleast one parameter
if [ $# -eq 0 ] ; then
printf “%s\n” “Type--help for help.”
exit 192
fi
# Examine individualoptions
while getopts“$OPTSTRING” SWITCH ; do
case $SWITCH in
h) printf “%s\n” “usage:$SCRIPT [-h] -c companyid”
exit 0
;;
c) COMPANY=”$OPTARG”
;;
\?) exit 192
;;
*) printf“$SCRIPT:$LINENO: %s\n” “script error: unhandled argument”
exit 192
;;
esac
done
printf “$SCRIPT: %s\n”“Processing files for $COMPANY...”
This script is shorterthan the positional
这个脚本比定位参数的脚本更短,如果getopts出错,switch语句会不运行。
作为一个特定的情况,如果提供getopts命令作为一个额外的参数,getopts能够处理这些变量而不是脚本参数,这样可以使用特定的参数来测试开关。
getopt命令
虽然getopts命令使得脚本的编程稍微容易点,但是它没有遵循Linux开关标准,特别是getopts不允许使用双减号长开关。
为了绕开这个限制,Linux包含了它自己的getopt命令(注意不是前面的getopts)。同getopts的作用类似,但是getopt可以使用长开关并具有一些getopts没有的特性。它在脚本中以一种完全不同的方法使用。
因为getopt是一个外部命令,它不能像想getopts那样将开关保存在变量中。它没有办法将环境变量输出回给脚本。
同样,getopt不知道外壳脚本有哪些开关,除非使用“$@”命令将开关复制给getopt命令。最终,getopt不是使用循环,而是将所有的参数作为单独的一个组进行一次性处理。
如同getopts,getopt使用OPTSTRING的列表选项,这个列表可以使--options(-o)引导,以便使系统清楚后面是开关的列表,开关可以使用逗号进行分割。
传递给脚本的选项表必须使用双减号和“$@”追加给getopt命令。双减号表明getopt开关结束的地方和脚本开始的地方。
列表9.5展示的脚本是使用getopt命令完成getopts.sh一样的功能。注意--name(或者-n)开关用于将脚本的名字传递给getopt命令用在任何错误的消息中。
Listing 9.5 getopt.sh
#!/bin/bash
#
#getopt.sh – ademonstration of getopt
declare -rxSCRIPT=${0##*/}
declare RESULT
RESULT=’getopt --name“$SCRIPT” --options “-h, -c:” -- “$@”’
printf “status code=$?result=\”$RESULT\”\n”
下面是运行程序的结果:
$ bash getopt.sh -h
status code=0 result=”-h --”
$ bash getopt.sh -c
getopt.sh: optionrequires an argument -- c
status code=1 result=”--”
$ bash getopt.sh -x
getopt.sh: invalidoption -- x
status code=1 result=”--”
状态码(status code)表明运行结果是否成功。状态码为1,表示getopt显示错误信息。状态码为2表示给getopt命令的选项有问题。
长开关使用--longoptions(或者-l)。它包含逗号分隔的长选项列表。例如:允许使用--help则使用下面的语法:
RESULT=’getopt--name “$SCRIPT” --options “-h, -c:” --longoptions “help” -- “$@”’
getopt还有一个增强。为了给一个长选项指定一个选项参数,增加一个等号和参数名。
如果双冒号跟着开关名,它表明该开关是一个可选的参数而不是必需使用的。如果POSIXLY_COMPATIBLE变量存在,选项表以“+”开始。开关不允许使用参数且第一个参数作为开关项目的结束。
如果GETOPT_COMPATIBLE外壳变量存在,getopt的行为更新C语言标准库中的getopt。一些老版本中的getopt将这种行 为作为缺省值。如果你需要检查这种行为,使用--test(或者-T)开关来测试它的C语言兼容模式:如果不是在兼容模式,状态码返回4。
在getopt命令检查完开关后要做什么呢?它们使用set命令来替换原始参数。
evalset – “$RESULT”
现在参数可以使用定位参数检查也可以使用内置的getopts检查,如列表9.6所示:
Listing 9.6 getopt_demo.sh
#!/bin/bash
#
# getopt_demo.sh
#
# This script expects the switch -c and a companyname. --help (-h)
# is also allowed.
shopt -s -o nounset
declare -rx SCRIPT=${0##*/}
declare -r OPTSTRING=”-h,-c:”
declare COMPANY
declare RESULT
# Check getopt mode
getopt -T
if [ $? -ne 4 ] ; then
printf “$SCRIPT: %s\n” “getopt is in compatibilitymode” >&2
exit 192
fi
# Test parameters
RESULT=’getopt --name “$SCRIPT” --options “$OPTSTRING”\
--longoptions “help” \ -- “$@”’
if [ $? -gt 0 ] ; then
exit 192
fi
# Replace the parameters with the results of getopt
eval set -- “$RESULT”
# Process the parameters
while [ $# -gt 0 ] ; do
case “$1”in
-h | --help) # Show help
printf “%s\n” “usage: $SCRIPT [-h][--help] -ccompanyid”
exit 0
;;
-c ) shift
if [ $# -eq 0 ] ; then
printf “$SCRIPT:$LINENO: %s\n” “company for -c ismissing” >&2
exit 192
fi
COMPANY=”$1”
;;
esac
shift
done
if [ -z “$COMPANY” ] ; then
printf “%s\n” “company name missing” >&2
exit 192
fi
printf “$SCRIPT: %s\n” “Processing files for$COMPANY...”
# <-- begin work here
exit 0
看上去好像多做了许多工作,但是当脚本有许多复杂的开关时,getopt使得处理参数变得更容易些。
还有一些特殊的开关,--alternative(或者-a)开关允许长选项使用一个单独的减号作为前导字符。使用这个开关违背了Linux协议约 定。--quiet-output(或者-Q)可以在检查完后不返回已处理列表给标准输出设备。--quiet(或者-q)表明只返回状态码不返回任何错 误信息,以便你定义自己的错误信息。--shell开关使用引号来保护特定字符。例如空格等。它也许是外壳处理这些字符的一种特殊的方法(只有在C语言兼 容模式才有用)。
子外壳(subshell)
第七章中“复合命令”提到的一组命令可以使用大括号组合在一起。这些命令就像被分配给了一个组,而且只返回一个状态码。
$ { sleep 5 ; printf “%s\n” “Slept for 5 seconds” ;}
休眠5秒。
子外壳是使用小括号包含起来的一组命令。和命令组不同,如果子外壳单独占用一行,最后一个命令不需要使用分号。
$ ( sleep 5 ; printf “%s\n” “Slept for 5 seconds” )
休眠5秒。
子外壳就像使用括号括起来的命令组和独立脚本的混合体。象命令组一样它返回单独的状态码,象独立的外壳脚本,它有自己的环境变量。
$ declare -ix COUNT=15
$ { COUNT=10 ; printf “%d\n” “$COUNT” ; }
10
$ printf “%d\n” “$COUNT”
10
$ ( COUNT=20 ; printf “%d\n” “$COUNT” )
20
$ printf “%d\n” “$COUNT”
10
在这个示例中,命令组可以改变变量COUNT的值,而在子外壳中,没有改变COUNT的值,因为子外壳中的COUNT是父外壳中COUNT的一个副本,其值的变更不影响父外壳中值。
子外壳通常用于管道的连接。使用管道命令的结果可以重定向到子外壳中处理。这些数据似乎就是子外壳的标准输入,如列表9.7所示:
Listing 9.7 subshell.sh
#!/bin/bash
#
# subshell.sh
#
# Perform some operation to all the files in adirectory
shopt -s -o nounset
declare -rx SCRIPT=${0##*/}
declare -rx INCOMING_DIRECTORY=”incoming”
ls -1 “$INCOMING_DIRECTORY” |
(
while read FILE ; do
printf “$SCRIPT: Processing %s...\n” “$FILE”
# <-- do something here
done
)
printf “Done\n”
exit 0
read命令一次从标准输入读入一行,在本实例中,它读取有ls命令建立的一个文件列表。
$ bashsubshell.sh
subshell.sh: Processing alabama_orders.txt...
subshell.sh: Processing new_york_orders.txt...
subshell.sh: Processing ohio_orders.txt...
Done
子外壳不仅仅继承了环境变量,更详细的内容参见第14章“函数和脚本的执行”。
参数处理大大的增加了脚本使用的灵活性,子外壳命令是一个不可缺少的工具。但是在脚本真正的做到完美还有许多基础知识需要掌握。没有作业控制和信号处理的脚本仍不能称之为完美无缺。
命令参考
getopt命令开关
--longoptions(or -l)—期望长选项使用逗号分隔的列表。
--alternative(or -a)—允许长选项只使用一个单独的减号引导。
--quiet-output(or -Q)—检查开关并不将处理结果返回到标准输出中。
--quiet (or -q)—任何错误都不显示出错信息。
--shell (or -u)—使用引号来保护特定字符。
--test ( or -T)—用于C语言兼容性的测试。
微信公众号:
猿人谷
如果您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】
如果您希望与我交流互动,欢迎关注微信公众号
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。