如何将命令行参数传递给 shell 脚本

最近拖延症和畏难情绪泛滥得厉害,todo 加了一条又一条,忙不过来索性开摆了,哎。工作确实是有那么亿点小忙,工作思路也有点不明确,还得看论文;漏洞复现也是障碍重重,POC 针对的是 HTTP2 我搭建好的环境偏偏是 HTTP1.1,还得学习下 HTTP, HTTP2, HTTP1.1。

现实总是那么骨感。最开始我只想学习 Race Condition,后来研究案例学了下 WebRTC,又看了 ruby, mvc, rails,之后了解到 websocket 也存在条件竞争,就简单看了下 websocket。主要是我基础太差,碰到一个名词总是无法直接给出具体概念,不过我觉得这种学习方法也蛮好的,深度优先,逐渐丰富自己的技术栈;但也有其缺点,容易跑偏,学着学着就不知道学哪去了,不及时总结的话甚至不知道自己学了点啥。

怎么办,我开始迷茫了,呜呜呜 T_T

牢骚到此为止,写完这篇文章起码能划掉一条 todo 了。

hacking for fun


工作中有遇到处理大量 url 的情况,就寻思着写个 shell 脚本,其实脚本去年就写好了,一直拖着没总结,拖延症晚期了我。相较于 Python,Shell 脚本有其天然优势,脚本中的每行代码都相当于是在命令行中执行,这也就允许我们方便地使用大量现有工具,简化实现,快速完成任务。此外,为了增加代码的灵活性,使用位置参数无疑首选方法,你也不想每次该参数都直接修改脚本吧!?But, shell 脚本的位置参数具体如何使用呢?希望这篇文章可以回答你的疑问。

先放几个符号:

Parameter(s) Description
$0 第一个位置参数
$1 … $9 the argument list elements from 1 to 9
${10} … ${N} the argument list elements beyond 9 (note the parameter expansion syntax!)
$* 指代除了 $0 之外的所有参数
$@ 指代除了 $0 之外的所有参数
$# 指代除了 $0 之外的参数的数量

shift

如果将 $1 及之后的参数序列看成一个栈的话,shift 相当于是将 $1 出栈,之前的 $2 就成了新的 $1,其他参数依次类推。如果仅使用 shift 的话,默认移一位,也可用 shift <n> 移动 n 个参数。

举个栗子:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
numargs=$#
echo "$0"
echo "all of the params: $@"
echo "the number of params: $#"
for ((i=1 ; i <= numargs ; i++))
do
echo "$1"
shift
done

运行结果如下,$0 是 shell 脚本的第一个参数,一般被设置为脚本的名字,剩下的参数从 $1$4 依次排序。

shift <n> 可以移动 n 个参数,我们尝试一次移动两个。

1
2
3
4
5
6
7
#!/bin/bash
numargs=$#
for ((i=1 ; i <= numargs ; i++))
do
echo "$1"
shift 2
done

报错了,不过也是意料之中。

虽然我 shift 2 报错了,但退一万步讲,shift 1 最后一次迭代的 $1 是 “caishao!”,echo 之后再使用 shift 不会报错嘛?

1
2
3
4
5
6
7
8
#!/bin/bash
numargs=$#
for ((i=1 ; i <= numargs ; i++))
do
echo "$1"
shift 1
echo "$1"
done

修改下代码,观察最后一次迭代后 $1 的值。

最后一次迭代,先是输出了 “caishao!” 之后 shift 1,然后输出了一共空值。解释:**$# 的值非负,就不会报错**。最后 shift 之后, 可以把命令看成 sh ./shift1.sh,只是没传参数罢了,这几行代码也没说非得要个参数不是?没报错也就不难理解了。

再看一个比较复杂的栗子。

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
#!/bin/sh

content="I hate you"

# 定义函数
display_help() {
echo "不会有人看不懂源码吧,不会吧,不会吧?!"
}

while :
do
case "$1" in
-c | --content)
content="$2"
shift 2
;;
-h | --help)
display_help # 调用函数 display_help
# 脚本就跑到这了,不用 shift 了
exit 0
;;
-u | --user)
username="$2"
shift 2
;;
-v | --verbose)
verbose="verbose"
shift
;;
--) # End of all options
shift
break
;;
-*)
echo "Error: Unknown option: $1" >&2
exit 1
;;
*) # No more options
break
;;
esac
done

echo "$content, $username"
# End of file

经常配环境的小伙伴都知道,调用 bash 脚本(或者说其他命令行工具)的基础语法是 COMMAND [options] <params>。我们简单跑一下脚本。

shift 之殇:

shift 处理位置参数可太死板了!事无巨细,全是用户自己处理。有一个参数的选项用 shift 2,没参数的选项用 shift,甚至没法解释组合使用的选项(如:-fu <USER>),也没有简单的方式指定那些选项是必需的,参数多了维护起来也费劲。

众所周知,Linux 以“优雅”著称,那么位置参数传递有无“优雅”的解决方案呢? 下面有请 getopts !!

getopts

getopts is neither able to parse GNU-style long options (--myoption) nor XF86-style long options (-myoption)

getopts 是 shell 的内置命令,也是用来处理命令行参数的。只能解析一个字符的选项,无法解析长选项,如 --myoption-myoption

基础语法如下:

1
getopts OPTSTRING VARNAME [ARGS...]

工作原理:每次从 OPTSTRING 中读一个选项,选项名称存储在 VARNAME,如果需要参数,再读取对应参数 ARGS,参数值存储在 $OPTARG 中。$OPTARG 总是存放下一个待处理的位置参数。简单来讲就是根据相应的语法规则,自动给你加了个 shift [1\2] 移位命令。

我们对上面 shift 部分的代码使用 getopts 进行修改。

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
#!/bin/sh

content="I hate you"

usage() {
echo -e "不会有人看不懂源码吧,不会吧,不会吧?!\n"
echo -e "Usage: sh example2.sh -u caishao [-c] ['I love you']\nOptions:\n -u\t-\t a person you love\n -c\t-\t sth you want to say\n -h\t-\t show this help content\n" 1>&2
exit 1
}

while getopts ":c:hu:" opt; do
case $opt in
c)
content="$OPTARG"
;;
h)
usage
;;
u)
username="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
*)
usage
;;
esac
done
shift $((OPTIND - 1))

if [ -z "${username}" ]; then
usage; exit 1;
fi

echo "$content, $username"
# End of file

运行结果演示:

因为 getopts 每次仅读取一个选项及其参数值,且在遇到第一个非选项的参数(不是以 ‘-‘ 开头的字符串)时,会返回 FALSE,这也使得我们可以使用 while 循环优雅地读取参数。

getopts ":c:hu:" opt; 中的 ":c:hu:" 就是上文提到的 OPTSTRING。其中 c:u: 表示选项 -c-u 后会跟一个参数,其值存储在内置变量 $OPTARG。对 OPTSTRING 的解析方式也说明了 getopts 无法解析长选项。

getopts 不会影响原来的位置参数序列,也就是说 getopts 处理完位置参数后,$1 表示的还是 -c-u (栗子,懂我意思吧)。如果要读取之后的参数,可以使用上文中提到的 shift,但首先需要将 getopts 处理过的参数出栈,shift $((OPTIND-1))

回到 ":c:hu:",其中第一个 : 表示 getopts 的错误汇报(errot-reporting)模式使用 silent 模式,忽略错误。

getopts 的错误汇报(errot-reporting)模式:

  • 详尽模式(verbose mode):事无巨细,啥都报
  • 静默模式(silent mode):忽略错误,以 ?: 后的内容汇报

echo "Invalid option: -$OPTARG" >&2 中的 >&2 表示将内容输出到 stderr,这里的 2 是特殊的 [[文件描述符(fd)]] 。

所谓文件描述符(File descriptor),是一个非负整数,本质是一个索引值。当打开一个文件时,内核向进程返回一个文件描述符(open系统调用返回得到),后续read、write这个文件时,只需要用这个文件描述符来标识这个文件,将其作为参数传入read、write。0,1,2 这三个文件描述符值已经被赋予特殊含义,分别是标准输入(STDIN_FILENO),标准输出(STDOUT_FILENO),标准错误(STDERR_FILENO)。

linux 中如果你不想显示结果的话,可以使用 2> /dev/null 将错误重定向到一个空设备中。脚本中的 >&2 指明输出的是 stderr 才能跟 2> /dev/null 无缝衔接,不然 shell 咋知道输出的是报错呢。

剩下还有没搞懂的自己谷歌下吧,这次写的废话有那么亿点点多了。

参考资料:

  1. Small getopts tutorial
  2. Handling positional parameters
  3. Wikipedia Getopts