如何使用 getopts 解析 Linux Shell 脚本选项

您是否希望您的 Linux shell 脚本能够更优雅地处理命令行选项和参数? Bash 的 getopts
内置函数让您可以巧妙地解析命令行选项——而且它也很简单。我们告诉你如何。
介绍内置的 getopts
将值传递到 Bash 脚本中是一件非常简单的事情。您从命令行或另一个脚本调用您的脚本,并在脚本名称后面提供您的值列表。这些值可以在脚本中作为变量访问,第一个变量以 $1
开头,第二个变量以 $2
开头,依此类推。
但是,如果您想将选项传递给脚本,情况很快就会变得更加复杂。当我们说选项时,我们指的是像 ls
这样的程序可以处理的选项、标志或开关。它们前面有一个破折号“-
”,通常作为程序打开或关闭其某些功能的指示器。
ls
命令有 50 多个选项,主要与格式化输出有关。 -X
(按扩展名排序)选项按文件扩展名的字母顺序对输出进行排序。 -U
(未排序)选项按目录顺序列出。
选项就是这样——它们是可选的。您不知道用户将选择使用哪些选项(如果有的话),您也不知道他们在命令行中列出它们的顺序。这增加了解析选项所需的代码的复杂性。
如果您的某些选项带有一个参数,称为选项参数,事情会变得更加复杂,例如,ls -w
(宽度)选项预计后跟一个数字,代表输出的最大显示宽度。当然,您可能会将其他参数传递到您的脚本中,这些参数只是数据值,根本不是选项。
值得庆幸的是 getopts
为您处理了这种复杂性。因为它是内置的,所以它在所有具有 Bash shell 的系统上都可用,因此无需安装任何东西。
注意:getopts 不是getopt
有一个名为 getopt
的旧实用程序。这是一个小型实用程序程序,不是内置程序。 getopt
的许多不同版本具有不同的行为,而 getops
内置函数遵循 POSIX 准则。
type getopts
type getopt

因为 getopt
不是内置函数,所以它不具有 getopts
的一些自动优势,例如明智地处理空格。使用 getopts
,Bash shell 正在运行您的脚本,Bash shell 正在执行选项解析。您不需要调用外部程序来处理解析。
权衡是 getopts
不处理双虚线、长格式的选项名称。因此,您可以使用格式类似于 -w
的选项,但不能使用“---wide-format
”。另一方面,如果您的脚本接受选项 -a
、 -b
和 -c
、getopts
让您可以组合它们,例如 -abc
、-bca
或 -bac
等等。
我们将在本文中讨论和演示 getopts
,因此请确保将最后的“s”添加到命令名称中。
快速回顾:处理参数值
此脚本不使用像 -a
或 -b
这样的虚线选项。它确实在命令行上接受“正常”参数,并且这些参数在脚本内部作为值进行访问。
#!/bin/bash
# get the variables one by one
echo "Variable One: $1"
echo "Variable Two: $2"
echo "Variable Three: $3"
# loop through the variables
for var in "$@" do
echo "$var"
done
这些参数在脚本中作为变量 $1
、$2
或 $3
进行访问。
将此文本复制到编辑器中并将其保存为名为“variables.sh”的文件。我们需要使用 chmod
命令使其可执行。您需要为我们讨论的所有脚本执行此步骤。只需每次替换适当脚本文件的名称即可。
chmod +x variables.sh

如果我们不带参数运行我们的脚本,我们会得到这个输出。
./variables.sh

我们没有传递任何参数,因此脚本没有要报告的值。这次我们提供一些参数。
./variables.sh how to geek

正如预期的那样,变量 $1
、$2
和 $3
已设置为参数值,我们可以看到它们已打印出来。
这种一对一的参数处理意味着我们需要提前知道会有多少个参数。脚本底部的循环不关心有多少参数,它总是遍历所有参数。
如果我们提供第四个参数,它不会分配给变量,但循环仍会处理它。
./variables.sh how to geek website

如果我们将两个单词用引号括起来,它们将被视为一个参数。
./variables.sh how "to geek"

如果我们需要我们的脚本来处理选项、带参数的选项和“正常”数据类型参数的所有组合,我们将需要将选项与常规参数分开。我们可以通过将所有选项(带或不带参数)放在常规参数之前来实现。
但是,在我们会走之前,我们不要跑。让我们看一下处理命令行选项的最简单情况。
处理选项
我们在 while
循环中使用 getopts
。循环的每次迭代都对传递给脚本的一个选项起作用。在每种情况下,变量 OPTION
都设置为由 getopts
标识的选项。
随着循环的每次迭代,getopts
移动到下一个选项。当没有更多选项时,getopts
返回 false
并且 while
循环退出。
OPTION
变量与每个 case 语句子句中的模式相匹配。因为我们使用的是 case 语句,所以在命令行上提供选项的顺序无关紧要。每个选项都被放入 case 语句中,并触发适当的子句。
case 语句中的各个子句使得在脚本中执行特定于选项的操作变得容易。通常,在真实世界的脚本中,您会在每个子句中设置一个变量,这些变量将在脚本中进一步充当标志,允许或拒绝某些功能。
将此文本复制到编辑器中并将其保存为名为“options.sh”的脚本,并使其可执行。
#!/bin/bash
while getopts 'abc' OPTION; do
case "$OPTION" in
a)
echo "Option a used" ;;
b)
echo "Option b used"
;;
c)
echo "Option c used"
;;
?)
echo "Usage: $(basename $0) [-a] [-b] [-c]"
exit 1
;;
esac
done
这是定义 while 循环的行。
while getopts 'abc' OPTION; do
getopts
命令后跟选项字符串。这列出了我们将用作选项的字母。只有此列表中的字母才能用作选项。所以在这种情况下,-d
将无效。这将被 ?)
子句捕获,因为 getopts
为未识别的选项返回一个问号“?
”。如果发生这种情况,正确的用法将打印到终端窗口:
echo "Usage: $(basename $0) [-a] [-b] [-c]"
按照惯例,在这种类型的正确用法消息中,将选项包装在方括号“[]
”中意味着该选项是可选的。 basename 命令从文件名中去除任何目录路径。脚本文件名保存在 Bash 脚本的 $0
中。
让我们将此脚本与不同的命令行组合一起使用。
./options.sh -a
./options.sh -a -b -c
./options.sh -ab -c
./options.sh -cab

正如我们所看到的,我们所有的选项测试组合都被正确解析和处理。如果我们尝试一个不存在的选项怎么办?
./options.sh -d

触发了 usage 子句,这很好,但我们也从 shell 中得到一条错误消息。这对您的用例可能重要,也可能不重要。如果您从另一个必须解析错误消息的脚本调用该脚本,那么如果 shell 也生成错误消息,这将变得更加困难。
关闭 shell 错误消息非常容易。我们需要做的就是将冒号“:
”作为选项字符串的第一个字符。
要么编辑“options.sh”文件并添加一个冒号作为选项字符串的第一个字符,要么将此脚本保存为“options2.sh”,并使其可执行。
#!/bin/bash
while getopts ':abc' OPTION; do
case "$OPTION" in
a)
echo "Option a used"
;;
b)
echo "Option b used"
;;
c)
echo "Option c used"
;;
?)
echo "Usage: $(basename $0) [-a] [-b] [-c]"
exit 1
;;
esac
done
当我们运行它并产生错误时,我们会收到我们自己的错误消息,而没有任何 shell 消息。
./options2.sh.sh -d

将 getopts 与选项参数一起使用
要告诉 getopts
一个选项后面将跟一个参数,请在选项字符串中紧跟在选项字母后面放置一个冒号“:
”。
如果我们在选项字符串中的“b”和“c”后面加上冒号,getopt
将期望这些选项的参数。将此脚本复制到您的编辑器中并将其保存为“arguments.sh”,并使其可执行。
请记住,选项字符串中的第一个冒号用于抑制 shell 错误消息——它与参数处理无关。
当 getopt
处理带有参数的选项时,该参数被放置在 OPTARG
变量中。如果你想在脚本的其他地方使用这个值,你需要将它复制到另一个变量。
#!/bin/bash
while getopts ':ab:c:' OPTION; do
case "$OPTION" in
a)
echo "Option a used"
;;
b)
argB="$OPTARG"
echo "Option b used with: $argB"
;;
c)
argC="$OPTARG"
echo "Option c used with: $argC"
;;
?)
echo "Usage: $(basename $0) [-a] [-b argument] [-c argument]"
exit 1
;;
esac
done
让我们运行它,看看它是如何工作的。
./arguments.sh -a -b "how to geek" -c reviewgeek
./arguments.sh -c reviewgeek -a

所以现在我们可以处理带参数或不带参数的选项,而不管它们在命令行中给出的顺序如何。
但是常规参数呢?我们之前说过,我们知道我们必须在任何选项之后将它们放在命令行上。让我们看看如果我们这样做会发生什么。
混合选项和参数
我们将更改之前的脚本以包含更多行。当 while
循环退出并且所有选项都已处理后,我们将尝试访问常规参数。我们将打印出 $1
中的值。
将此脚本保存为“arguments2.sh”,并使其可执行。
#!/bin/bash
while getopts ':ab:c:' OPTION; do
case "$OPTION" in
a)
echo "Option a used"
;;
b)
argB="$OPTARG"
echo "Option b used with: $argB"
;;
c)
argC="$OPTARG"
echo "Option c used with: $argC"
;;
?)
echo "Usage: $(basename $0) [-a] [-b argument] [-c argument]"
exit 1
;;
esac
done
echo "Variable one is: $1"
现在我们将尝试一些选项和参数的组合。
./arguments2.sh dave
./arguments2.sh -a dave
./arguments2.sh -a -c how-to-geek dave

所以现在我们可以看到问题所在。一旦使用了任何选项,$1
之后的变量就会填充选项标志及其参数。在最后一个示例中,$4
将保存参数值“dave”,但是如果您不知道将使用多少选项和参数,您如何在脚本中访问它?
答案是使用 OPTIND
和 shift
命令。
shift
命令从参数列表中丢弃第一个参数(无论类型如何)。其他参数“向下排列”,因此参数 2 变为参数 1,参数 3 变为参数 2,依此类推。所以 $2
变成了 $1
, $3
变成了 $2
,等等。
如果您为 shift
提供一个数字,它将从列表中删除那么多参数。
OPTIND
在找到和处理选项和参数时对其进行计数。处理完所有选项和参数后,OPTIND
将比选项的数量多一个。因此,如果我们使用 shift 从参数列表中删除 (OPTIND-1)
参数,我们将在 $1
中留下常规参数。
这正是这个脚本所做的。将此脚本保存为“arguments3.sh”并使其可执行。
#!/bin/bash
while getopts ':ab:c:' OPTION; do
case "$OPTION" in
a)
echo "Option a used"
;;
b)
argB="$OPTARG"
echo "Option b used with: $argB"
;;
c)
argC="$OPTARG"
echo "Option c used with: $argC"
;;
?)
echo "Usage: $(basename $0) [-a] [-b argument] [-c argument]"
exit 1
;;
esac
done
echo "Before - variable one is: $1"
shift "$(($OPTIND -1))"
echo "After - variable one is: $1"
echo "The rest of the arguments (operands)"
for x in "$@"
do
echo $x
done
我们将使用选项、参数和参数的组合来运行它。
./arguments3.sh -a -c how-to-geek "dave dee" dozy beaky mick tich

我们可以看到在我们调用 shift
之前,$1
持有“-a”,但是在 shift 命令之后 $1
持有我们的第一个非选项,非参数参数。我们可以像在没有选项解析的脚本中一样轻松地遍历所有参数。
有选择总是好的
在脚本中处理选项及其参数并不需要很复杂。使用 getopts
,您可以创建处理命令行选项、参数和参数的脚本,就像符合 POSIX 标准的本机脚本一样。