到目前为止,我们的程序缺少的一个功能是接受和处理命令行选项和参数的能力。在本章中,我们将研究允许我们的程序访问命令行内容的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))
shift
done
每次执行 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 = c
Argument 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函数。
xxxxxxxxxx
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
变量已更改为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
,使程序输出一条有用的用法消息。
以下是实现命令行处理所需的代码:
xxxxxxxxxx
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
参数的处理方式很有趣。当检测到时,它会导致额外的移位,将位置参数 $1
前进到提供给 -f
选项的 filename
参数。
getopts
选项上面的位置参数解析代码是手头任务的一个很好的解决方案,但这不是我们可以采取的唯一方法。有一个名为 getopts
的shell内置程序(不要与同名的外部命令 getopt
混淆)可以为我们做一些工作。每次调用 getopts
(通常在循环中),它都会在指定变量中返回当前参数,并递增计数器 OPTIND
以指向下一个位置参数。如果选项需要参数,则参数将在变量 OPTARG
中返回。
getopts
语法如下所示:
getopts optstring var [arg...]
optstring 参数是一个由单字符选项名称组成的字符串。 getopts
不(容易)支持长格式选项。此外,如果选项名称后面有冒号字符,则意味着该选项需要一个参数。
虽然 getopts
只支持单个字符的选项名称,但它确实允许在没有中间连字符的情况下将多个选项串在一起,正如我们在 ls
等许多命令中看到的那样:
xxxxxxxxxx
ls -la
如果 optstring 以冒号开头,当存在无效的选项或缺少必需的参数时, getopts
将静音其自己的错误消息。
var 参数是将保存当前选项名称的变量的名称。
通常, getopts
处理位置参数,但它也可能处理 var 后面列出的任何其他参数。
getopts
内置函数返回成功退出状态,直到它没有要处理的参数为止。
是的,这似乎有点复杂,但当我们看到它的实际应用时,它会变得更加清晰。使用 getopts
,我们可以编写这种替代技术的简短演示:
xxxxxxxxxx
# getopts-test: process command line options using getopts
PROGNAME="$(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";;
esac
done
echo "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-test
interactive = '' filename = ''
[me@linuxbox ~]$ getopts-test -i
interactive = '1' filename = ''
[me@linuxbox ~]$ getopts-test -f foo.html
interactive = '' filename = 'foo.html'
[me@linuxbox ~]$ getopts-test -if foo.html
interactive = '1' filename = 'foo.html'
[me@linuxbox ~]$ getopts-test -i -f foo.html
interactive = '1' filename = 'foo.html'
[me@linuxbox ~]$ getopts-test -a
option 'a' invalid
interactive = '' filename = ''
[me@linuxbox ~]$ getopts-test -f
option 'f' missing argument
interactive = '' 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函数,原因稍后就会清楚。
xxxxxxxxxx
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
函数被调用以执行页面的实际生成。它的输出要么被定向到标准输出(如果变量 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
我们还没结束呢。我们还可以做更多的事情,我们可以做出改进。