迄今为止,我们的程序中还缺少一个特性,就是接收和处理命令行选项和参数的能力。本章中,我们会研究 shell 的这个特性,以允许程序获得命令行的内容。
访问命令行
shell 提供了一组变量,名为位置参数(positional parameters ),它包含了命令行上单个的字词。该变量按 0
到 9
被命名。可以如下演示:
Copy #!/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
变量的值。当执行时没有参数,结果是这样的:
Copy [me@linuxbox ~]$ posit-param
$0 = /home/me/bin/posit-param
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =
即使没有提供参数,$0
也总是包含显示在命令行上的第一个项目,就是被执行的程序路径名。当提供了参数时,看到的是这样的结果:
Copy [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 还提供了一个变量 $#
,包含了命令行上参数的数目:
Copy #!/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
"
结果如下:
Copy [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 - 访问更多的参数
但是,当我们给程序大量的参数时,会发生什么呢?
Copy [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
)。
Copy #!/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
。程序工作中的状态如下:
Copy [me@linuxbox ~]$ posit-param2 a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d
简单的应用
即使不用 shift
,也可以用位置参数写一些有用的应用程序。举个例子,下面是一个简单的文件信息程序:
Copy #!/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 函数。
Copy 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
开始。当用双引号括起来的时候,将每个位置参数扩展为一个个分离的词,如同由双引号引用一般。
下面是个脚本,显示了这些特殊参数的行为:
Copy #!/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"
这个绕来绕去的程序里,我们创建了两个参数:word
和 words with spaces
,并把它们传递给了 pass_params
函数。那个函数,反过来,把它们传递到 print_params
函数,每个函数都使用了特殊参数 $*
和 $@
可用的四种方式。当执行时,脚本显露出了差异。
Copy [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
,使得程序输出一个有用的信息性消息。
下面是实现命令行处理所需的代码:
Copy 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
选项提供的文件名参数中。
接下来我们加一些代码,以实现互动模式。
Copy # 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 函数,这么做的理由,我们马上就会明白。
Copy 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
程序已经变得复杂而完善。下面完整列出一份,并高亮显示最近所做的更改。
Copy #!/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
我们还没有完成。还有更多的事可以做,还可以做很多改进。
扩展阅读