在本章中,我们将讨论向脚本传递参数的不同方式,特别关注标志。我们将从重述位置参数开始,然后继续作为标志传递的参数。在此之后,我们将讨论如何使用getopts
shell 内置在您自己的脚本中使用标志。
本章将介绍以下命令:getopts
和shift
。
本章将涵盖以下主题:
- 位置参数与标志
getopts
Shell 内置
本章的所有脚本都可以在 GitHub 上找到,链接如下:https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter15 。跟随你的 Ubuntu Linux 虚拟机上的例子——不需要其他资源。对于 single-flag.sh 脚本,只有最终版本可以在网上找到。在您的系统上执行之前,请确保验证标题中的脚本版本。
我们将在本章开始时简要回顾一下位置论点。您可能还记得第 8 章变量和用户输入,我们能够使用位置参数向脚本传递参数。
简单地说,使用以下语法:
bash script.sh argument1 argument2 ...
在前面的(虚拟的)script.sh
中,我们可以通过查看参数的位置来获取用户提供的值:$1
是第一个参数,$2
是第二个参数,依此类推。请记住$0
是一个特殊的论点,它与剧本的名字有关:在这种情况下,script.sh
。
这种方法相对简单,但也容易出错。当您编写这个脚本时,您需要广泛检查用户提供的输入;他们是否给出了足够的论据,但不是太多?或者,也许有些参数是可选的,所以一些组合是可能的?所有这些事情都需要考虑,如果可能的话,都需要处理。
再说剧本作者(你!),还有脚本调用者的负担。在他们能够成功调用您的脚本之前,他们需要知道如何传递所需的信息。对于我们的脚本,我们应用了两种做法,旨在最大限度地减少用户的负担:
- 我们的脚本头包含一个
Usage:
字段 - 当我们的脚本被错误调用时,我们会打印一条错误消息,其中带有与标题相似/相等的用法提示
然而,这种方法容易出错,并且不总是非常用户友好的。不过还有另一种选择:选项,更常见的是旗帜。
也许您还没有意识到这一点,但是您在命令行上使用的大多数命令都使用位置参数和标志的组合。Linux 中最基本的命令cd
使用一个位置参数:您想要移动到的目录。
它实际上有两个标志,你也可以使用:-L
和-P
。这些旗帜的目的是小众的,不值得在这里解释。几乎所有命令都互补地使用标志和位置参数。
那么,我们什么时候用哪个?根据经验,旗帜通常用于修饰符,而位置参数用于目标。目标很简单这就是:你想用命令操纵的东西。在ls
的情况下,这意味着位置参数是应该由命令列出(操作)的文件或目录。
对于ls -l /tmp/
命令,/tmp/
为目标,-l
为用于修改ls
行为的标志。默认情况下,ls
列出所有没有额外信息的文件,如所有权、权限、大小等。如果我们想修改ls
的行为,我们可以添加一个或多个标志:-l
告诉ls
使用长列表格式,该格式在自己的行上打印每个文件,并打印关于该文件的额外信息。
注意在ls /tmp/
和ls -l /tmp/
之间,目标不变,但是输出变,因为我们用旗帜修改了!
有些旗帜更为特殊:它们需要自己的位置参数!因此,我们不仅可以使用标志来修改命令,而且标志本身也有多个选项来修改命令的行为。
一个很好的例子是find
命令:默认情况下,它查找一个目录中的所有文件,如下所示:
reader@ubuntu:~/scripts/chapter_14$ find
.
./reverser-crontab
./wall.txt
./base-crontab
./date-redirection-crontab
或者,我们可以使用带有位置参数的find
不在当前工作目录中搜索,而是在其他地方搜索,如下所示:
reader@ubuntu:~/scripts/chapter_14$ find ../chapter_10
../chapter_10
../chapter_10/error.txt
../chapter_10/grep-file.txt
../chapter_10/search.txt
../chapter_10/character-class.txt
../chapter_10/grep-then-else.sh
现在,find
也允许我们使用-type
标志只打印某一类型的文件。但是仅使用-type
标志,我们还没有指定要打印的文件类型。通过在标志后直接指定文件类型(这里排序是关键的,我们告诉标志要寻找什么。它看起来如下所示:
reader@ubuntu:/$ find /boot/ -type d
/boot/
/boot/grub
/boot/grub/i386-pc
/boot/grub/fonts
/boot/grub/locale
这里我们在/boot/
目录中寻找一种类型的d
(目录)。对-type
旗帜的其他争论包括f
(文件)、l
(符号链接)和b
(阻挡装置)。
一如既往,订购很重要,如果您没有正确订购,就会发生类似的情况:
reader@ubuntu:/$ find -type d /boot/
find: paths must precede expression: '/boot/'
find: possible unquoted pattern after predicate '-type'?
对我们来说不幸的是,并非所有的命令都是平等的。有些人对用户更宽容,并尽最大努力理解作为输入给出的内容。其他人要严格得多:他们将运行任何通过的东西,即使它没有任何功能意义。请务必确认您是否正确使用了该命令及其修饰符!
The preceding examples use flags differently to how we'll learn to use them with getopts
. These examples should only serve to illustrate the concepts of script arguments, flags, and flags-with-arguments. These implementations are written without the use of getopts
and thus do not map precisely to what we'll be doing later.
现在真正的乐趣开始了!在本章的第二部分,我们将解释内置的getopts
Shell。getopts
命令用于脚本的开头,以获取您以旗帜形式提供的options*******。它有一个非常具体的语法,一开始看起来会很混乱,但是,一旦我们全面了解了它,它应该不会太复杂,让您难以理解。*
**不过,在我们深入讨论之前,我们需要讨论两件事:
getopts
和getopt
的区别- 短期与长期选择
如前所述,getopts
是一个Shell 内置。它在普通的伯恩 Shell(sh
)和 Bash 中都有。它起源于 1986 年左右,作为 1980 年前创建的getopt
的替代品。
与getopts
不同的是,getopt
并没有内置在 Shell 中:它是一个独立的程序,已经被移植到许多不同的 Unix 和类似 Unix 的发行版中。getopts
和getopt
的主要区别如下:
getopt
不能很好地处理空标志参数;getopts
确实getopts
包含在伯恩 Shell 和 Bash 中;getopt
需要单独安装getopt
允许解析长选项(--help
而不是-h
);getopts
不getopts
有更简单的语法;getopt
比较复杂(主要是因为是外部程序,不是内建)
总的来说,共识似乎是大多数情况下,使用getopts
更可取(除非你真的想要长选项)。由于getopts
是一个内置的 Bash,我们也将使用它,特别是因为我们不需要长选项。
您在终端上使用的大多数命令都有短选项(在终端上交互工作时几乎总是使用它来节省时间)和长选项(它更具描述性,更适合创建可读性更好的脚本)。根据我们的经验,短选项更普遍,如果使用正确,也更容易识别。
下面的列表显示了最常见的短标志,它们对大多数命令都是一样的:
-h
:打印命令的帮助/用法-v
:使命令冗长-q
:使命令安静-f <file>
:将文件传递给命令-r
:递归执行操作-d
:在调试模式下运行命令
Do not assume all commands parse the short flags, as specified previously. While this is true for most commands, don't all follow these trends. What is printed here has been found from personal experience and should always be verified by you before running a command that is new to you. That being said, running a command without arguments/flags or with a -h
will, at least 90% of the time, print the correct usage for you to admire.
尽管为我们的getopts
脚本提供长选项会很好,但是即使是长选项也不能代替编写可读的脚本和为使用您的脚本的用户创建良好的提示。我们觉得这比拥有长选项重要得多!除此之外,getopts
的语法比类似的getopt
干净多了,坚持 KISS 原则仍然是我们的目标之一。
我们不再花更多的时间在这一章看不到实际的代码,而是直接跳进去展示一个非常简单的getopts
脚本的例子。当然,我们会一步一步地给你讲解,让你有机会了解全部。
我们正在创建的脚本只做一些简单的事情:如果它找到了-v
标志,它会打印一条冗长的消息,告诉我们它找到了标志。如果它找不到任何标志,则不打印任何内容。如果它发现任何其他标志,它会为用户打印一个错误。很简单,对吧?
让我们来看看:
reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh
reader@ubuntu:~/scripts/chapter_15$ cat !$
cat single-flag.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-12-08
# Description: Shows the basic getopts syntax.
# Usage: ./single-flag.sh [flags]
#####################################
# Parse the flags in a while loop.
# After the last flag, getopts returns false which ends the loop.
optstring=":v"
while getopts ${optstring} options; do
case ${options} in
v)
echo "-v was found!"
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
如果我们运行这个脚本,我们将看到以下情况:
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh # No flag, do nothing.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -p
Invalid option: -p. # Wrong flag, print an error.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v
-v was found! # Correct flag, print the message.
所以,我们的剧本至少能达到预期效果!但是为什么会这样呢?让我们来看看。我们将跳过标题,因为现在应该很清楚了。我们将从while
行开始,它包含getopts
命令和optstring
:
# Parse the flags in a while loop.
# After the last flag, getopts returns false which ends the loop.
optstring=":v"
while getopts ${optstring} options; do
optstring
,可能是 opt 离子字符串** 的缩写,告诉getopts
应该预期哪些选项。在这种情况下,我们期待的只有v
。但是,我们以冒号(:
)开始optstring
,这是optstring
的特殊字符,将getopts
设置为无声错误报告模式。
因为我们更喜欢自己处理错误情况,所以我们总是以冒号开始我们的optstring
。然而,请随意查看当您移除结肠时会发生什么。
之后,getopts
的语法非常简单,如下所示:
getopts optstring name [arg]
我们可以看到这个命令,后跟optstring
(为了提高可读性,我们将其抽象为一个单独的变量),以存储解析结果的变量的名称结束。
getopts
的最后一个可选方面允许我们传递自己的一组参数,而不是默认传递给脚本的所有内容(0 到 9 美元)。我们在练习中不需要/使用这个,但是知道这个肯定很好。像往常一样,因为这是一个内置的 Shell,所以您可以通过执行help getopts
在上面找到信息。
我们将这个命令放在一个while
循环中,这样它就遍历了我们传递给脚本的所有参数。如果没有更多的参数需要getopts
解析,它将返回除0
之外的退出状态,这将导致while
循环退出。
然而,当我们处于循环中时,我们将触及case
语句。如您所知,case
语句基本上是更长的if-elif-elif-elif-else
语句的更好语法。在我们的示例脚本中,如下所示:
case ${options} in
v)
echo "-v was found!"
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
注意case
语句是如何以单词esac
结束的(大小写相反)。对于我们定义的所有标志(目前只有-v
,我们有一个代码块将只为该标志执行。
当我们查看${options}
变量时,我们发现的另一个东西是?
通配符(因为我们在getopts
命令中为名称指定了该变量)。我们把它放在案例陈述的末尾,作为捕捉错误的一种手段。如果它遇到了?)
代码块,我们已经向getopts
展示了一个它不理解的标志。在这种情况下,我们打印一个错误并退出脚本。
最后一行的done
结束了while
循环,并表示我们所有的标志都应该被处理了。
It might seem a bit unnecessary to have both an optstring
and a case for all possible options. For now, this is indeed the case, but a bit further on in this chapter we'll show you how the optstring
is used to specify things beyond just the letter; at that point, it should be clear why the optstring
is here. For now, don't worry about it too much and just enter the flags in both locations.
对我们来说幸运的是,我们不必满足于一面旗帜:我们可以定义很多(直到我们用完字母表!).
我们将创建一个新的脚本,向读者打印一条消息。如果没有指定标志,我们将打印一条默认消息。如果我们遇到标志-b
或标志-g
,我们将根据标志打印不同的消息。我们还将包括对-h
标志的说明,当遇到时,它将打印一条帮助消息。
具有这些要求的脚本可能如下所示:
reader@ubuntu:~/scripts/chapter_15$ vim hey.sh
reader@ubuntu:~/scripts/chapter_15$ cat hey.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-12-14
# Description: Getopts with multiple flags.
# Usage: ./hey.sh [flags]
#####################################
# Abstract the help as a function, so it does not clutter our script.
print_help() {
echo "Usage: $0 [flags]"
echo "Flags:"
echo "-h for help."
echo "-b for male greeting."
echo "-g for female greeting."
}
# Parse the flags.
optstring=":bgh"
while getopts ${optstring} options; do
case ${options} in
b)
gender="boy"
;;
g)
gender="girl"
;;
h)
print_help
exit 0 # Stop script, but consider it a success.
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
# If $gender is n (nonzero), print specific greeting.
# Otherwise, print a neutral greeting.
if [[ -n ${gender} ]]; then
echo "Hey ${gender}!"
else
echo "Hey there!"
fi
在这一点上,这个脚本应该是可读的,尤其是包含注释。从顶部开始,我们从标题开始,然后是print_help()
功能,当遇到-h
标志时,它会打印我们的帮助消息(正如我们在后面看到的几行)。
接下来是optstring
,它仍然以冒号开头,因此getopts
的详细错误被关闭(我们将自己处理这个问题)。在optstring
中,我们要处理的三个旗帜,即-b
、-g
和-h
,都被定义为一个单独的字符串:bgh
。
对于这些标志中的每一个,我们在case
语句中都有一个条目:对于b)
和g)
,分别将gender
变量设置为boy
或girl
。对于h)
,在调用exit 0
之前,调用我们定义的函数。(想想我们为什么要这么做!如果您不确定,请不要退出运行脚本。)
我们总是通过使用?)
语法处理未知标志来结束getopts
块。
继续,在我们的case
语句以esac
结束后,我们进入实际的功能。我们检查是否定义了gender
变量:如果是,我们打印一条包含根据标志设置的值的消息。如果未设置(如果既未指定-b
也未指定-g
,则属于这种情况),我们会打印省略性别的通用问候语。
这也是为什么我们在找到-h
后exit 0
:否则帮助信息和问候都会给用户(这很奇怪,因为用户要求只是用-h
的帮助页面)。
让我们看看我们的剧本:
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -h
Usage: hey.sh [flags]
Flags:
-h for help.
-b for male greeting.
-g for female greeting.
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh
Hey there!
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b
Hey boy!
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -g
Hey girl!
到目前为止,一切顺利!如果我们用-h
来称呼它,我们会看到多行帮助消息被打印出来。默认情况下,每个echo
以一个换行符结束,所以我们的五个回声打印在五行上。我们可以只使用一个echo
和\n
字符,但是这样更易读
如果我们在没有标志的情况下运行我们的脚本,我们将看到通用的问候。用-b
或-g
运行它会给出特定性别的问候。那不是很容易吗?
实际上是!然而,它即将变得稍微复杂一点。正如我们之前所解释的,用户往往相当不可预测,可能会使用太多的标志,或者多次使用相同的标志。
让我们看看我们的脚本对此的反应:
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -h -b
Usage: hey.sh [flags]
Flags:
-h for help.
-b for male greeting.
-g for female greeting.
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -h
Usage: hey.sh [flags]
Flags:
-h for help.
-b for male greeting.
-g for female greeting.
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -h -g
Usage: hey.sh [flags]
Flags:
-h for help.
-b for male greeting.
-g for female greeting.
所以,不管指定了多少个标志,只要脚本遇到-h
标志,就会打印帮助消息并退出(由于exit 0
)。为了您的理解,在调试中使用bash -x
运行前面的命令,以查看它们实际上是否不同,即使用户没有看到这一点(提示:检查gender=boy
和gender=girl
的分配)。
这给我们带来了一个重要的观点:*标志按照用户提供的顺序进行解析!*为了进一步说明这一点,我们来看另一个用户摆弄标志的例子:
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -g -b
Hey boy!
reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -g
Hey girl!
当用户同时提供-b
和-g
标志时,性别的两个变量分配都由系统执行。然而,看起来好像最终的标志是获胜的,尽管我们刚刚声明标志是按顺序解析的!为什么会这样?
一如既往,一个漂亮的bash -x
让我们对这种情况有了一个很好的了解:
reader@ubuntu:~/scripts/chapter_15$ bash -x hey.sh -b -g
+ optstring=:bgh
+ getopts :bgh options
+ case ${options} in
+ gender=boy
+ getopts :bgh options
+ case ${options} in
+ gender=girl
+ getopts :bgh options
+ [[ -n girl ]]
+ echo 'Hey girl!'
Hey girl!
最初,gender
变量被赋予boy
的值。然而,当解析下一个标志时,变量的值被新值、girl
覆盖。由于-g
标志是最后一个,gender
变量以girl
结束,因此这就是打印的内容。
正如您将在本章的下一部分看到的,可以为标志提供一个参数。但是,对于没有参数的标志,有一个非常酷的特性,许多命令都使用:标志链接。这听起来可能很复杂,但实际上很简单:如果你有多个标志,你可以把它们都放在一个破折号后面。
对于我们的脚本,它看起来像这样:
reader@ubuntu:~/scripts/chapter_15$ bash -x hey.sh -bgh
+ optstring=:bgh
+ getopts :bgh options
+ case ${options} in
+ gender=boy
+ getopts :bgh options
+ case ${options} in
+ gender=girl
+ getopts :bgh options
+ case ${options} in
+ print_help
<SNIPPED>
我们将所有旗帜指定为一束:我们使用了-bgh
而不是-b -g -h
。正如我们之前得出的结论,标记是按顺序处理的,在我们的串联示例中仍然如此(正如调试指令清楚显示的那样)。例如,这与ls -al
没有太大区别。同样,这仅在标志没有参数的情况下有效。
在optstring
中,冒号除了关闭详细的错误记录之外还有一个额外的含义:当放在一个字母之后时,它向getopts
发出信号,表示需要一个选项参数。
如果我们回顾我们的第一个例子,optstring
只是:v
。如果我们想让-v
旗帜接受一个论点,我们可以在v
后面加上一个冒号,这样就会产生下面的optstring
: :v:
。然后,我们可以使用我们之前见过的特殊变量OPTARG
,来获取optionarg*ument*。
我们将修改我们的single-flag.sh
脚本,向您展示这是如何工作的:
reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh
reader@ubuntu:~/scripts/chapter_15$ cat single-flag.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.1.0
# Date: 2018-12-14
# Description: Shows the basic getopts syntax.
# Usage: ./single-flag.sh [flags]
#####################################
# Parse the flags in a while loop.
# After the last flag, getopts returns false which ends the loop.
optstring=":v:"
while getopts ${optstring} options; do
case ${options} in
v)
echo "-v was found!"
echo "-v option argument is: ${OPTARG}."
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
为了您的方便,已更改的行已突出显示。通过在optstring
中添加一个冒号,并在v)
块中使用OPTARG
变量,我们现在可以看到运行脚本时的以下行为:
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v Hello
-v was found!
-v option argument is: Hello.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -vHello
-v was found!
-v option argument is: Hello.
如您所见,只要我们提供 flag 和 flag 参数,我们的脚本就可以正常工作。我们甚至不需要在 flag 和 flag 参数之间留一个空格;由于getopts
知道一个参数是预期的,它可以处理一个空格或者不处理空格。我们总是建议在任何情况下都包含空格,以确保可读性,但在技术上并不需要。
此外,这也证明了为什么我们需要一个单独的optstring
:case
的说法是一样的,但是getopts
现在期待一个争论,如果创作者省略了optstring
的话,我们不可能做到这一点。
就像所有看起来好得难以置信的事情一样,这是其中一种情况。如果用户对您的脚本很好,这很好,但是如果他/她对您的脚本不好,可能会发生以下情况:
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v
Invalid option: -v.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v ''
-v was found!
-v option argument is:
现在我们已经告诉getopts
期待对-v
标志的一个参数,如果没有参数,它实际上将不能正确识别标志。然而,第二个脚本调用中的''
所表示的空参数是可以的。(技术上没问题,也就是说,因为没有用户会这么做。)
幸运的是,对此有一个解决方案——:)
块,如下所示:
reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh
reader@ubuntu:~/scripts/chapter_15$ cat single-flag.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.2.0
# Date: 2018-12-14
# Description: Shows the basic getopts syntax.
# Usage: ./single-flag.sh [flags]
#####################################
# Parse the flags in a while loop.
# After the last flag, getopts returns false which ends the loop.
optstring=":v:"
while getopts ${optstring} options; do
case ${options} in
v)
echo "-v was found!"
echo "-v option argument is: ${OPTARG}."
;;
:)
echo "-${OPTARG} requires an argument."
exit 1
;;
?)
echo "Invalid option: -${OPTARG}."
exit 1
;;
esac
done
错误的标志和丢失的选项参数都被解析为OPTARG
,这可能有点令人困惑。在不使这种情况变得更加复杂的情况下,这完全取决于此时的case
语句块是包含?)
还是:)
。对于?)
块,所有未被识别的内容(整个标志)都被视为选项参数,并且:)
块仅在optstring
包含带有参数的选项的正确指令时触发。
现在一切都应该按计划进行:
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v
-v requires an argument.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v Hi
-v was found!
-v option argument is: Hi.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -x Hi
Invalid option: -x.
reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -x -v Hi
Invalid option: -x.
同样,由于标志的顺序处理,由于?)
块中的exit 1
,最终调用从未到达-v
标志。然而,所有其他情况现在都得到了妥善解决。很好!
The actual processing that getopts
does involves multiple passes and the use of shift
. This is a little too technical for this chapter, but for those curious among you, the Further reading section includes a very in-depth explanation of this mechanism that you can read at your leisure.
可以将位置参数(以我们在本章之前使用它们的方式)与选项和选项参数相结合。在这种情况下,需要考虑一些事情:
- 默认情况下,Bash 将
-f
等标志识别为位置参数 - 正如标志和标志参数有顺序一样,标志和位置参数也有顺序
当处理getopts
和位置参数的混合时,*标志和标志选项应该总是在位置参数之前提供!*这是因为我们希望在获取位置参数之前解析和处理所有标志和标志参数。对于脚本和命令行工具来说,这是一个相当典型的场景,但这仍然是我们必须考虑的事情。
一如既往,所有上述观点都可以通过一个例子得到最好的说明。我们将创建一个简单的脚本,作为常见文件操作的包装器。有了这个脚本,file-tool.sh
,我们将能够做以下事情:
- 列出文件(默认行为)
- 删除文件(使用
-d
选项) - 清空文件(使用
-e
选项) - 重命名文件(使用
-m
选项,包括另一个文件名) - 调用帮助功能(用
-h
)
看一下脚本:
reader@ubuntu:~/scripts/chapter_15$ vim file-tool.sh
reader@ubuntu:~/scripts/chapter_15$ cat file-tool.sh
#!/bin/bash
#####################################
# Author: Sebastiaan Tammer
# Version: v1.0.0
# Date: 2018-12-14
# Description: A tool which allows us to manipulate files.
# Usage: ./file-tool.sh [flags] <file-name>
#####################################
print_help() {
echo "Usage: $0 [flags] <file-name>"
echo "Flags:"
echo "No flags for file listing."
echo "-d to delete the file."
echo "-e to empty the file."
echo "-m <new-file-name> to rename the file."
echo "-h for help."
}
command="ls -l" # Default command, can be overridden.
optstring=":dem:h" # The m option contains an option argument.
while getopts ${optstring} options; do
case ${options} in
d)
command="rm -f";;
e)
command="cp /dev/null";;
m)
new_filename=${OPTARG}; command="mv";;
h)
print_help; exit 0;;
:)
echo "-${OPTARG} requires an argument."; exit 1;;
?)
echo "Invalid option: -${OPTARG}." exit 1;;
esac
done
# Remove the parsed flags from the arguments array with shift.
shift $(( ${OPTIND} - 1 )) # -1 so the file-name is not shifted away.
filename=$1
# Make sure the user supplied a writable file to manipulate.
if [[ $# -ne 1 || ! -w ${filename} ]]; then
echo "Supply a writable file to manipulate! Exiting script."
exit 1
fi
# Everything should be fine, execute the operation.
if [[ -n ${new_filename} ]]; then # Only set for -m.
${command} ${filename} $(dirname ${filename})/${new_filename}
else # Everything besides -m.
${command} ${filename}
fi
那是一个大的,不是吗?我们通过将多行压缩成单行(在case
语句中)的方式稍微缩短了一点,但它仍然不是一个短脚本。虽然一开始看起来很吓人,但我们确信,随着你到目前为止的曝光,以及剧本中的评论,你应该可以理解这一点。如果现在还不能完全理解,别担心——我们现在要解释所有新的有趣的台词。
我们跳过了标题、print_help()
功能和ls -l
的默认命令。第一个有趣的部分是optstring
,它现在包含有和没有选项参数的选项:
optstring=":dem:h" # The m option contains an option argument.
当我们到达m)
块时,我们将选项参数保存在new_filename
变量中以备后用。
当我们完成对getopts
的case
语句时,我们会遇到一个我们之前短暂看到过的命令:shift
。这个命令允许我们移动位置参数:如果我们做shift 2
,参数$4
变成$2
,参数$3
变成$1
,旧的$1
和$2
被移除。
当处理标志后面的位置参数时,所有标志和标志参数也被视为位置参数。在这种情况下,如果我们将脚本称为file-tool.sh -m newfile /tmp/oldfile
,Bash 将解释如下内容:
$1
:解释为-m
$2
:解释为新文件$3
:解释为/tmp/oldfile
幸运的是,getopts
将处理过的选项(和选项参数)保存在变量中:$OPTIND
(来自optionind*ex*)。更准确地说,在解析了一个选项后,它将$OPTIND
设置为下一个可能的选项或选项参数:它从 1 开始,在找到传递给脚本的第一个非选项参数时结束。
在我们的例子中,一旦getopts
到达我们的位置参数/tmp/oldfile
,则$OPTIND
变量将是3
。由于我们只需要将shift
之前的所有点去掉,我们从$OPTIND
中减去 1,如下所示:
shift $(( ${OPTIND} - 1 )) # -1 so the file-name is not shifted away.
记住,$(( ... ))
是算术的简写;产生的数字用于shift
命令。脚本的其余部分非常简单:我们将做一些检查,以确保我们只剩下一个位置参数(我们想要操作的文件的文件名),以及我们是否对该文件拥有写权限。
接下来,根据我们选择的操作,我们要么为mv
做一个复杂的操作,要么为所有其他操作做一个简单的操作。对于 rename 命令,我们将使用一点命令替换来确定原始文件名的目录名,然后我们将在 rename 中重用它。
如果我们像我们应该做的那样做我们的测试,脚本应该能够完全满足我们设定的所有需求。我们鼓励你试一试。
更好的是,看看你能否想出一个我们没有想到的破坏脚本功能的情况。如果你真的发现了什么(剧透提醒:我们知道一些缺点!),尽量自己修。
As you may start to realize, we're entering territory in which it is very hard to harden scripts for every user input. For example, in the last example, if we supply the -m
option but omit the content, the filename we supply will be seen as the option argument. In this case, instead of throwing an error for a missing option argument, our script will shift
the filename away and complain that it doesn't have it. While this script should serve for educational purposes, it is not something that we would trust for our workplace scripting. It is often better not to mix getopts
with positional arguments, as you would avoid many of the complexities we've faced here. Just have the user supply the filename as another option argument (-f
, anyone?) and you'll be much happier!
本章首先回顾了在 Bash 中如何使用位置参数。我们继续向您展示了我们到目前为止引入的大多数命令行工具(以及我们还没有引入的工具)如何使用标志,通常作为脚本功能的修饰符,而位置参数用于指示命令的目标。
然后,我们为读者介绍了一种在他们自己的脚本中合并选项和选项参数的方法:通过使用getopts
shell 内建。我们首先讨论了遗留程序getopt
和较新的内置程序getopts
之间的差异,这是本章剩余部分的重点。
由于getopts
只允许我们使用短选项(而getopt
和其他一些命令行工具也使用长选项,用双破折号表示),我们向您展示了这是如何由于对常见短选项(如-h
、-v
等)的认可而不成为问题的。
我们用几个例子恰当地介绍了getopts
语法。我们展示了如何使用带和不带标志参数的标志,以及我们如何需要一个optstring
来向getopts
发出信号,说明哪些选项有参数(甚至哪些选项是可以期待的)。
我们在本章的最后向您展示了如何通过巧妙地使用shift
命令来处理这个问题,从而将选项和选项参数与位置参数相结合。
本章介绍了以下命令:getopts
和shift
。
- 为什么标志经常被用作修饰符,而位置参数被用作目标?
- 为什么我们在
while
循环中运行getopts
? - 为什么我们在
case
语句中需要?)
? - 为什么我们(有时)需要
case
语句中的:)
? - 如果我们正在解决所有选项,为什么我们需要一个单独的
optstring
? - 为什么在
shift
中使用OPTIND
变量时需要减去 1? - 将选项与位置参数混合是个好主意吗?
有关本章主题的更多信息,请参考以下链接:
- 迎头痛击黑客在
getopts
:http://wiki.bash-hackers.org/howto/getopts_tutorial getopts
深度:https://www.computerhope.com/unix/bash/getopts.htm**