命令行参数
$0 表示程序名。
$1 至 9则是位置参数。# 表示参数的个数。
\* 将所有参数当做一个整体来引用 @ 把每个参数作为一个字符串返回,可以使用for循环来遍历
? 最近一个执行的命令的退出状态。0表示执行成功_ 上一个命令的最后一个参数。使用快捷键 ESC+. 也是这个效果
位置参数
位置参数不止9个,更多的参数也是一样支持的。只是要使用${10}这样的形式引用。
$1 和 ${1}的效果是一样的。
不用花括号的话,$10 会被认为是 $1 和一个字符 0。
带空格的参数值
每个参数都是用空格分隔的。要在参数值中包含空格,必须要使用引号(单引号或双引号都可)。
将文本字符串作为参数传递时,引号并非数据的一部分。它们只是表明数据的起止位置。
获取脚本名
$0 表示脚本名,但是不同的调用方法返回的结果也是不同的。下面的脚本就是简单的打印$0的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ cat filename.sh #!/bin/bash echo $0 $ ./ filename.sh -bash: ./: 是一个目录 $ cat filename.sh #!/bin/bash echo $0 $ ./filename.sh ./filename.sh $ bash filename.sh filename.sh $ bash /root/filename.sh /root/filename.sh |
使用 basename 命令
如果要使用脚本名称来进行判断,可以先用命令 basename 把路径的信息给过滤掉。命令的效果如下:
1 2 |
$ basename /var/log/messages messages |
所以上面的脚本可以修改成这样:
1 2 3 4 5 6 7 8 9 |
$ cat filename.sh #!/bin/bash echo $(basename $0) $ ./filename.sh filename.sh $ bash filename.sh filename.sh $ bash /root/filename.sh filename.sh |
测试参数
在脚本中使用参数要确保参数存在,否则运行时有可能会报错:
1 2 3 4 5 6 7 |
$ cat add.sh #!/bin/bash echo $1 + $2 = $[ $1 + $2 ] $ ./add.sh 1 2 1 + 2 = 3 $ ./add.sh 1 ./add.sh:行2: 1 + : 语法错误: 期待操作数 (错误符号是 "+ ") |
如果只是当做字符串引用,也不会报错。没有传参的参数默认都是空:
1 2 3 4 5 6 7 8 9 |
$ cat hello.sh #!/bin/bash echo Hello $1 $2. $ ./hello.sh Tom Jerry Hello Tom Jerry. $ ./hello.sh Jerry Hello Jerry . $ ./hello.sh Hello . |
判断参数是否存在
在 shell 中利用 -n 来判定字符串非空,-z 则正好相反,空即是真。上面已经测试过了,未定义的参数默认是空:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ cat hello.sh #!/bin/bash if [ -n "$1" ] then echo Hello $1. else echo Hello Nobody. fi $ ./hello.sh Tom Hello Tom. $ ./hello.sh Hello Nobody. |
这里的判断的 $1 要加上双引号,否则会被认为是字符串。一个字符串当然非空,所以结果会永远为真。
判断参数的个数
上面的例子的脚本也可以通过判断参数数量是否大于0来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ cat hello.sh #!/bin/bash echo 参数数量: $# if [ $# -gt 0 ] then echo Hello $1. else echo Hello Nobody. fi $ ./hello.sh 参数数量: 0 Hello Nobody. $ ./hello.sh Tom 参数数量: 1 Hello Tom. $ ./hello.sh Tom Jerry 参数数量: 2 Hello Tom. $ |
这里 -gt 比较的是前后两个数字(INT),所以$#是不加引号的。
用这种方法也能判断参数是否存在。两种方法,效果一样,不同的书上都看到有人使用。
这里是一样加法的例子,必须要传入2个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ cat add.sh #!/bin/bash if [ $# -eq 2 ] then echo $1 + $2 = $[ $1 + $2 ] else echo 需要参数: 2, 实际参数: $#. fi $ ./add.sh 1 2 1 + 2 = 3 $ ./add.sh 1 2 3 需要参数: 2, 实际参数: 3. $ ./add.sh 1 需要参数: 2, 实际参数: 1. |
如果要表示不相等,就是 if [ $# -ne 2 ]
获取最后一个参数
这是一个使用 $# 的小技巧。使用${$#}
似乎就是参数的最后一个变量了。
但是其实不然,花括号里不能这样用$,这里要把里面的换成感叹号:
1 2 3 4 5 6 7 8 9 10 |
$ cat hello.sh #!/bin/bash if [ $# -gt 0 ] then echo Hello ${!#}. else echo Hello Nobody. fi $ ./hello.sh Tom Jerry Hello Jerry. |
如果没有任何命令行参数,那么就是返回$0,也就是脚本名。
上面感叹号的问题,效果是引用变量的值而不是变量自身。类似于指针的取值。把#号换成一个有名字的变量来说明比较直观:
1 2 3 4 5 6 7 8 9 10 11 |
$ cat parameter.sh #!/bin/bash paramater=key key=value echo "${paramater}" echo "${!paramater}" echo "${key}" $ ./parameter.sh key value value |
不加感叹号,就是直接去该变量的值。加上感叹号,就是去变量值所对应的变量名的那个变量的值。
获取所有参数
$* 和 $@ 都是表示所有的字符串,但是在遍历的时候会有区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ cat all.sh #!/bin/bash echo '$* 的效果:' count=1 for i in "$*" do echo $count: $i count=$[ $count + 1 ] done echo '$@ 的效果:' count=1 for i in "$@" do echo $count: $i count=$[ $count + 1 ] done $ ./all.sh Oliver Barry Kara Sara Kane $* 的效果: 1: Oliver Barry Kara Sara Kane $@ 的效果: 1: Oliver 2: Barry 3: Kara 4: Sara 5: Kane |
$*就一个整体的值,无法遍历。要遍历每一个变量要使用$@。这里的双引号很重要。
不加引号的话,就是把 $* 和 $@ 的内容(变量解析后就是多个词)传递给for循环遍历,这样两个参数的效果是一样的。和直接传不加引号的字符串的效果一样。
加上引号,引号里的内容就是一个整体。如果是$*,这个整体里的所有内容还是一个词,不会拆。如果是$@,这个整体里会按空格拆分成多个词。
下面是演示的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
$ cat all2.sh #!/bin/bash echo '$* 不加引号的效果:' count=1 for i in $* do echo $count: $i count=$[ $count + 1 ] done echo '$@ 不加引号的效果:' count=1 for i in $@ do echo $count: $i count=$[ $count + 1 ] done echo '直接遍历不加引号的字符的效果:' count=1 for i in Oliver Barry Kara Sara Kane do echo $count: $i count=$[ $count + 1 ] done echo '加引号遍历的效果:' count=1 for i in "Oliver Barry Kara Sara Kane" do echo $count: $i count=$[ $count + 1 ] done $ ./all2.sh Oliver Barry Kara Sara Kane $* 不加引号的效果: 1: Oliver 2: Barry 3: Kara 4: Sara 5: Kane $@ 不加引号的效果: 1: Oliver 2: Barry 3: Kara 4: Sara 5: Kane 直接遍历不加引号的字符的效果: 1: Oliver 2: Barry 3: Kara 4: Sara 5: Kane 加引号遍历的效果: 1: Oliver Barry Kara Sara Kane |
**强调:**特殊参数$@一定要用在双引号内,效果是每个参数都扩展为分隔的单词。在使用for循环遍历的时候会体现出效果。
移动变量 shift
shift 命令能够用来操作命令行参数。默认情况下将每个参数向左移动一个位置。被移出的参数就被丢弃了,无法恢复。
先掌握这个命令的使用,使用这个命令可以方便地解析命令行参数。
使用示例
下面是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ cat pop.sh #!/bin/bash count=1 while [ -n "$1" ] # while [ $# -ne 0 ] do echo "$count: $1" count=$[ $count + 1 ] shift done $ ./pop.sh Oliver Barry Kara Sara Kane 1: Oliver 2: Barry 3: Kara 4: Sara 5: Kane |
这里有2中判断方法来判断是否还有参数,效果是一样的。
移动多个位置
带参数执行shift,指明要移动几个位置就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ cat pop.sh #!/bin/bash count=1 # while [ -n "$1" ] while [ $# -ne 0 ] do if [ -n "$2" ] then echo "$count: $1, $2" shift 2 else echo "$count: $1" shift fi count=$[ $count + 1 ] done $ ./pop.sh Oliver Barry Kara Sara Kane 1: Oliver, Barry 2: Kara, Sara 3: Kane |
简单修改下上面的脚本,一次输出2个参数,然后移动2个位置。
处理选项
当shell脚本需要多个命令行参数时,在调用脚本的时候就必须将所有参数按固定的顺序。
或者还可以使用选项来指定参数的值。
case 配合 shift
这个例子里有带值的选项也有不带值的选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
$ cat format.sh #!/bin/bash prefix="" # 前缀 base="test" # 默认字符串 suffix="" # 后缀 upper=off # 是否大写 # 解析命令行参数 while [ -n "$1" ] do case "$1" in -a) suffix="$2" shift ;; -b) prefix="$2" shift ;; -s) base="$2" shift ;; -u) upper=on ;; *) echo "$1 is not an option" exit 1 ;; # 发现未知参数,直接退出 esac shift done # 添加前缀和后缀 output="${prefix:+${prefix}_}${base}${suffix:+_${suffix}}" # 判断是否要全大写输出 if [ $upper = on ] then output=${output^^} fi # 输出结果 echo "$output" $ ./format.sh -a after test_after $ ./format.sh -s hello -b befor befor_hello $ ./format.sh -s hello -u -a after -b befor BEFOR_HELLO_AFTER $ ./format.sh -s hello -u -a after -b befor -l -l is not an option |
case语句找到一个选项就处理一个选项。如果还需要在命令行提供其他参数,可以在通用情况的处理部分中处理。而这里因为不需要提供任何参数,凡是解析不正确的就报告错误并退出(exit 1)。
能解析参数的版本
这个版本匹配所有的参数进行格式化输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
$ cat format.sh #!/bin/bash prefix="" # 前缀 base="test" # 默认字符串 suffix="" # 后缀 upper=off # 是否大写 # 显示声明一下这是个数组变量,其实没有必要 declare -a names # 需要格式化输出的所有原始字符串 # 解析命令行参数 while [ -n "$1" ] do case "$1" in -a) suffix="$2" shift ;; -b) prefix="$2" shift ;; -s) base="$2" shift ;; -u) upper=on ;; *) names=("${names[@]}" "$1") ;; esac shift done names[0]=${names[0]:-$base} for name in "${names[@]}" do # 添加前缀和后缀 output="${prefix:+${prefix}_}${name}${suffix:+_${suffix}}" # 判断是否要全大写输出 if [ $upper = on ] then output=${output^^} fi # 输出结果 echo "$output" done $ $ ./format.sh -a after -b befor -u value1 value2 value3 BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER $ ./format.sh -a after after1 -b befor befor1 -u value1 value2 value3 BEFOR_AFTER1_AFTER BEFOR_BEFOR1_AFTER BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER $ ./format.sh -a after after1 -b befor befor1 -u -v value1 value2 value3 BEFOR_AFTER1_AFTER BEFOR_BEFOR1_AFTER BEFOR_-V_AFTER BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER |
看最后的两项的结果,提供的命令行参数有问题,但是程序无法发现。
倒数第二项可以认为提供的参数是对的,但是选项和参数交替出现。
而最后一项提供了一个错误的选项,但是无法识别出来。
解决这个问题,需要更加规范的方法来分离参数和选项。下一小节的内容。
数组带空格的问题
数组添加元素有很多方法,这里是一种重新创建数组的做法:
1 |
array_name=("${array_name[@]}" value1 ... valueN) |
可以一次添加多个元素,如果字符串包含空格,就要加上引号。
和命令行参数的$@与$*一样,数组所有的元素也有这两个类似的符号。最严谨的方法是使用 "${names[@]}"
使用带双引号的@。
添加元素和取出元素的时候都要注意,否则存在带空格的元素的时候就会破坏数组原本的元素分隔。
添加元素这里使用:
1 |
names=("${names[@]}" "$1") |
不单是数组里的元素,被添加的元素也要加上双引号,否则如果有空格,就会按多个元素被添加进数组。
遍历元素使用:
1 |
for name in "${names[@]}" |
只有添加的时候正确了,才能正确的遍历。然后遍历的时候也要保证正确。
验证效果:
1 2 3 4 |
$ ./format.sh -a after -b befor -u value1 "value2 value3" value4 BEFOR_VALUE1_AFTER BEFOR_VALUE2 VALUE3_AFTER BEFOR_VALUE4_AFTER |
完美。
分离参数和选项
这里的参数就是命令行参数中除了定义的选项之外,其他额外的参数。要同时处理参数和选项,就要用特殊字符(双破折线–)将二者分开。双破折线表明选项列表结束,双破折线后面的都是参数。基于这个逻辑,只要在case语句中加一项判断就行了。
把上面的脚本做一些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
$ cat format.sh #!/bin/bash prefix="" # 前缀 base="test" # 默认字符串 suffix="" # 后缀 upper=off # 是否大写 # 显示声明一下这是个数组变量,其实没有必要 declare -a names # 需要格式化输出的所有原始字符串 # 解析选项 while [ -n "$1" ] do case "$1" in -a) suffix="$2" shift ;; -b) prefix="$2" shift ;; -s) base="$2" shift ;; -u) upper=on ;; --) shift break ;; *) echo "$1 is not an option" exit 1 ;; # 发现未知参数,直接退出 esac shift done # 解析参数 while [ -n "$1" ] do names=("${names[@]}" "$1") shift done names[0]=${names[0]:-$base} for name in "${names[@]}" do # 添加前缀和后缀 output="${prefix:+${prefix}_}${name}${suffix:+_${suffix}}" # 判断是否要全大写输出 if [ $upper = on ] then output=${output^^} fi # 输出结果 echo "$output" done |
基于这个版本,在使用的时候,需要先输入选项,然后使用双破折线隔开,再输入参数。当脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作参数:
1 2 3 4 5 6 7 8 |
$ ./format.sh -a after -b befor -u value1 value2 value3 value1 is not an option $ ./format.sh -a after -b befor -u -- value1 value2 value3 BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER $ ./format.sh -a after -b befor -v -u -- value1 value2 value3 -v is not an option |
第一次没有使用双破折线,所以报错。
第二次正确的用双破折号分隔了参数和选项。
第三次在选项部分出现了未定义的选项,也能发现错误。
小结
这一小节的内容也是为下面的getopt命令做铺垫。getopt就是可以帮我们完成命令行参数的解析,返回一个用双破折线隔开选项和参数的规整的参数列表。
另外这里还不支持选项合并:
1 |
$ ls -al |
这些问题,用getopt都能解决,而且还支持长选项。
getopt 命令
使用getopt命令,可以解析任何命令行选项和参数,但是用法比较复杂。getopt的命令用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$ getopt --help 用法: getopt optstring parameters getopt [options] [--] optstring parameters getopt [options] -o|--options optstring [options] [--] parameters 选项: -a, --alternative 允许长选项以 - 开始 -h, --help 这个简短的用法指南 -l, --longoptions <长选项> 要识别的长选项 -n, --name <程序名> 将错误报告给的程序名 -o, --options <选项字符串> 要识别的短选项 -q, --quiet 禁止 getopt(3) 的错误报告 -Q, --quiet-output 无正常输出 -s, --shell <shell> 设置 shell 引用规则 -T, --test 测试 getopt(1) 版本 -u, --unquoted 不引用输出 -V, --version 输出版本信息 $ |
用法一共有3种格式,下面都会用到。
在命令行中简单使用
先看第一个最简单的格式:
1 |
getopt optstring parameters |
第二部分optstring(选项字符串),是这个命令解析的格式。
第三部分parameters(getopt命令的参数),就是需要解析的内容。
因此,getopt会按照 optstring 的设置,将 parameters 解析为相应的选项和参数。参考示例来理解:
1 2 |
$ getopt ab:cd -ad value1 -b best1 value2 value3 -a -d -b best1 -- value1 value2 value3 |
主要理解 ab:cd
的意义。
这里定义的都是短选项,4个字母代表有4个选项。b后面的冒号表示这个选项需要一个参数。如果不给选项b一个参数,就会报错:
1 2 3 |
$ getopt ab:cd -ad value1 -b getopt:选项需要一个参数 -- b -a -d -- value1 |
使用双破折线
如果添加了双破折线,那么无轮后面是什么,都会作为参数而不是选项来处理:
1 2 3 4 |
$ getopt ab:cd -- -ad value1 -b best1 value2 value3 -- -ad value1 -b best1 value2 value3 $ getopt ab:cd -ad value1 -- -b best1 value2 value3 -a -d -- value1 -b best1 value2 value3 |
这依然是是命令用法的第一种格式,双破折线是parameters内容的一部分。
双破折线出现位置之前的内容按照optstring的设置来解析,之后的内容一律认为是参数。即使有类似选项的内容,被认作为是参数。
参数包含空格的问题
第一种格式和第二、第三种在功能上也是有区别的。这里输出的参数都是不带引号的。而另外两种格式输出的参数都是带引号的。
重要的区别不在引号上,而是这种用法不支持处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。
支持长选项
参考上面的示例,加上长选项的支持。使用长选项的示例如下:
1 2 |
$ getopt -o ab:cd --long arga,argb:,argc,argd -- -ad -b best --argd value1 value2 -a -d -b 'best' --argd -- 'value1' 'value2' |
这是命令用法的第三种格式。
-o 表示定义短选项
–long 其实是–longoptions,不过省略任意个字母程序都能认识。或者也可以用-l。这个是指定长选项的。所有内容都要连起来,不能有空格。选项之间用逗号隔开。定义完之后,在用双破折号隔开,后面的内容就是parameters。
错误报告引用的程序名
之前已经试过一次解析错误的报告了:
1 2 3 |
$ getopt ab:cd -ad value1 -b getopt:选项需要一个参数 -- b -a -d -- value1 |
这里错误报告的是getopt错误,可以把这个默认的内容替换掉。一般是换成执行的脚本的名字。
这里使用命令用法的第二种格式,把 optstring 和 parameters 都放到双破折线后面:
1 2 |
$ getopt -- ab:cd -ad value1 -b best1 -a -d -b 'best1' -- 'value1' |
这样在双破折线前面就可加getopt命令的选项,这里要指定-n选项:
1 2 3 |
$ getopt -n test.sh -- ab:cd -ad value1 -b test.sh:选项需要一个参数 -- b -a -d -- 'value1' |
这里看到包裹错误是,名字已经被替换掉了。
在脚本中,可以使用 $(basename $0)
或者直接用$0。
禁止错误报告
还有一个-q参数,可以禁止错误报告,解析错误的选项和参数将被丢弃:
1 2 |
$ getopt -n test.sh -q -- ab:cd -ad value1 -b -a -d -- 'value1' |
可选参数
还有一种可选参数,使用两个冒号。这个选项可以有一个或零个参数:
1 2 3 4 |
$ getopt -o a::bc: -l arga::,argb,argc: -- -a value1 --arga value2 -a '' --arga '' -- 'value1' 'value2' $ getopt -o a::bc: -l arga::,argb,argc: -- -avalue1 --arga=value2 -a 'value1' --arga 'value2' -- |
第一次执行是传递的参数是错误的。因为是可选参数,参数和值之间不能有空格隔开,否则会有歧义。必须要连在一起才能认为是前一个选项的参数。否则就被认作是独立的参数了。
小结
getopt 命令的选项所指定的选项字符串的规则:
- 短选项,每一个字符代表一个选项
- 长选项,每一个字符串代表一个选项,用逗号分隔
- 选项后跟一个冒号,表示选项需要一个参数
- 选项后跟两个冒号,表示选项有一个可选参数(一个或零个参数)
- 可选参数的参数和值之间不能有空格,短选项直接连起来,长选项加等号连起来
在脚本中使用 getopt
现在已经可以用getopt命令,将命令行参数按照规定的格式解析成规整的格式了。并且在解析过程中,还能发现参数格式错误的情况并报告。
接下来就是在脚本中使用经过getopt命令解析后的参数了。
set 命令
要在脚本中使用getopt。首先,要用getopt命令生成格式化后的版本来替换已有的命令行选项和参数。需要用到set命令。
set命令能够处理shell中的各种变量。具体不展开,这里只用了这个命令的一个选项,双破折线(–)。效果是将命令行参数替换成set命令的参数值。
然后,该方法会将原始脚本的命令行参数传给getopt命令执行,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数:
1 |
set -- $(getopt ab:cd "$@") |
现在原始的命令行参数变量的值会被getopt命令的输出替换。而getopt已经为我们格式化好了命令行参数。
直接使用
在之前编写的脚本的基础上,只要在开头加上一行代码,就可以直接使用了:
1 |
set -- $(getopt a:b:s:u "$@") |
加上这句后,就是让后续的代码处理getopt返回的参数,而不是调用命令时的命令行参数。
验证效果:
1 2 3 4 5 6 7 8 9 |
$ ./format.sh -u -a after -b befor value1 value2 value3 BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER $ ./format.sh -u -a after -b befor value1 "value2 value3" value4 BEFOR_VALUE1_AFTER BEFOR_VALUE2_AFTER BEFOR_VALUE3_AFTER BEFOR_VALUE4_AFTER |
第二条命令并不能处理带空格的参数,因为这里使用的是getopt的第一种格式。
使用第二种格式来解析
要处理空格,就需要使用第二种格式(或者第三种),将命令修改为如下:
1 |
set -- $(getopt -- a:b:s:u "$@") |
简单的在最前面加上双破折线就好了。这条语句是错误的,后面还要修改。
再来验证一下:
1 2 3 4 5 |
$ ./format.sh -u -a after -b befor value1 "value2 value3" value4 'BEFOR'_'VALUE1'_'AFTER' 'BEFOR'_'VALUE2_'AFTER' 'BEFOR'_VALUE3'_'AFTER' 'BEFOR'_'VALUE4'_'AFTER' |
使用第二、第三种格式,会用引号来限定参数的内容。但是引号干扰了set命令。
使用 eval 命令
这里出现了一个新的问题,不但没有正确的处理空格,输出的内容还有额外的引号。空格的问题先放一放,这里需要用到eval命令来解决新问题。
eval 命令用于将其后的内容作为单个命令读取和执行,这里用于处理getopt命令生成的参数的转义字符。
关于eval命令,还有一种使用的情景。有时候在脚本中拼接出来的字符串即使打印出来看正确。并且直接复制、粘贴在交互界面中也能正确读被当做命令运行。但是却无法在脚本中被执行。这个时候就可以使用eval命令来解决。它能够把字符串当做命令来执行。
在脚本中通过各种引用和判断拼接出一个复杂的命令的时候,有时候就会出现无法执行的情况。这时候就直接赋值、粘贴去交换界面试一下,如果拼接的结果本身没问题,那么加上eval命令后,应该就能用运行。
修改命令如下:
1 |
eval set -- $(getopt -- a:b:s:u "$@") |
再次验证:
1 2 3 4 |
$ ./format.sh -u -a after -b befor value1 "value2 value3" value4 BEFOR_VALUE1_AFTER BEFOR_VALUE2 VALUE3_AFTER BEFOR_VALUE4_AFTER |
第一种格式加上eval命令也是没有问题的,所以可以无脑用上。
解决空格问题
只要能正确的使用getopt的第二种或第三种格式,那么参数包含空格的问题也就解决了。看上一小节。
参数解析错误并退出
执行命令时,使用错误的参数,当前的效果如下:
1 2 3 4 5 6 |
$ ./format.sh -u -w -a after -b befor value1 "value2 value3" value4 getopt:无效选项 -- w BEFOR_VALUE1_AFTER BEFOR_VALUE2 VALUE3_AFTER BEFOR_VALUE4_AFTER $ |
解析发现问题了,并且报告了,但是脚本没有终止,而是继续执行。如果要判断出解析错误,就需要使用$?参数。然后退出脚本则是用exit命令。
这里直接直接使用$?并无法获取到参数解析错误的结果。因为此时的结果是set命令(也可能是eval命令)的执行结果,而getopt是再前一条的命令。
解决这个问题,要先把getopt命令执行一遍,进行判断。然后再用set调用一遍,可以直接使用之前执行的结果:
1 2 3 |
getopt_cmd=$(getopt -n $(basename $0) -- a:b:s:u "$@") [ $? -ne 0 ] && exit 1 eval set -- "$getopt_cmd" |
这里还加上了报告错误时名称的定义。exit退出时也要指定退出状态为非0,因为是运行错误。
验证效果:
1 2 3 4 5 6 |
$ ./format.sh -v -a after -w -b format.sh:无效选项 -- v format.sh:无效选项 -- w format.sh:选项需要一个参数 -- b $ echo $? 1 |
现在解析有问题后,就会直接退出。
完整的代码示例
这里加上长选项以及可选参数的功能。
多加了一个参数 -m, --mark
由于指定使用什么连接符:
- 默认直接连,不使用连接符号
- 加上选项,默认使用下划线连接
- 为选项加上参数后,则使用参数来连接
参数比较多,加了 -h, --help
选项打印参数说明。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
$ cat format.sh #!/bin/bash mark="" # 连接符号 prefix="" # 前缀 base="test" # 默认字符串 suffix="" # 后缀 upper=off # 是否大写 # 显示声明一下这是个数组变量,其实没有必要 declare -a names # 需要格式化输出的所有原始字符串 # 打印的帮助信息 help_str=" 参数说明: -h, --help: 打印帮助信息 -m, --mark [连接符]: 使用连接符,默认是下划线(_),可以指定 -a, --after string: 添加后缀 -b, --befor string: 添加前缀 -s, --string string: 指定中间的字符串,默认是“test” -u, --upper: 全大写输出 " # 解析命令行参数 getopt_cmd=$(getopt -o m::ha:b:s:u --long mark::,help,after:,befor:,string:,upper -n $(basename $0) -- "$@") [ $? -ne 0 ] && exit 1 eval set -- "$getopt_cmd" # 解析选项 while [ -n "$1" ] do case "$1" in -m|--mark) case "$2" in "") mark="_" shift ;; *) mark="$2" shift ;; esac ;; -h|--help) echo -e "$help_str" exit ;; -a|--after) suffix="$2" shift ;; -b|--befor) prefix="$2" shift ;; -s|--string) base="$2" shift ;; -u|--upper) upper=on ;; --) shift break ;; *) echo "$1 is not an option" exit 1 ;; # 发现未知参数,直接退出 esac shift done # 解析参数 while [ -n "$1" ] do names=("${names[@]}" "$1") shift done names[0]=${names[0]:-$base} for name in "${names[@]}" do # 添加前缀和后缀 output="${prefix:+${prefix}${mark}}${name}${suffix:+${mark}${suffix}}" # 判断是否要全大写输出 if [ $upper = on ] then output=${output^^} fi # 输出结果 echo "$output" done |
验证效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ ./format.sh -a after -b befor VALUE1 "VALUE2 VALUE3" VALUE4 beforVALUE1after beforVALUE2 VALUE3after beforVALUE4after $ ./format.sh -a after -b befor --mark befor_test_after $ ./format.sh -a after -b befor --mark="||" -u BEFOR||TEST||AFTER $ ./format.sh -a after -b befor --mark="||" -u --help 参数说明: -h, --help: 打印帮助信息 -m, --mark [连接符]: 使用连接符,默认是下划线(_),可以指定 -a, --after string: 添加后缀 -b, --befor string: 添加前缀 -s, --string string: 指定中间的字符串,默认是“test” -u, --upper: 全大写输出 $ |
有getopt就够用了。顺便再简单讲下getopts。
getopts功能上差一点,不过封装的更高级,用起来更简单,需要的代码也会少一点。
getopts 命令
getopts是另一个解析命令行参数的工具。它是Bash的内部命令。
它的优势在于:
- 不需要通过一个外部程序来处理位置参数
- 可以很容易地设置用来解析的Shell变量
- getopts 定义在 POSIX 中
不支持长选项:
getopts 不能解析 GUN 风格的长选项(–long),也不能解析 XF86 风格的长选项(-long)
getopt 是将选项和参数处理后只生成一个输出。我们还要用 set 来完成传递的工作。
getopts 能够和已有的shell参数变量配合默契。每次调用时,一次只处理命令行上检测到的一个参数。处理之后,它会退出并返回一个大于0的退出状态码。这样就非常方便的可以在while循环中使用。
基本用法
getopts 会使用到一下3个变量:
OPTIND: 存放下一个要处理的参数的索引。这是 getopts 在调用过程中记住自己状态的方式。
OPTARG: 由 getopts 找到的选项所对应的参数。
OPTERR: 值为0或1。指示Bash是否应该显示由 getopts 产生的错误信息。
getopts 命令的基本语法:
1 |
getopts 选项字符串 名称 [参数] |
选项字符串(OPTSTRING):getopts 会有哪些选项,哪些是有参数的(选项后有冒号)
名称(VARNAME):getopts 会将找到的选项赋值给这个名称的变量
参数(ARGS):一般情况向缺省,getopts会去解析脚本调用时的所有的参数。如果执行了这个参数,getopts就不解析传递给脚本的参数了,而是解析这里的参数。
getopts 不会移动变量。在处理完所有的选项后,命令就会停止,并将参数留给我们来继续处理。此时可以先用shit命令配合OPTIND的值来移动到第一个参数的位置:
1 |
shift $[ $OPTIND - 1 ] |
错误报告模式
getopts命令支持两种错误报告模式:
- 详细错误报告模式
- 抑制错误报告模式
对于产品中的脚本,推荐使用抑制错误报告模式。
详细错误报告模式
在详细错误报告模式下,如果 getopts 遇到了一个无效的选项,VARNAME 的值会被设置为问号(?),并且变量 OPTARG 不会被设置。如果需要的参数没找到,VARNAME的值也会被设置为问号(?),变量 OPRARG 也不会被设置,并且会打印一个错误信息。
抑制错误报告模式
在抑制错误报告模式下,如果 getopts 遇到一个无效的选项,VARNAME 的值会被设置为问号(?),并且变量 OPTARG 会被设置为选项字符。如果需要的参数没找到,VARNAME的值会被设置为冒号(:),并且变量 OPTARG 中会包含选项字符。
要使用抑制错误报告模式,只需要在调用 getopts 时,设置选项字符串(OPTSTRING)时以冒号开头即可。下面的例子用的就是一直错误报告模式。
示例代码
这里使用抑制错误报告模式,所以需要自己分析并且报告解析错误。都在代码里了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
$ cat say_hello.sh #!/bin/bash defaultname="nobody" # 默认的名字 declare -a names # 存放名字的数组 hello="hello" # 打招呼的用语 end="!" # 结束的内容 tittle=off # 是否首字母大写 # 解析选项 while getopts :n:h:e:t opt do case "$opt" in n) defaultname="$OPTARG" ;; h) hello="$OPTARG" ;; e) end="$OPTARG" ;; t) tittle=on ;; :) # 没有为需要参数的选项指定参数 echo "This option -$OPTARG requires an argument." exit 1 ;; ?) # 发现了无效的选项 echo "-$OPTARG is not an option" exit 2 ;; esac done # 解析参数 shift $[ $OPTIND -1 ] # 移动到第一个参数的位置 # 这次用for循环遍历 for arg in "$@" do names=("${names[@]}" "$arg") done names[0]=${names[0]:-$defaultname} for name in "${names[@]}" do [ "$tittle" = on ] && output="${hello^} ${name^} $end" || output="$hello $name $end" echo "${output}" done |
验证执行如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ ./say_hello.sh hello nobody ! $ ./say_hello.sh -n adam hello adam ! $ ./say_hello.sh -n adam -h hi -e. -t Hi Adam . $ ./say_hello.sh -h hi -e. -t adam bob clark Hi Adam . Hi Bob . Hi Clark . $ ./say_hello.sh -a -h hi -e. -t adam bob clark -a is not an option $ ./say_hello.sh -h This option -h requires an argument. |
选项和参数不能混排:
1 2 3 4 5 |
$ ./say_hello.sh adam hello adam ! $ ./say_hello.sh adam -t hello adam ! hello -t ! |
支持双破折线:
1 2 3 4 |
$ ./say_hello.sh -t adam Hello Adam ! $ ./say_hello.sh -t -- adam Hello Adam ! |
比较下来,使用起来会比getopt方便很多,不过功能上也差了很多,可选参数(双冒号::)应该也是不支持的。另外,如果熟悉getopt的话,每一步的操作都是自己的代码控制的。而getopts就简化了很多地方,比如不会调用shift移动变量。
将选项标准化
有些字母选项在Linux世界里已经有了某种程度的标准含义。如果在shell脚本中支持这些选项,就应该使用标准的字母来定义。
下面是一些命令行选项中经常会用到的选项和含义:
选项 | 描述 |
---|---|
-a | 显示所有对象(显示隐藏) |
-c | 生成一个计数 |
-d | 指定一个目录 |
-e | 扩展一个对象 |
-f | 指定读入数据的文件 |
-h | 显示命令的帮助信息 |
-i | 忽略文本大小写 |
-l | 产生输出的长格式版本 |
-n | 使用非交互模式(批处理) |
-o | 将所有输出重定向到指定的输出文件 |
-q | 以安静模式运行 |
-r | 递归地处理目录和文件 |
-s | 以安静模式运行 |
-v | 生成详细输出 |
-x | 排除某个对象 |
-y | 对所有问题回答yes |
FROM:
https://blog.51cto.com/steed/2443313
https://blog.51cto.com/steed/2443718?source=dra
1 Comment
tangdudu · 04/19/2022 at 10:11 AM
非常实用的blog, 谢谢