侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

Shell脚本编程(10)之处理选项

zze
zze
2019-12-23 / 0 评论 / 0 点赞 / 713 阅读 / 11746 字

本部分内容参考自《Linux命令行与shell脚本编程大全 第3版》。

如果你已经看过前面的内容,应该就见过了一些同时提供了参数和选项的 bash 命令。 选项是跟在单破折线后面的单个字母,它能改变命令的行为。本节将会介绍 3 种在脚本中处理选项的方法。

查找选项

表面上看,命令行选项也没什么特殊的。在命令行上,它们紧跟在脚本名之后,就跟命令行参数一样。实际上,如果愿意,你可以像处理命令行参数一样处理命令行选项。

处理简单选项

上一篇文章中,你看到了如何使用 shift 命令来依次处理脚本程序携带的命令行参数。你也可以用同样的方法来处理命令行选项。
在提取每个单独参数时,用 case 语句来判断某个参数是否为选项。

$ cat test23.sh 
#!/bin/bash
while [ -n "$1" ]
do
	case "$1" in
		-a) echo "Found the -a option" ;;
		-b) echo "Found the -b option" ;;
		-c) echo "Found the -c option" ;;
		 *) echo "$1 is not an option" ;;
	esac
	shift
done
$ ./test23.sh -c -d
Found the -c option
-d is not an option

case 语句会检查每个参数是不是有效选项。如果是的话,就运行对应 case 语句中的命令。
不管选项按什么顺序出现在命令行上,这种方法都适用。
case 语句在命令行参数中找到一个选项,就处理一个选项。如果命令行上还提供了其他参数,你可以在 case 语句的通用情况处理部分中处理。

分离参数和选项

你会经常遇到想在 shell 脚本中同时使用选项和参数的情况。 Linux 中处理这个问题的标准方式是用特殊字符来将二者分开,该字符会告诉脚本何时选项结束以及普通参数何时开始。
对 Linux 来说,这个特殊字符是双破折线(-- )。 shell 会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作参数,而不是选项来处理了。
要检查双破折线,只要在 case 语句中加一项就行了。

$ cat test24.sh 
#!/bin/bash
while [ -n "$1" ]
do
	case "$1" in
		-a) echo "Found the -a option" ;;
	-b) echo "Found the -b option";;
-c) echo "Found the -c option" ;;
--) shift
	break ;;
*) echo "$1 is not an option";;
esac
shift
done
#
count=1
for param in $@
do
	echo "Parameter #$count: $param"
	count=$[ $count + 1 ]
done

在遇到双破折线时,脚本用 break 命令来跳出 while 循环。由于过早地跳出了循环,我们需要再加一条 shift 命令来将双破折线移出参数变量。

$ ./test24.sh  -a -b -c -- test1 test2 test3
Found the -a option
Found the -b option
Found the -c option
Parameter #1: test1
Parameter #2: test2
Parameter #3: test3

当脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作命令行参数。

处理带值的选项

有些选项会带上一个额外的参数值。在这种情况下,命令行看起来像下面这样。

$ ./testing.sh -a test1 -b -c -d test2

当命令行选项要求额外的参数时,脚本必须能检测到并正确处理。下面是如何处理的例子。

$ ./test -a -b value1 -c
-bash: ./test: No such file or directory
[root@zzehost 1223]# ./test24 -a -b value1 -c
-bash: ./test24: No such file or directory
[root@zzehost 1223]# ./test25.sh -a -b value1 -c
Found the -a option
Found the -b option, with parameter value value1
Found the -c option
$ cat test25.sh 
#!/bin/bash
while [ -n "$1" ]
do
	case "$1" in
		-a) echo "Found the -a option";;
	-b) param="$2"
		echo "Found the -b option, with parameter value $param"
		shift ;;
	-c) echo "Found the -c option";;
--) shift
	break ;;
*) echo "$1 is not an option";;
esac
shift
done
[root@zzehost 1223]# ./test25.sh -a -b value1 -c
Found the -a option
Found the -b option, with parameter value value1
Found the -c option

在这个例子中, case 语句定义了三个它要处理的选项。 -b 选项还需要一个额外的参数值。由于要处理的参数是 $1,额外的参数值就应该位于 $2(因为所有的参数在处理完之后都会被移出)。只要将参数值从 $2 变量中提取出来就可以了。当然,因为这个选项占用了两个参数位,所以你还需要使用 shift 命令多移动一个位置。
只用这些基本的特性,整个过程就能正常工作,不管按什么顺序放置选项(但要记住包含每个选项相应的选项参数)。
现在 shell 脚本中已经有了处理命令行选项的基本能力,但还有一些限制。比如,如果你想将多个选项放进一个参数中时,它就不能工作了。

$ ./test25.sh -ac
-ac is not an option

在 Linux 中,合并选项是一个很常见的用法,而且如果脚本想要对用户更友好一些,也要给用户提供这种特性。幸好,有另外一种处理选项的方法能够帮忙。

使用getopt命令

getopt 命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而在脚本中解析它们时更方便。

命令的格式

getopt 命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。它的命令格式如下:

getopt optstring parameters

optstring 是这个过程的关键所在。它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。
首先,在 optstring 中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。 getopt 命令会基于你定义的 optstring 解析提供的参数。
下面是个 getopt 如何工作的简单例子。

$ getopt ab:cd -a -b test1 -cd test2 test3
 -a -b test1 -c -d -- test2 test3

optstring 定义了四个有效选项字母: abcd。冒号(:)被放在了字母 b 后面,因为 b 选项需要一个参数值。当 getopt 命令运行时,它会检查提供的参数列表( -a -b test1 -cd test2 test3),并基于提供的 optstring 进行解析。注意,它会自动将 -cd 选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数。
如果指定了一个不在 optstring 中的选项,默认情况下, getopt 命令会产生一条错误消息。

$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- 'e'
 -a -b test1 -c -d -- test2 test3

如果想忽略这条错误消息,可以在命令后加 -q 选项。

$ getopt -q ab:cd -a -b test1 -cde test2 test3
 -a -b 'test1' -c -d -- 'test2' 'test3'

注意, getopt 命令选项必须出现在 optstring 之前。现在应该可以在脚本中使用此命令处理命令行选项了。

 在脚本中使用getopt

可以在脚本中使用 getopt 来格式化脚本所携带的任何命令行选项或参数,但用起来略微复杂。
方法是用 getopt 命令生成的格式化后的版本来替换已有的命令行选项和参数。用 set 命令能够做到。
set 命令的选项之一是双破折线(--),它会将命令行参数替换成 set 命令的命令行值。
然后,该方法会将原始脚本的命令行参数传给 getopt 命令,之后再将 getopt 命令的输出传给 set 命令,用 getopt 格式化后的命令行参数来替换原始的命令行参数,看起来如下所示。

set -- $(getopt -q ab:cd "$@")

现在原始的命令行参数变量的值会被 getopt 命令的输出替换,而 getopt 已经为我们格式化好了命令行参数。
利用该方法,现在就可以写出能帮我们处理命令行参数的脚本。

$ cat test26.sh 
#!/bin/bash
set -- $(getopt -q ab:cd "$@")
#
while [ -n "$1" ]
do
	case "$1" in
		-a) echo "Found the -a option" ;;
		-b) param="$2"
			echo "Found the -b option, with parameter value $param"
			shift ;;
		-c) echo "Found the -c option" ;;
		--) shift
			break ;;
		 *) echo "$1 is not an option";;
	esac
	shift
done
#
count=1
for param in "$@"
do
	echo "Parameter #$count: $param"
	count=$[ $count + 1 ]
done

现在如果运行带有复杂选项的脚本,就可以看出效果更好了。

$ ./test26.sh -ac
Found the -a option
Found the -c option

当然,之前的功能照样没有问题。

$ ./test26.sh -ac -b bvalue test1 test2 test3
Found the -a option
Found the -c option
Found the -b option, with parameter value 'bvalue'
Parameter #1: 'test1'
Parameter #2: 'test2'
Parameter #3: 'test3'

现在看起来相当不错了。但是,在 getopt 命令中仍然隐藏着一个小问题。看看这个例子。

$ ./test26.sh -ac -b bvalue 'test1 test2' test3
Found the -a option
Found the -c option
Found the -b option, with parameter value 'bvalue'
Parameter #1: 'test1
Parameter #2: test2'
Parameter #3: 'test3'

getopt 命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。幸而还有另外一个办法能解决这个问题。

 使用更高级的getopts

getopts 命令(注意是复数)内建于 bash shell。它跟近亲 getopt 看起来很像,但多了一些扩展功能。
getopt 不同,前者将命令行上选项和参数处理后只生成一个输出,而 getopts 命令能够和已有的 shell 参数变量配合默契。
每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于 0 的退出状态码。这让它非常适合用于解析命令行所有参数的循环中。
getopts 命令的格式如下:

getopts optstring variable

optstring 值类似于 getopt 命令中的那个。有效的选项字母都会列在 optstring 中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在 optstring 之前加一个冒号。 getopts 命令将当前参数保存在命令行中定义的 variable 中。
optstring 值类似于 getopt 命令中的那个。有效的选项字母都会列在 optstring 中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在 optstring 之前加一个冒号。 getopts 命令将当前参数保存在命令行中定义的 variable 中。
让我们看个使用 getopts 命令的简单例子。

$ cat test27.sh 
#!/bin/bash
while getopts :ab:c opt
do
	case "$opt" in
		a) echo "Found the -a option" ;;
		b) echo "Found the -b option, with value $OPTARG";;
		c) echo "Found the -c option" ;;
		*) echo "Unknown option: $opt";;
	esac
done
$ ./test27.sh -ac -b testValue
Found the -a option
Found the -c option
Found the -b option, with value testValue

while 语句定义了 getopts 命令,指明了要查找哪些命令行选项,以及每次迭代中存储它们的变量名(opt)。
你会注意到在本例中 case 语句的用法有些不同。 getopts 命令解析命令行选项时会移除开头的单破折线,所以在 case 定义中不用单破折线。
getopts 命令有几个好用的功能。对新手来说,可以在参数值中包含空格。

$ ./test27.sh -ac -b 'test1 test2'
Found the -a option
Found the -c option
Found the -b option, with value test1 test2

另一个好用的功能是将选项字母和参数值放在一起使用,而不用加空格。

$ ./test27.sh -acbtestValue
Found the -a option
Found the -c option
Found the -b option, with value testValue

getopts 命令能够从 -b 选项中正确解析出 testValue 值。除此之外, getopts 还能够将命令行上找到的所有未定义的选项统一输出成问号。

$ ./test27.sh -acd
Found the -a option
Found the -c option
Unknown option: ?

optstring 中未定义的选项字母会以问号形式发送给代码。
getopts 命令知道何时停止处理选项,并将参数留给你处理。在 getopts 处理每个选项时,它会将 OPTIND 环境变量值增一。在 getopts 完成处理时,你可以使用 shift 命令和 OPTIND 值来移动参数。

$ cat test28.sh 
#!/bin/bash
while getopts :ab:cd opt
do
	case "$opt" in
		a) echo "Found the -a option" ;;
		b) echo "Found the -b option, with value $OPTARG" ;;
		c) echo "Found the -c option" ;;
		d) echo "Found the -d option" ;;
		*) echo "Unknown option: $opt" ;;
	esac
done
#
shift $[ $OPTIND - 1 ]
#
echo
count=1
for param in "$@"
do
	echo "Parameter $count: $param"
	count=$[ $count + 1 ]
done
$ ./test28.sh -abtestValue -c param1 param2 param3
Found the -a option
Found the -b option, with value testValue
Found the -c option

Parameter 1: param1
Parameter 2: param2
Parameter 3: param3

现在你就拥有了一个能在所有 shell 脚本中使用的全功能命令行选项和参数处理工具。

将选项标准化

在创建 shell 脚本时,显然可以控制具体怎么做。你完全可以决定用哪些字母选项以及它们的用法。
但有些字母选项在 Linux 世界里已经拥有了某种程度的标准含义。如果你能在 shell 脚本中支持这些选项,脚本看起来能更友好一些。
下表显示了Linux中用到的一些命令行选项的常用含义。

选项描述
-a显示所有对象
-c生成一个计数
-d指定一个目录
-e扩展一个对象
-f指定读入数据的文件
-h显示命令的帮助信息
-i忽略文本大小写
-l产生输出的长格式版本
-n使用非交互模式(批处理)
-o将所有输出重定向到指定的输出文件
-q以安静模式运行
-r递归的处理目录和文件
-s以安静模式运行
-v生成详细输出
-x排除某个对象
-y对所有问题回答 yes
0

评论区