第32章:位置参数
迄今为止,我们的程序中还缺少一个特性,就是接收和处理命令行选项和参数的能力。本章中,我们会研究 shell 的这个特性,以允许程序获得命令行的内容。
访问命令行
shell 提供了一组变量,名为位置参数(positional parameters),它包含了命令行上单个的字词。该变量按 0 到 9 被命名。可以如下演示:
#!/bin/bash
# posit-param: script to view command line parameters
echo "
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"这个简单的脚本展示 $0 到 $9 变量的值。当执行时没有参数,结果是这样的:
即使没有提供参数,$0 也总是包含显示在命令行上的第一个项目,就是被执行的程序路径名。当提供了参数时,看到的是这样的结果:
注意:你可以用参数扩展访问比
9更大的参数位置。要指定一个比9更大的数,将数字包含在花括号中,如${10}、${55}、${210}等等。
确定参数的数目
shell 还提供了一个变量 $#,包含了命令行上参数的数目:
结果如下:
shift - 访问更多的参数
但是,当我们给程序大量的参数时,会发生什么呢?
在这个示例中,通配符 * 扩展成了 82 个参数。如何能处理这么多参数呢?shell 提供了一个方法,尽管处理起来比较笨拙。shift 命令在每次执行时会使得所有的参数「向下移动」。事实上,使用 shift,仅需一个参数就可以(除了永远不变的 $0)。
每次执行 shift,$2 的值就被移动到 $1,$3 的值就被移动到 $2,以此类推。$# 的值也会减去一。
在 posit-param2 程序中,我们创建了一个循环,评估剩下参数的个数,只要还有一个剩下,就继续。我们展示当前参数,每次循环迭代中都会增加变量 count,以提供一个正在运行的参数数目的计数,并在最后执行 shift,用下一个参数装载到 $1。程序工作中的状态如下:
简单的应用
即使不用 shift,也可以用位置参数写一些有用的应用程序。举个例子,下面是一个简单的文件信息程序:
这个程序显示了一个指定文件的类型(由 file 命令判断)和状态(来自 stat 命令)。这个程序中一个有趣的特性是 PROGNAME 变量。它的值来自 basename "$0" 命令的结果。basename 命令移除了路径名中的前置部分,仅保留一个文件的基本的名称。在上面的例子中,basename 移除了包含在 $0 参数中路径名的前置部分,我们示例程序的完整路径名。当构建消息时,这个值很有用处,比如程序末尾处的使用信息。用这种方法编写程序,可以重命名脚本,而消息也会自动调整以包含程序的名称。
用 Shell 函数使用位置参数
位置参数,一如可以用来传递参数给 shell 脚本,它们也可以被用来传递参数给 shell 函数。要演示这个,让我们把 file_info 脚本转换为一个 shell 函数。
现在,如果一个包含了 file_info shell 函数的脚本,用一个文件名参数呼叫了这个函数,这个参数会被传递到函数中。
有了这种能力,就可以写很多有用的 shell 函数,不仅可以用在脚本中,还能用在 .bashrc 文件中。
注意,PROGNAME 变量已经被改变为 FUNCNAME shell 变量了。shell 自动更新此变量,以保持追踪当前被执行的 shell 函数。记住,$0 总是包含了命令行上第一个项目的完整路径名(即程序的名称),且不会包含我们可能期望的 shell 函数的名称。
批量处理位置参数
把所有的位置参数作为一个组来管理,有时比较有用。例如,我们可能想写一个对另一个程序的「包装」。意思是,创建一个脚本或 shell 函数,以简化对另一个程序的调用。这个包装,在这里,提供了一个神秘的命令行选项列表,然后把这一系列的参数传到更低级的程序中。
shell 为这个功能提供了两个特殊参数。两者都会扩展到一个完整的位置参数列表,但是在细节上有些微妙的差异。具体见表 32-1 中所述。
表 32-1:* 和 @ 特殊参数
参数
描述
$*
扩展到位置参数列表,从 1 开始。当用双引号括起来的时候,会扩展为一个由双引号引用的字符串,包含所有的位置参数,由 IFS shell 变量的第一个字符分隔开(默认情况下是个空格字符。)
$@
扩展到位置参数列表,从 1 开始。当用双引号括起来的时候,将每个位置参数扩展为一个个分离的词,如同由双引号引用一般。
下面是个脚本,显示了这些特殊参数的行为:
这个绕来绕去的程序里,我们创建了两个参数:word 和 words with spaces,并把它们传递给了 pass_params 函数。那个函数,反过来,把它们传递到 print_params 函数,每个函数都使用了特殊参数 $* 和 $@ 可用的四种方式。当执行时,脚本显露出了差异。
参数 $* 和 $@ 两者,都制造了一个四个单词的结果。
word words with spaces
"$*" 产生了单个单词:"word words with spaces"
"$@" 产生了两个单词:"word" "words with spaces"
这符合我们的实际意图。从中可以得到的教训是,即使 shell 提供了四种获取位置参数列表的不同方式,"$@" 对于大多数情况仍然是最有用的,因为它保留了每个位置参数的完整性。要保证安全,就应该永远用它,除非有令人信服的不使用它的理由。
一个更完整的应用
在长时间的中断之后,我们准备恢复关于 sys_info_page 程序的工作,上一次还是在第 27 章中。下个改进将增加几个命令选项,如下:
输出文件。我们会增加一个选项以指定包含程序输出的文件名。这将通过
-f file或--file file来指定。互动模式。该选项会提示用户填写输出的文件名,并确定所指定的文件是否已存在。如果是,会在覆盖已存在的文件前提示用户。该选项由
-i或--interactive指定。帮助。可以指定
-h或--help,使得程序输出一个有用的信息性消息。
下面是实现命令行处理所需的代码:
首先,我们加了一个名为 usage 的 shell 函数,当调用了帮助选项或者用户尝试一个未知的选项时,显示一条消息。
随后,我们开始处理循环。这个循环在 $1 非空时会持续。在循环的末尾,有一个 shift 命令推进未知参数,已确保循环最终会结束。
在循环内,有一个 case 语句,检查当前位置参数,以查看其是否匹配任何受支持的选项。如果找到一个受支持的参数,就会起作用。如果遇到一个未知选项,就会显示用法信息,脚本就会终止,抛出错误信息。
处理 -f 参数的方式比较有趣。当其被检测到,会引发一个额外的 shift,它会将位置参数 $1 推进到 -f 选项提供的文件名参数中。
接下来我们加一些代码,以实现互动模式。
如果 interactive 变量非空,就会开始一个无限循环,其中包含文件名提示和后续的现有文件处理代码。如果期望的输出文件已经存在,会提示用户是覆盖还是选择另一个文件名伙食退出程序。如果用户选择覆盖一个现有文件,就会执行一个 break 终止循环。注意 case 语句是如何检测用户选择覆盖还是退出的。任何其它选择都会导致循环继续,并再次提示用户。
要实现输出文件名的功能,必须首先将已存在的页面编写代码转换为一个 shell 函数,这么做的理由,我们马上就会明白。
用于处理 -f 选项的逻辑的代码,出现在上述列表的末尾。其中,我们测试了文件名的存在,如果找到了,就执行测试,看该文件是否确实可写。这一步,会执行一个 touch 命令,接下来是测试结果文件是否是一个常规文件。这两个测试关注的是所输入的是一个非法的路径名(若是,touch 会报错),以及该文件是否已经存在,以及其是否是一个常规文件。
我们可以看到,write_html_page 函数被用来执行实际的页面生成。其输出,或者是直接到标准输出(如果变量 file-name 为空),或者是重定向到指定文件。由于对 HTML 代码,我们有两个可能的目的地,所以将 write_html_page 转换到 shell 函数,有避免冗余代码的意义。
总结
加入了位置参数,我们现在可以编写颇具功能的脚本了。对于简单重复的任务,位置参数使得编写非常有用的 shell 函数成为可能,并可以替换用户的 .bashrc 文件。
我们的 sys_info_page 程序已经变得复杂而完善。下面完整列出一份,并高亮显示最近所做的更改。
我们还没有完成。还有更多的事可以做,还可以做很多改进。
扩展阅读
Bash Hackers Wiki 有一篇关于位置参数的文章:http://wiki.bash-hackers.org/scripting/posparams
Bash Reference Manual 有一篇关于特殊参数的文章,包括
$*和$@:http://www.gnu.org/software/bash/manual/bashref.html#Special-Parameters关于本章所讨论的技术,
bash包含了一个内建命令,叫getopts,还可以用来处理命令行参数。在bash手册页的 SHELL BUILTIN COMMANDS 章节中,Bash Hackers Wiki 的文章在:http://wiki.bash-hackers.org/howto/getopts_tutorial
Last updated
Was this helpful?