我正在编写一个脚本,该脚本将字符串作为输入以及用户可以通过使用参数作为指示符来选择的其他选项。换句话说,是这样的:
./script "My input string" -pxz
或者
./script -pxz "My input string"
但我遇到了以下问题。输入非 getopts 样式的参数后, getopts 命令似乎停止工作。检查一下,例如:
#!/bin/bash
while getopts "ab" arg; do
echo $arg received
done
当我们运行它时,我们得到:
$ ./example.sh -a -b -a string -b
a received
b received
a received
$
它停在“string”处并且不会继续。 getopts 命令返回非零退出状态,因为“string”不是它期望读取的参数类型,并且 while 循环结束。我尝试添加第二个while getopts
,但这没有任何作用。 getopts 的“读取头”仍然停留在该“字符串”参数上,并且将再次以非零退出状态退出。
这意味着,看起来,我无法让用户先输入字符串,然后输入选项,因为 getopts 永远无法读取字符串并到达那里。所以我不能这样做:
./script "My input string" -pxz
另一方面,如果我告诉用户首先输入他们的选项,然后输入字符串,我就会遇到如何检索字符串的问题。通常,如果没有选择,我会这样做:
string="$1"
但现在由于有选项,而且我不知道有多少个,我不再知道字符串占据什么位置。那么我怎样才能找回它呢?
现在,理想情况下,我的脚本实际上应该能够以这两种方式甚至两者的组合来工作。它应该能够处理如下输入:
./script -p "My input string" -xz
那么我该如何解决这个问题呢?
答案1
如果您使用的是 Linux,并且您有getopt
来自 util-linux(或 Busybox)的命令,它也可以像 GNU 工具那样对参数进行重新排序。它还支持参数、选项参数、可选选项参数中的空格以及--
终止选项处理。
标准行为是在看到第一个非选项时停止选项处理,因此其他实现可能不支持混合选项和非选项。如果POSIXLY_CORRECT
设置的话,即使是 GNU 也不支持它。
无论如何,使用下面的脚本:
$ ./opts.sh -a 123 "arg with space" more -bc args -- -also -args
option -a with arg '123'
option -b
option -c
remaining arguments (5):
<arg with space> <more> <args> <-also> <-args>
opts.sh
:
#!/bin/bash
getopt -T
if [ "$?" -ne 4 ]; then
echo "wrong version of 'getopt' installed, exiting..." >&2
exit 1
fi
params="$(getopt -o a:bc -- "$@")"
eval set -- "$params"
while [ "$#" -gt 0 ]; do
case "$1" in
-a)
echo "option -a with arg '$2'"
shift 2;;
-b)
echo "option -b"
shift;;
-c)
echo "option -c"
shift;;
--)
shift
break;;
*)
echo "something else: '$1'"
shift;;
esac
done
echo "remaining arguments ($#):"
printf "<%s> " "$@"
echo
(请注意,上述脚本仅适用于getopt
与 util-linux 增强功能兼容的实现。其他“传统”实现存在参数中的空格等问题。该getopt -T
测试用于检测实现是否兼容。)
答案2
这一页 (在 getopts 之后解析脚本参数)在我理解这一点的过程中帮助了我很多。但我将尝试从头开始解释这一切。
当任何东西从终端运行时,它会收到一个包含调用它的所有参数的数组。第一个参数占据该数组中的位置 0,始终是程序或脚本本身的名称(有时连同其路径 - 但它是在终端中键入的)。在 bash 脚本中,我们可以通过编号的变量访问该数组$0
-$n
其中 n 是我们收到的参数数量。例如,如果我们的脚本是这样调用的:
./script --the -great "brown fox" --------jumped
然后在我们的脚本中:
$0 = ./脚本
$1 = --the
$2 = - 太棒了
$3 = 棕色狐狸
$4 = --------跳跃
所以如果我们这样做:
echo $3
我们得到:
brown fox
现在,shell 使用一个内部变量来跟踪该数组的开始位置。所有数字变量(除了$0
)都是从该数组开头的偏移量。该shift
命令可用于移动数组的开头,以便它从之前的位置向下一个或多个位置开始。这可以$1
参考以前的情况$2
。因此,在我们的示例中,如果我们这样做:
shift
echo $1
echo $2
我们会得到
-great
brown fox
这整个转变过程中唯一的例外是$0
。$0
从不移动。它将始终包含您用于调用脚本的路径。
该shift
命令还可以指定一个数字。这将是它移动数组的位置数。所以shift 2
相当于使用了shift
两次(即默认数量为1)。
该getopts
命令有自己的变量,称为OPTIND
它用来跟踪它的最新情况。当getopts
成功获取参数(或完成获取参数中的最后一个字母选项(例如 -abc)时),它会添加一个以OPTIND
使其指向命令行上的下一个参数。下次getopts
调用时,它将从下一个参数开始解析。
现在,这是有趣的部分: in 中的数字OPTIND
还指距数组开头的偏移量。所以该shift
命令可用于影响getopts
:
#!/bin/bash
getopts "abc" arg
echo received $arg
shift
getopts "abc" arg
echo received $arg
如果我们运行它:
./script -a -b -c
我们得到:
received a
received c
发生的事情是这样的:每个脚本的开头都OPTIND
保存着值 1。这意味着当我们第一次调用getopts
它时,它开始从参数 1 读取,即从数组开头位置 1 的参数(基本上它读取$1
)。第一次调用变量后,它getopts
保存OPTIND
着值 2,指向参数-b
。但是,然后我们执行移位,这使得$2
now 指向以前的$3
。所以现在,在不改变 的值的情况下OPTIND
,它现在指向-c
。
这对我们有什么帮助?好吧,我们可以使用 in 中的值OPTIND
(我们可以访问)来移动数组,跳过所有已解析的参数并生成$1
第一个尚未解析的参数。考虑一下:
#!/bin/bash
while getopts "abc" arg; do
echo recieved $arg
done
shift $((OPTIND-1))
echo $1
如果我们运行它:
./script -a -b -a -b string -a
无论我们在它前面放置多少个选项,底部echo
的总是会输出。string
这是因为 while 循环完成后将OPTIND
始终指向该字符串。然后,我们使用算术扩展来传递比shift
字符串位置小一的数字,这意味着我们移动$1
到它的位置。因此,回显$1
将始终回显字符串。
我们还可以在遇到字符串后继续读取选项。唯一需要注意的是,OPTIND
即使在转变之后,它仍然保留其价值。因此,如果OPTIND
达到 5,我们进行移位,这$1
就是字符串,然后我们立即返回到getopts
它将从字符串后面的 5 个参数开始读取,而不是像我们希望的那样从后面的参数开始读取。为了解决这个问题,我们可以简单地设置OPTIND
为 2:
OPTIND=2
或 shift 然后将其设置为 1 (这样它将读取现在的内容$1
- 字符串后面的参数):
shift
OPTIND=1
将它们放在一起,下面是一些代码,它将读取所有选项并创建所有其他输入的数组:
#!/bin/bash
while [[ $# -gt 0 ]]; do
while getopts "abc" arg; do
echo recieved $arg
done
shift $((OPTIND-1))
# when we get here we know we have either hit the end of all
# arguments, or we have come to one that is an
# input string (not an option)
# so see which it is we test if $1 is set
if [[ ${1+set} = set ]]; then
INPUTS+=("$1")
shift
fi
OPTIND=1
done
echo "Here is the array:"
echo ${INPUTS[@]}