第三十二章:位置参数

到目前为止,我们的程序缺少的一个功能是接受和处理命令行选项和参数的能力。在本章中,我们将研究允许我们的程序访问命令行内容的shell功能。

第三十二章:位置参数访问命令行确定参数数量shift —— 获得许多参数简单应用程序在 Shell函数中使用位置参数处理位置参数 en Masse更完整的应用程序getopts 选项交互模式文件输出总结

访问命令行

shell提供了一组称为位置参数(positional parameters)的变量,其中包含命令行上的各个单词。变量的名称为0到9。它们可以这样演示:

这是一个简单的脚本,显示变量$0-$9的值。当在没有命令行参数的情况下执行时,结果如下:

即使没有提供参数,$0也将始终包含命令行上出现的第一个项,即正在执行的程序的路径名。当提供参数时,我们会看到以下结果:

注意:您实际上可以使用参数扩展访问九个以上的参数。要指定大于9的数字,请将数字括在大括号中,如${10}、${55}、${211}等。

确定参数数量

shell还提供了一个变量 $# ,其中包含命令行上的参数数量:

执行结果如下:

shift —— 获得许多参数

但是,当我们给程序提供大量参数时,会发生什么?

在这个示例系统中,通配符 * 扩展为82个参数。我们如何处理这么多?shell提供了一种方法,尽管很笨拙。每次执行 shift 命令时,所有参数都会“向下移动一个”。事实上,通过使用 shift ,可以只使用一个参数(除了永不改变的 $0 )。

每次执行 shift 时,$2的值都会移动到$1,$3的值会移动到$2,以此类推。 $# 的值也会减少1。

posit-param2 程序中,我们创建了一个循环,该循环计算剩余参数的数量,只要至少有一个参数就继续。我们显示当前参数,在循环的每次迭代中递增变量计数,以提供处理的参数数量的运行计数,最后,执行移位操作,用下一个参数加载 $1 。以下是工作中的程序:

简单应用程序

即使没有 shift ,也可以使用位置参数编写有用的应用程序。举个例子,这是一个简单的文件信息程序:

此程序显示指定文件的文件类型(由 file 命令确定)和文件状态(来自 stat 命令)。该程序的一个有趣特性是 PROGNAME 变量。它被赋予由 basename “$0” 命令产生的值。 basename 命令删除路径名的前导部分,只留下文件的基本名称。在我们的示例中, basename 删除了 $0 参数中包含的路径名的前导部分,即我们示例程序的完整路径名。在构建程序末尾的使用消息等消息时,此值非常有用。通过这种方式编码,可以重命名脚本,消息会自动调整以包含程序的名称。

在 Shell函数中使用位置参数

正如位置参数用于向shell脚本传递参数一样,它们也可以用于向shell函数传递参数。为了演示,我们将把 file_info 脚本转换为shell函数。

现在,如果包含 file_info shell函数的脚本使用文件名参数调用该函数,则该参数将传递给该函数。

有了这个功能,我们可以编写许多有用的shell函数,这些函数不仅可以在脚本中使用,还可以在 .bashrc 文件中使用。

请注意, PROGNAME 变量已更改为shell变量 FUNCNAME 。shell会自动更新此变量,以跟踪当前执行的shell函数。请注意, $0 始终包含命令行上第一个项目的完整路径名(即程序的名称),而不包含我们可能期望的shell函数的名称。

处理位置参数 en Masse

有时将所有位置参数作为一个组进行管理是有用的。例如,我们可能想为另一个程序编写一个“包装器”(wrapper)。这意味着我们创建了一个脚本或shell函数,简化了对另一个程序的调用。在这种情况下,包装器提供了一个神秘的命令行选项列表,然后将一个参数列表传递给低级程序。

shell为此提供了两个特殊参数。它们都扩展到完整的位置参数列表中,但在相当微妙的方面有所不同。如下表所示。

参数描述
$*展开位置参数列表,从1开始。当被双引号包围时,它会展开为一个包含所有位置参数的双引号字符串,每个参数由IFS shell变量的第一个字符(默认为空格字符)分隔。
$@展开位置参数列表,从1开始。当被双引号括起来时,它将每个位置参数展开为一个单独的单词,就像它被双引号包围一样。

下面是一个显示这些特殊参数的脚本:

在这个相当复杂的程序中,我们创建了两个参数:单词和带空格的单词,并将它们传递给 pass_params 函数。该函数进而使用具有特殊参数 $*$@ 的四种方法中的每一种将它们传递给 print_params 函数。执行时,脚本会显示差异。

使用我们的参数, $*$@ 都会产生四个单词的结果。

word words with spaces

“$*”产生一个单词的结果:

"word words with spaces"

“$@”产生两个单词的结果:

"word" "words with spaces"

这符合我们的实际意图。从中吸取的教训是,尽管shell提供了四种不同的获取位置参数列表的方法,但“$@”在大多数情况下是最有用的,因为它保留了每个位置参数的完整性。为了确保安全,应该始终使用它,除非我们有令人信服的理由不使用它。

更完整的应用程序

在长时间的中断之后,我们将继续我们的 sys_info_page 程序的工作,最后一次看到是在【第27章】。我们的下一个添加将向程序添加几个命令行选项,如下所示:

以下是实现命令行处理所需的代码:

首先,我们添加了一个名为 usage 的shell函数,用于在调用帮助选项或尝试未知选项时显示消息。

接下来,我们开始处理循环。当位置参数 $1 不为空时,此循环将继续。在循环结束时,我们有一个 shift 命令来推进位置参数,以确保循环最终终止。

在循环中,我们有一个 case 语句,它检查当前的位置参数,看看它是否与任何支持的选项匹配。如果找到支持的参数,则对其进行操作。如果发现未知选项,将显示使用消息,脚本将终止并显示错误。

-f 参数的处理方式很有趣。当检测到时,它会导致额外的移位,将位置参数 $1 前进到提供给 -f 选项的 filename 参数。

getopts 选项

上面的位置参数解析代码是手头任务的一个很好的解决方案,但这不是我们可以采取的唯一方法。有一个名为 getopts 的shell内置程序(不要与同名的外部命令 getopt 混淆)可以为我们做一些工作。每次调用 getopts (通常在循环中),它都会在指定变量中返回当前参数,并递增计数器 OPTIND 以指向下一个位置参数。如果选项需要参数,则参数将在变量 OPTARG 中返回。

getopts 语法如下所示:

getopts optstring var [arg...]

optstring 参数是一个由单字符选项名称组成的字符串。 getopts 不(容易)支持长格式选项。此外,如果选项名称后面有冒号字符,则意味着该选项需要一个参数。

虽然 getopts 只支持单个字符的选项名称,但它确实允许在没有中间连字符的情况下将多个选项串在一起,正如我们在 ls 等许多命令中看到的那样:

如果 optstring 以冒号开头,当存在无效的选项或缺少必需的参数时, getopts 将静音其自己的错误消息。

var 参数是将保存当前选项名称的变量的名称。

通常, getopts 处理位置参数,但它也可能处理 var 后面列出的任何其他参数。

getopts 内置函数返回成功退出状态,直到它没有要处理的参数为止。

是的,这似乎有点复杂,但当我们看到它的实际应用时,它会变得更加清晰。使用 getopts ,我们可以编写这种替代技术的简短演示:

让我们详细看看这段代码。我们从通常的变量定义和用法消息的函数定义开始。接下来,我们通过将 getopts 命令放入 while 循环中来进入有趣的部分。我们的 optstring 以冒号开头,它将抑制错误消息。冒号后面是我们的选项字母。 f 选项需要一个参数(在本例中为文件名),以便选项字母后面跟着冒号。最后,我们指定一个变量(opt)来保存我们的结果。 while 循环会一直持续到 getopts 用完要处理的参数为止。

while 循环中,我们有一个 case 语句来处理 opt 中返回的结果。

除了 case 语句中的最后两个模式外,这一切看起来都与预期的差不多。这个 ? 当检测到无效选项(即不在列表中的字母)时, getopts 会返回。请注意,在 case 语句中,我们必须转义问号,否则shell会将其解释为文件通配符。接下来,当一个选项缺少必需的参数时, getopts 会返回 : 。无论何时发生这两种错误情况中的任何一种, getopts 都会将有问题的选项字母放在 OPTARG 中。

让我们看看这个演示:

那么,我们应该使用哪种技术, while/shift 还是 getopts ?这一切都归结为我们的需求。 while/shift 方法提供了最多的控制(包括容易实现长选项名),而 getopts 需要更少的代码并支持单连字符多选项语法。

交互模式

有了位置参数代码,让我们实现交互模式。

如果 interactive 变量不为空,则会启动一个无休止的(endless)循环,其中包含文件名提示和后续的现有文件处理代码。如果所需的输出文件已存在,系统会提示用户覆盖、选择其他文件名或退出程序。如果用户选择覆盖现有文件,则执行 break 以终止循环。请注意 case 语句如何仅检测用户是选择覆盖还是退出。任何其他选择都会导致循环继续并再次提示用户。

文件输出

为了实现输出文件名功能,我们必须首先将现有的页面编写代码转换为shell函数,原因稍后就会清楚。

处理 -f 选项逻辑的代码出现在前面的列表末尾。在其中,我们测试是否存在文件名,如果找到,则执行测试以查看文件是否确实可写。为此,执行 touch 操作,然后进行测试以确定生成的文件是否是常规文件。这两个测试处理输入无效路径名的情况(touch 将失败),如果文件已经存在,则它是一个常规文件。

正如我们所看到的, write_html_page 函数被调用以执行页面的实际生成。它的输出要么被定向到标准输出(如果变量 filename 为空),要么被重定向到指定的文件。由于HTML代码有两个可能的目标,因此将 write_html_page 例程转换为shell函数以避免冗余代码是有意义的。

总结

通过添加位置参数,我们现在可以编写功能相当强大的脚本。对于简单、重复的任务,位置参数使编写非常有用的shell函数成为可能,这些函数可以放置在用户的 .bashrc 文件中。

我们的 sys_info_page 程序已经变得越来越复杂和复杂。以下是一个完整的列表,突出显示了最近的更改:

我们还没结束呢。我们还可以做更多的事情,我们可以做出改进。