如何将命令行参数传递给 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 … $9the 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 个参数。

举个栗子:

#!/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 个参数,我们尝试一次移动两个。

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

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

在这里插入图片描述

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

#!/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,只是没传参数罢了,这几行代码也没说非得要个参数不是?没报错也就不难理解了。

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

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

基础语法如下:

getopts OPTSTRING VARNAME [ARGS...]

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

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

#!/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
  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值