到目前为止,我们的程序缺少的一个功能是接受和处理命令行选项和参数的能力。在本章中,我们将研究允许我们的程序访问命令行内容的shell功能。
第三十二章:位置参数访问命令行确定参数数量shift —— 获得许多参数简单应用程序在 Shell函数中使用位置参数处理位置参数 en Masse更完整的应用程序getopts 选项交互模式文件输出总结
shell提供了一组称为位置参数(positional parameters)的变量,其中包含命令行上的各个单词。变量的名称为0到9。它们可以这样演示:
xxxxxxxxxx# 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的值。当在没有命令行参数的情况下执行时,结果如下:
xxxxxxxxxx[me@linuxbox ~]$ posit-param$0 = /home/me/bin/posit-param $1 = $2 = $3 = $4 = $5 = $6 = $7 = $8 = $9 =即使没有提供参数,$0也将始终包含命令行上出现的第一个项,即正在执行的程序的路径名。当提供参数时,我们会看到以下结果:
xxxxxxxxxx[me@linuxbox ~]$ posit-param a b c d $0 = /home/me/bin/posit-param $1 = a $2 = b $3 = c $4 = d $5 = $6 = $7 = $8 = $9 =注意:您实际上可以使用参数扩展访问九个以上的参数。要指定大于9的数字,请将数字括在大括号中,如${10}、${55}、${211}等。
shell还提供了一个变量 $# ,其中包含命令行上的参数数量:
xxxxxxxxxx# posit-param: script to view command line parameters echo " Number of arguments: $# \$0 = $0 \$1 = $1 \$2 = $2 \$3 = $3 \$4 = $4 \$5 = $5 \$6 = $6 \$7 = $7 \$8 = $8 \$9 = $9 "执行结果如下:
xxxxxxxxxx[me@linuxbox ~]$ posit-param a b c d Number of arguments: 4 $0 = /home/me/bin/posit-param $1 = a $2 = b $3 = c $4 = d $5 = $6 = $7 = $8 = $9 =shift —— 获得许多参数但是,当我们给程序提供大量参数时,会发生什么?
xxxxxxxxxx[me@linuxbox ~]$ posit-param *Number of arguments: 82 $0 = /home/me/bin/posit-param $1 = addresses.ldif $2 = bin $3 = bookmarks.html $4 = debian-500-i386-netinst.iso $5 = debian-500-i386-netinst.jigdo $6 = debian-500-i386-netinst.template $7 = debian-cd_info.tar.gz $8 = Desktop $9 = dirlist-bin.txt在这个示例系统中,通配符 * 扩展为82个参数。我们如何处理这么多?shell提供了一种方法,尽管很笨拙。每次执行 shift 命令时,所有参数都会“向下移动一个”。事实上,通过使用 shift ,可以只使用一个参数(除了永不改变的 $0 )。
xxxxxxxxxx# posit-param2: script to display all arguments count=1 while [[ $# -gt 0 ]]; do     echo "Argument $count = $1"     count=$((count + 1))     shiftdone每次执行 shift 时,$2的值都会移动到$1,$3的值会移动到$2,以此类推。 $# 的值也会减少1。
在 posit-param2 程序中,我们创建了一个循环,该循环计算剩余参数的数量,只要至少有一个参数就继续。我们显示当前参数,在循环的每次迭代中递增变量计数,以提供处理的参数数量的运行计数,最后,执行移位操作,用下一个参数加载 $1 。以下是工作中的程序:
xxxxxxxxxx[me@linuxbox ~]$ posit-param2 a b c d Argument 1 = a Argument 2 = b Argument 3 = cArgument 4 = d即使没有 shift ,也可以使用位置参数编写有用的应用程序。举个例子,这是一个简单的文件信息程序:
xxxxxxxxxx# file-info: simple file information program PROGNAME="$(basename "$0")" if [[ -e "$1" ]]; then     echo -e "\nFile Type:"     file "$1"     echo -e "\nFile Status:"     stat "$1" else     echo "$PROGNAME: usage: $PROGNAME file" >&2     exit 1 fi此程序显示指定文件的文件类型(由 file 命令确定)和文件状态(来自 stat 命令)。该程序的一个有趣特性是 PROGNAME 变量。它被赋予由 basename “$0” 命令产生的值。 basename 命令删除路径名的前导部分,只留下文件的基本名称。在我们的示例中, basename 删除了 $0 参数中包含的路径名的前导部分,即我们示例程序的完整路径名。在构建程序末尾的使用消息等消息时,此值非常有用。通过这种方式编码,可以重命名脚本,消息会自动调整以包含程序的名称。
正如位置参数用于向shell脚本传递参数一样,它们也可以用于向shell函数传递参数。为了演示,我们将把 file_info 脚本转换为shell函数。
xxxxxxxxxxfile_info () {     # file_info: function to display file information         if [[ -e "$1" ]]; then         echo -e "\nFile Type:"         file "$1"         echo -e "\nFile Status:"         stat "$1"    else         echo "$FUNCNAME: usage: $FUNCNAME file" >&2         return 1     fi }现在,如果包含 file_info shell函数的脚本使用文件名参数调用该函数,则该参数将传递给该函数。
有了这个功能,我们可以编写许多有用的shell函数,这些函数不仅可以在脚本中使用,还可以在 .bashrc 文件中使用。
请注意, PROGNAME 变量已更改为shell变量 FUNCNAME 。shell会自动更新此变量,以跟踪当前执行的shell函数。请注意, $0 始终包含命令行上第一个项目的完整路径名(即程序的名称),而不包含我们可能期望的shell函数的名称。
有时将所有位置参数作为一个组进行管理是有用的。例如,我们可能想为另一个程序编写一个“包装器”(wrapper)。这意味着我们创建了一个脚本或shell函数,简化了对另一个程序的调用。在这种情况下,包装器提供了一个神秘的命令行选项列表,然后将一个参数列表传递给低级程序。
shell为此提供了两个特殊参数。它们都扩展到完整的位置参数列表中,但在相当微妙的方面有所不同。如下表所示。
| 参数 | 描述 | 
|---|---|
| $* | 展开位置参数列表,从1开始。当被双引号包围时,它会展开为一个包含所有位置参数的双引号字符串,每个参数由IFS shell变量的第一个字符(默认为空格字符)分隔。 | 
| $@ | 展开位置参数列表,从1开始。当被双引号括起来时,它将每个位置参数展开为一个单独的单词,就像它被双引号包围一样。 | 
下面是一个显示这些特殊参数的脚本:
xxxxxxxxxx# posit-params3: script to demonstrate $* and $@ print_params () {     echo "\$1 = $1"     echo "\$2 = $2"     echo "\$3 = $3"     echo "\$4 = $4" } pass_params () {     echo -e "\n" '$* :';    print_params $*     echo -e "\n" '"$*" :';  print_params "$*"     echo -e "\n" '$@ :';    print_params $@     echo -e "\n" '"$@" :';  print_params "$@" } pass_params "word" "words with spaces"在这个相当复杂的程序中,我们创建了两个参数:单词和带空格的单词,并将它们传递给 pass_params 函数。该函数进而使用具有特殊参数 $* 和 $@ 的四种方法中的每一种将它们传递给 print_params 函数。执行时,脚本会显示差异。
xxxxxxxxxx[me@linuxbox ~]$ posit-param3  $* : $1 = word $2 = words $3 = with $4 = spaces  "$*" : $1 = word words with spaces $2 = $3 = $4 =  $@ : $1 = word $2 = words $3 = with $4 = spaces  "$@" : $1 = word $2 = words with spaces $3 = $4 =使用我们的参数, $* 和 $@ 都会产生四个单词的结果。
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 ,使程序输出一条有用的用法消息。
以下是实现命令行处理所需的代码:
xxxxxxxxxxusage () {     echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"     return } # process command line options interactive= filename= while [[ -n "$1" ]]; do     case "$1" in         -f | --file)        shift                             filename="$1"                             ;;         -i | --interactive) interactive=1                             ;;         -h | --help)        usage                             exit                             ;;         *)                  usage >&2                             exit 1                             ;;     esac     shift done首先,我们添加了一个名为 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 等许多命令中看到的那样:
xxxxxxxxxxls -la如果 optstring 以冒号开头,当存在无效的选项或缺少必需的参数时, getopts 将静音其自己的错误消息。
var 参数是将保存当前选项名称的变量的名称。
通常, getopts 处理位置参数,但它也可能处理 var 后面列出的任何其他参数。
getopts 内置函数返回成功退出状态,直到它没有要处理的参数为止。
是的,这似乎有点复杂,但当我们看到它的实际应用时,它会变得更加清晰。使用 getopts ,我们可以编写这种替代技术的简短演示:
xxxxxxxxxx# getopts-test: process command line options using getoptsPROGNAME="$(basename "$0")"interactive=filename=usage () {    echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"    return}while getopts :f:ih opt; do    case "$opt" in        f) filename="$OPTARG" ;;        i) interactive=1 ;;        h) usage ;;        \?) echo "option '$OPTARG' invalid" ;;        :) echo "option '$OPTARG' missing argument";;    esacdoneecho "interactive = '$interactive' filename = '$filename'"让我们详细看看这段代码。我们从通常的变量定义和用法消息的函数定义开始。接下来,我们通过将 getopts 命令放入 while 循环中来进入有趣的部分。我们的 optstring 以冒号开头,它将抑制错误消息。冒号后面是我们的选项字母。 f 选项需要一个参数(在本例中为文件名),以便选项字母后面跟着冒号。最后,我们指定一个变量(opt)来保存我们的结果。 while 循环会一直持续到 getopts 用完要处理的参数为止。
在 while 循环中,我们有一个 case 语句来处理 opt 中返回的结果。
除了 case 语句中的最后两个模式外,这一切看起来都与预期的差不多。这个 ? 当检测到无效选项(即不在列表中的字母)时, getopts 会返回。请注意,在 case 语句中,我们必须转义问号,否则shell会将其解释为文件通配符。接下来,当一个选项缺少必需的参数时, getopts 会返回 : 。无论何时发生这两种错误情况中的任何一种, getopts 都会将有问题的选项字母放在 OPTARG 中。
让我们看看这个演示:
xxxxxxxxxx[me@linuxbox ~]$ getopts-testinteractive = '' filename = ''[me@linuxbox ~]$ getopts-test -iinteractive = '1' filename = ''[me@linuxbox ~]$ getopts-test -f foo.htmlinteractive = '' filename = 'foo.html'[me@linuxbox ~]$ getopts-test -if foo.htmlinteractive = '1' filename = 'foo.html'[me@linuxbox ~]$ getopts-test -i -f foo.htmlinteractive = '1' filename = 'foo.html'[me@linuxbox ~]$ getopts-test -aoption 'a' invalidinteractive = '' filename = ''[me@linuxbox ~]$ getopts-test -foption 'f' missing argumentinteractive = '' filename = ''那么,我们应该使用哪种技术, while/shift 还是 getopts ?这一切都归结为我们的需求。 while/shift 方法提供了最多的控制(包括容易实现长选项名),而 getopts 需要更少的代码并支持单连字符多选项语法。
有了位置参数代码,让我们实现交互模式。
xxxxxxxxxx# interactive mode if [[ -n "$interactive" ]]; then     while true; do         read -r -p "Enter name of output file: " filename        if [[ -e "$filename" ]]; then             read -r -p "'$filename' exists. Overwrite? [y/n/q] > "             case "$REPLY" in                 Y|y)    break                         ;;                 Q|q)    echo "Program terminated."                         exit                         ;;                 *)      continue                         ;;             esac        elif [[ -z "$filename" ]]; then            continue        else            break         fi    done fi如果 interactive 变量不为空,则会启动一个无休止的(endless)循环,其中包含文件名提示和后续的现有文件处理代码。如果所需的输出文件已存在,系统会提示用户覆盖、选择其他文件名或退出程序。如果用户选择覆盖现有文件,则执行 break 以终止循环。请注意 case 语句如何仅检测用户是选择覆盖还是退出。任何其他选择都会导致循环继续并再次提示用户。
为了实现输出文件名功能,我们必须首先将现有的页面编写代码转换为shell函数,原因稍后就会清楚。
xxxxxxxxxxwrite_html_page () {     cat << _EOF_ <html>     <head>         <title>$TITLE</title>     </head>     <body>         <h1>$TITLE</h1>         <p>$TIMESTAMP</p>         $(report_uptime)        $(report_disk_space)         $(report_home_space)     </body> </html> _EOF_     return }# output html page if [[ -n "$filename" ]]; then     if touch "$filename" && [[ -f "$filename" ]]; then         write_html_page > "$filename"     else         echo "$PROGNAME: Cannot write file '$filename'" >&2         exit 1     fi else     write_html_page fi处理 -f 选项逻辑的代码出现在前面的列表末尾。在其中,我们测试是否存在文件名,如果找到,则执行测试以查看文件是否确实可写。为此,执行 touch 操作,然后进行测试以确定生成的文件是否是常规文件。这两个测试处理输入无效路径名的情况(touch 将失败),如果文件已经存在,则它是一个常规文件。
正如我们所看到的, write_html_page 函数被调用以执行页面的实际生成。它的输出要么被定向到标准输出(如果变量 filename 为空),要么被重定向到指定的文件。由于HTML代码有两个可能的目标,因此将 write_html_page 例程转换为shell函数以避免冗余代码是有意义的。
通过添加位置参数,我们现在可以编写功能相当强大的脚本。对于简单、重复的任务,位置参数使编写非常有用的shell函数成为可能,这些函数可以放置在用户的 .bashrc 文件中。
我们的 sys_info_page 程序已经变得越来越复杂和复杂。以下是一个完整的列表,突出显示了最近的更改:
xxxxxxxxxx# sys_info_page: program to output a system information page PROGNAME="$(basename "$0")" TITLE="System Information Report For $HOSTNAME" CURRENT_TIME="$(date +"%x %r %Z")" TIMESTAMP="Generated $CURRENT_TIME, by $USER" report_uptime () {     cat << _EOF_     <h2>System Uptime</h2>     <pre>$(uptime)</pre> _EOF_     return } report_disk_space () {     cat << _EOF_     <h2>Disk Space Utilization</h2>     <pre>$(df -h)</pre> _EOF_     return } report_home_space () {     if [[ "$(id -u)" -eq 0 ]]; then         cat << _EOF_    <h2>Home Space Utilization (All Users)</h2>     <pre>$(du -sh /home/*)</pre> _EOF_     else         cat << _EOF_     <h2>Home Space Utilization ($USER)</h2>     <pre>$(du -sh "$HOME")</pre> _EOF_     fi     return } usage () {     echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"     return }write_html_page () {     cat << _EOF_ <html>     <head>         <title>$TITLE</title>     </head>     <body>         <h1>$TITLE</h1>         <p>$TIMESTAMP</p>         $(report_uptime)         $(report_disk_space)         $(report_home_space)     </body> </html> _EOF_     return } # process command line options interactive= filename= while [[ -n "$1" ]]; do     case "$1" in         -f | --file)        shift                             filename="$1"                             ;;         -i | --interactive) interactive=1                             ;;         -h | --help)        usage                             exit                             ;;         *)                  usage >&2                             exit 1                             ;;     esac     shift done # interactive mode if [[ -n "$interactive" ]]; then    while true; do         read -r -p "Enter name of output file: " filename         if [[ -e "$filename" ]]; then             read -r -p "'$filename' exists. Overwrite? [y/n/q] > "             case "$REPLY" in                 Y|y)    break                         ;;                 Q|q)    echo "Program terminated."                         exit                         ;;                 *)      continue                         ;;             esac        elif [[ -z "$filename" ]]; then            continue        else            break         fi     done fi # output html page if [[ -n "$filename" ]]; then     if touch "$filename" && [[ -f "$filename" ]]; then         write_html_page > "$filename"     else         echo "$PROGNAME: Cannot write file '$filename'" >&2         exit 1     fi else     write_html_page fi我们还没结束呢。我们还可以做更多的事情,我们可以做出改进。