接上一篇文章shell编程(七) : [shell基础] 使用结构化命令
前面介绍了shell脚本的输出以及怎么处理这些输出,对于编程语言来说,有输出方法一定也会有输入方法。
命令行参数就是最基本的输入,命令也是脚本,命令通过判断不同的输入命令行参数来执行不同的功能。
shell 脚本通过位置参数获取用户输入的命令行参数,位置参数变量是标准的数字:$0
是程序名,$1
是第1个参数,$2
是第2个参数,一次类推,这里注意,$0、$1、$2...
中的 0、1、2
不是数字而是字符,所以 $10
会被解析为 $1
和 0
,写一个简单的脚本验证一下:
给脚本传了两个参数 a
和 b
,分别对应 $1
和 $2
,就像上面分析的一样,输出为 $1
和 0
的拼接,即 a0
。那要使用第10个参数怎么办呢,必须使用花括号,比如: ${10}
。
输入参数时,每个位置参数用空格隔开,如果需要在参数中包含空格,必须使用引号(单引号或双引号)将该参数括起来。
一个常见的规则,对于方法的入参一定要进行测试(检错),不然会发生意想不到的事情。常见的入参测试就是监测是否为空,前面有介绍到 test
的 -n
参数,它可以监测入参是否为空。另外根据不同逻辑,还需要测试入参的其他属性,比如是否为数字/字符串等等。
bash shell 提供了一些特殊变量用于记录关于命令行参数的信息。
特殊变量 $#
可以存储脚本运行时携带的命令行参数的个数,可以在脚本中任何地方使用这个特殊变量,就像普通变量一样,如下:
有没有发现,其实命令行参数是放在数组中的,元素0存放脚本文件名(命令名),其他元素存放参数,而在脚本中访问这个数组的元素的方法就是使用 $
-> $0、$1、$2
等,可以想到最后一个元素就是 ${$#}
,但实际不是这样的,而是 ${!#}
,如下:
有时脚本需要传入不定个数的参数,要获取这些参数,可以先用 $#
获取参数个数,再遍历所有参数。Linux还提供了两种更简单的方法 $*
和 $@
,$*
变量会将命令行上提供的所有参数当作一个单词保存,$@
变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词,也就是说$*
变量会将所有参数当成单个参数,而 $@
变量会单独处理每个参数,如下:
bash shell提供了另一种遍历命令行参数的方法,shift
命令,很适合在不知道参数个数的情况下使用。
shift 命令会根据它们的相对位置来移动命令行参数。默认情况下它会将每个参数变量向左移动一个位置,即变量 $3
的值会移到 $2
中,变量 $2
的值会移到 $1
中,而变量 $1
的值则会被删除(注意,变量 $0
的值,也就是程序名,不会改变),移动参数的同时,参数个数 $#
会减1,直到 $#
减到 1
,shift
不再移动参数位置,如下:
其中,while
测试$1
是否为空,不为空则继续循环, echo "Parameter #$count = $1"
先显示了 $1
,之后使用 shift
将后面的参数向左移动,每次都显示$1
,直到$1
为空,实现遍历所有命令行参数。
另外,shift
还可以控制移动距离,shift
后接移动的距离参数,如下:
可以看到,死循环了,这是因为 shift
会在参数个数 $#
小于移动距离的时候停止移动参数。
当 $1 = Tina
时,$# = 1
,移动距离为2,所以 shift
不会再移动参数,而 while
的结束条件是 $1
为空,所以 while
永远不会结束。
命令除了可以带参数,也可以带选项,选项是跟在单破折线后面的单个字母,它能改变命令的行为。
同前面读取命令行参数的方法一样,也可以使用 shift
来读取命令行选项,如下:
case 语句会检查每个参数是不是有效选项。如果是的话,就运行对应 case 语句中的命令。
不管选项按什么顺序出现在命令行上,这种方法都适用。
有的选项也会带参数,这种方法也可以处理,如下:
例子中 -b
选项需要一个额外的参数,由于要处理的参数是 $1
,额外的参数值就应该位于 $2
,因为参数占了额外的一位,所以处理完要使用 shift
向后移动一位。如果选项有两个参数,这两个参数就位于 $2
和 $3
,最后就应该使用 shift 2
向后移动两位。
当然,对于带参数的选项,也可以作参数合法性的检测。
日常使用中,经常遇到即带参数又带选项的的命令,这时使用上面的方法就行不通了,需要将参数和选项分开处理,Linux提供符号双破折线 --
来实现这一功能。规定 --
之前是命令的选项,之后是命令的参数,如下:
getopt
就像前面介绍到的, ls -l -h
命令可以简写成 ls -lh
一样,脚本命令选项也应该可以这么简写,但上面的介绍方法就不适用了,所以bash shell提供了 getopt
命令。
getopt 命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。
格式如下:
getopt optstring parameters
optstring
定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。
首先,在 optstring 中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。 getopt 命令会基于你定义的 optstring 解析提供的参数,如下:
getopt abc:d -a -bd -c parm parm1 parm2
-------------------output
-a -b -d -c parm -- parm1 parm2
getopt 自动将输入根据 optstring
部分的定义(即abc:d
)解析成一般形式的选项和参数列表。同样,选项部分和参数部分用 --
隔开,其中 parm
是选项 -c
的参数,parm1
和 parm2
是命令的参数。
值得注意的是,如果输入的参数不符合 optstring
部分的定义,getopt 命令会产生一条错误消息。
getopt abc:d -a -bd -c parm -e parm1 parm2
-------------------output
getopt: invalid option -- 'e'
-a -b -d -c parm -- parm1 parm2
虽然 getopt
生气地输出了错误信息,但它还是把正确的事情做完了,所以我们可以选择使用 getopt
的 -q
选项来忽略输出的错误消息,如下:
getopt -q abc:d -a -bd -c parm -e parm1 parm2
-------------------output
-a -b -d -c parm -- parm1 parm2
注意, 既然 -q
是属于 getopt
命令的,它就得紧跟在 getopt
的后面, optstring
的前面,不然会被后面的命令认错。
在脚本中使用 getopt
命令需要配合 set
命令,格式如下:
set -- $(getopt -q ab:cd "$@")
首先用 getopt
解析传入脚本的选项和参数,再将解析结果传给 set
,其中 --
是set
的选项,它会将脚本的选项和参数替换成 set
命令的参数值。
set
命令可以将它的参数覆盖脚本的位置参数,如下:
虽然脚本的输入的参数为 1 2 3
,但 set
命令将脚本参数替换成了 a b c
。
set -- $(getopt -q ab:cd "$@")
正式利用了这一点,位置参数不能解析类似 -ab
这样的组合选项,但脚本引用选项和参数又必须利用位置参数这一特性,所以先使用 getopt
解析好脚本的选项和参数,再传给 set
覆盖位置参数。其中有一个问题,就是当 set
接收到脚本的诸如 -a
这一的选项时,set
会认为这是set
自己的选项,不会将其覆盖脚本的位置参数,而 --
的作用就是无论set
接收到什么都将其覆盖到脚本的位置参数。如下:
set
后面没有使用 --
选项,结果当遇到 set
没有的选项 -c
时就报错了。可以看到 set
也没有-d
参数,但没有报 -d
的错误,这是因为Linux命令行错误会只报遇到的第一个错误,可以自己尝试一下,把脚本的-c
去掉,就会报-d
的错误了。
这次使用了set
的--
选项,结果没有报错可以正常输出。
但仍然有一个问题,就是getopt
不能解析带空格的参数,只要遇到空格,getopt
就会认为是两个参数,如下:
本想给-b
选项输入值为 hello vistar
的参数,为了让它看起来是一个整体,还想前面介绍echo
一样加了""
,结果于事无补,getopt
将hello vistar
解析成了两个参数,然而getopt
没有解决向这一问题的办法,所以又有了getopts
命令,有说后面的s
是复数,但我觉得是super
,因为getopts
比getopt
厉害了。
getopts
getopt
将命令行的选项和参数处理后只生成一个输出,而 getopts 命令每次调用时,只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。
getopts
命令的格式如下:
getopts optstring variable
optstring
与 getopt
命令的类似,有效的选项字母都会列在 optstring
中,如果选项字母要求有一个参数值,就加一个冒号。
不同的是,要去掉错误消息的话,可以在 optstring
之前加一个冒号。
getopts
命令将当前参数保存在命令行中定义的 variable
中,这个变量名可以自定义,每次调用 getopts
,通过这个变量访问命令行的参数。
getopts
命令会用到两个环境变量。每次调用 getopts
时,如果选项需要跟一个参数, OPTARG
环境变量就会保存这个值,如果选项后面没有参数,该环境变量存储的值为空。
示例如下:
OPTIND
环境变量保存了参数列表中 getopts
正在处理的参数位置,初始值为1。getopts
命令在处理每个选项时,会将 OPTIND
环境变量值增一,在 getopts 完成选项的处理后,可以使用 shift
命令和 OPTIND
值来访问脚本的参数,如下:
其中,var
是-b
的参数,var1
和 var2
是脚本的参数。
另外,getopts
还有几个的特点,
-
,从上面case
语句中的分支中可以看到;在 bash shell 脚本圈子里,有些字母已经有约定俗成的含义,我们在写bash脚本时,遵循这一约定,不仅使自己脚本看起来高级,也有利于别人使用。
一些字母的常用含义如下:
选项 | 描述 |
---|---|
-a | 显示所有对象 |
-c | 计数 |
-d | 指定一个目录 |
-e | 扩张一个对象 |
-f | 指定读入数据的文件 |
-h | 显示命令的帮助信息 |
-i | 忽略文本大小写 |
-l | 产生输出的长格式文本 |
-n | 使用非交互模式(批处理) |
-o | 将所有输出重定向到指定的输出文件 |
-q | 以安静模式运行 |
-r | 递归地处理目录和文件 |
-s | 以安静模式运行 |
-v | 生成详细输出 |
-x | 排除某个对象 |
-y | 对所有问题回答yes |
获取选项和参数只是一种简单的获取用户输入的方法,有时在脚本运行的过程中,为了提高和用户的交互性能,也需要获取用户的输入,bash shell为此提供了 read
命令。
read
命令从标准输入(键盘)或另一个文件描述符中接收输入。在收到输入后, read 命令会将数据放进一个变量,有点像input
函数,如下:
也可以使用 -p
选项(prompt)将输入提示直接放在 read
后面,如下:
read
会将输入的数据都存入变量,但如果需要输入多个数据,并单独处理,可以定义多个变量,read
会按空格分割,将数据存入每个变量中,如果输入的数据比定义的变量个数多,会将多余的数据都存入最后一个变量,如下:
另外,read
也可以不指定变量,这时它会把数据存入环境变量REPLY
中,使用方法和其他变量一样。
在read
等待用户输入时,如果一直等不到输入,会一直等下去,但有时希望如果没有输入要继续往下执行,这是可以使用-t
选项设置超时时间,等待超过这一时间后,read
命令会返回一个非零退出状态码,如下,设置5s超时:
我们在Linux上输入密码时,是不会显示密码的,这个功能可以使用read
的-s
选项实现,如下:
也可以使用read
命令从文件中读取内容,每次调用 read 命令,它都会从文件中读取一行文本。当文件中再没有内容时, read 命令会退出并返回非零退出状态码。
通常使用cat
命令获取文件的全部内容,然后用管道 |
传给 read
命令,如下:
其中,使用count
记录string文件的行号,每循环一次read
会读取一行内容。