第32章:位置参数

迄今为止,我们的程序中还缺少一个特性,就是接收和处理命令行选项和参数的能力。本章中,我们会研究 shell 的这个特性,以允许程序获得命令行的内容。

访问命令行

shell 提供了一组变量,名为位置参数(positional parameters),它包含了命令行上单个的字词。该变量按 09 被命名。可以如下演示:

#!/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 变量的值。当执行时没有参数,结果是这样的:

[me@linuxbox ~]$ posit-param
$0 = /home/me/bin/posit-param
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =

即使没有提供参数,$0 也总是包含显示在命令行上的第一个项目,就是被执行的程序路径名。当提供了参数时,看到的是这样的结果:

[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 更大的参数位置。要指定一个比 9 更大的数,将数字包含在花括号中,如 ${10}${55}${210} 等等。

确定参数的数目

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

#!/bin/bash

# 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
"

结果如下:

[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 - 访问更多的参数

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

[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)。

#!/bin/bash

# posit-param2: script to display all arguments

count=1

while [[ $# -gt 0 ]]; do
    echo "Argument $count = $1"
    count=$((count + 1))
    shift
done

每次执行 shift$2 的值就被移动到 $1$3 的值就被移动到 $2,以此类推。$# 的值也会减去一。

posit-param2 程序中,我们创建了一个循环,评估剩下参数的个数,只要还有一个剩下,就继续。我们展示当前参数,每次循环迭代中都会增加变量 count,以提供一个正在运行的参数数目的计数,并在最后执行 shift,用下一个参数装载到 $1。程序工作中的状态如下:

[me@linuxbox ~]$ posit-param2 a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d

简单的应用

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

#!/bin/bash

# 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 脚本,它们也可以被用来传递参数给 shell 函数。要演示这个,让我们把 file_info 脚本转换为一个 shell 函数。

file_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 变量已经被改变为 FUNCNAME shell 变量了。shell 自动更新此变量,以保持追踪当前被执行的 shell 函数。记住,$0 总是包含了命令行上第一个项目的完整路径名(即程序的名称),且不会包含我们可能期望的 shell 函数的名称。

批量处理位置参数

把所有的位置参数作为一个组来管理,有时比较有用。例如,我们可能想写一个对另一个程序的「包装」。意思是,创建一个脚本或 shell 函数,以简化对另一个程序的调用。这个包装,在这里,提供了一个神秘的命令行选项列表,然后把这一系列的参数传到更低级的程序中。

shell 为这个功能提供了两个特殊参数。两者都会扩展到一个完整的位置参数列表,但是在细节上有些微妙的差异。具体见表 32-1 中所述。

表 32-1:*@ 特殊参数

参数

描述

$*

扩展到位置参数列表,从 1 开始。当用双引号括起来的时候,会扩展为一个由双引号引用的字符串,包含所有的位置参数,由 IFS shell 变量的第一个字符分隔开(默认情况下是个空格字符。)

$@

扩展到位置参数列表,从 1 开始。当用双引号括起来的时候,将每个位置参数扩展为一个个分离的词,如同由双引号引用一般。

下面是个脚本,显示了这些特殊参数的行为:

#!/bin/bash

# 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"

这个绕来绕去的程序里,我们创建了两个参数:wordwords with spaces,并把它们传递给了 pass_params 函数。那个函数,反过来,把它们传递到 print_params 函数,每个函数都使用了特殊参数 $*$@ 可用的四种方式。当执行时,脚本显露出了差异。

[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 ,使得程序输出一个有用的信息性消息。

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

usage () {
    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 参数的方式比较有趣。当其被检测到,会引发一个额外的 shift,它会将位置参数 $1 推进到 -f 选项提供的文件名参数中。

接下来我们加一些代码,以实现互动模式。

# interactive mode

if [[ -n "$interactive" ]]; then
    while true; do
        read -p "Enter name of output file: " filename
        if [[ -e "$filename" ]]; then
            read -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 变量非空,就会开始一个无限循环,其中包含文件名提示和后续的现有文件处理代码。如果期望的输出文件已经存在,会提示用户是覆盖还是选择另一个文件名伙食退出程序。如果用户选择覆盖一个现有文件,就会执行一个 break 终止循环。注意 case 语句是如何检测用户选择覆盖还是退出的。任何其它选择都会导致循环继续,并再次提示用户。

要实现输出文件名的功能,必须首先将已存在的页面编写代码转换为一个 shell 函数,这么做的理由,我们马上就会明白。

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
}

# 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 函数被用来执行实际的页面生成。其输出,或者是直接到标准输出(如果变量 file-name 为空),或者是重定向到指定文件。由于对 HTML 代码,我们有两个可能的目的地,所以将 write_html_page 转换到 shell 函数,有避免冗余代码的意义。

总结

加入了位置参数,我们现在可以编写颇具功能的脚本了。对于简单重复的任务,位置参数使得编写非常有用的 shell 函数成为可能,并可以替换用户的 .bashrc 文件。

我们的 sys_info_page 程序已经变得复杂而完善。下面完整列出一份,并高亮显示最近所做的更改。

#!/bin/bash

# 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 -p "Enter name of output file: " filename
        if [[ -e "$filename" ]]; then
            read -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

我们还没有完成。还有更多的事可以做,还可以做很多改进。

扩展阅读

Last updated

Was this helpful?