使用Bash编写Linux Shell脚本-9. 参数和子壳

时间:2021-02-04 08:47:35

为了成为一个灵活的工具,一个合格的脚本必须提供额外的信息来说明此脚本的作用,如何执行此脚本以及在哪儿执行此脚本。和命令一样脚本也使用参数。开关和参数提高了重用性同时也减少了成本,节省了时间。

定位的参数

有三种有效的方法可以使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语言兼容性的测试。