转载自:/fox_lht/archive//09/20/5897336.aspx
7.复合命令
除了最简单的脚本,你很少想要执行每一个命令。执行一组命令或者重复执行一组命令若干次比执行单个命令更加有助。复合命令是将命令封装在一组其他命令中。
从可读性来说,封装后的命令使用缩进格式将会使复合命令的代码清晰并便于阅读。管理员曾经抱怨过我的缩进比标准的缩进少了一个空格(我必须使用尺子在屏幕上测量才能确定此事),我认为这不是什么问题,但是他说,当输入0时,它的程序会崩溃。
复合命令总是有两个命令组成。命令的结束符是该命令相反拼写顺序,就像使用括号将命令括住了。例如:神秘莫测的命令esac实际上是复合命令case的结束符。
命令状态码
每一个Linux命令都返回一个状态码(退出状态),他是一个0~255之间的数字,用来表示该命令遇到的问题。如果状态码返回的是0,则表示该命令运行成功,其他的状态码表示某种错误。
状态码包含在变量“$?”中。
$ unzip no_file.zip
unzip:cannot find no_file.zip, no_file.zip.zip or no_file.zip.ZIP.
$ printf “%d\n” “$?”
9
unzip命令找不到要解压的文件,返回的状态码是9。
非官方的Linux惯例使用状态码127并且比标准的错误代码要小。例如:ls返回了状态码9,它表示“bad file number”。完整的错误代码列在附录D:“错误代码”中。
如果命令被信号中断,Bash返回状态码128,加上信号码。最终,用户的错误码应该大于191,Bash返回的错误码为63。信号码列在附录E:信号。
if test ! -x “$who” ; then
printf “$SCRIPT:$LINENO: the command $who is not available – “\
“aborting\n “ >&2
exit 192
fi
一般,大部分Linux命令只是简单的返回1或0,表示失败还是成功。这也许就是你的脚本所需要的所有信息。特殊的错误信息任然显示在标准输出上。
$ ls po_1473.txt
po_1473.txt
$ printf “%d\n” $?
0
$ ls no_file
no_file not found
$ printf “%d\n” $?
1
状态码不同于let命令返回的真值(第六章讨论过),本节称之为逻辑表达式。在let命令中,false的值是0,这符合计算机语言的习惯,但是状态码是0表示成功而不是失败。
$ let “RESULT=1>0”
$ printf “%d %d\n” “$RESULT” $?
1 0
$ test 1 -gt 0
$ printf “%d\n” $?
0
let命令分配1给RESULT,表明1大于0。test命令返回状态码0表明命令运行成功。let命令返回状态码0,表明let命令成功进行比较。
这些相反的码和习惯可能会导致错误,这些错误很难调试出来。Bash有两个内置命令true和false。这些是返回的状态码,而不是let命令的真值。
$ true
$ printf “%d\n” “$?”
0
$ false
$ printf “%d\n” “$?”
1
true命令分配一个成功的状态码(0)。fasle分配一个错误的状态码(1)。
有点混乱吧?
如果你需要保存逻辑比较的成功状态最好还是使用test命令。大部分外壳使用状态码而不是真值。
在管道中,一次运行几个命令。从管道返回的状态码是最后一个命令的状态码。下面的示例中,显示的是wc命令而不是ls命令的状态码。
$ ls badfile.txt | wc -l
ls: badfile.txt: No such file or directory
0
$ printf “%d\n” “$?”
0
虽然ls报告了一个错误,管道返回的还是成功的状态码,因为wc命令是运行成功的。
Bash也定义了一个数组称之为PIPESTATUS,它包含了上此运行管道中每一个命令的单独状态。
$ ls badfile.txt | wc -l
ls: badfile.txt: No such file or directory
0
$ printf “%d %d\n” “${PIPESTATUS[0]}” “${PIPESTATUS[1]}”
1 0
$?是PIPESTATUS数组的最后一个值的别名。
一个命令或管道可以被“!”进行对状态进行取反操作,如果状态时0取反则为1,如果大于0,取反则为0。
if命令
if命令执行二选一或多选一的操作。
通常if命令和test命令一起使用。
NUM_ORDERS=`ls -1 | wc -l`
if [ “$NUM_ORDERS” -lt “$CUTOFF” ] ; then
printf “%s\n” “Too few orders...try running again later”
exit 192
fi
这个例子是对当前目录中的文件进行统计,如果没有足够的文件数,则显示一则消息,否则就到fi命令结束。
then命令前分号是必须要有的,虽然它是和if一起工作的,但是它仍然是一个单独的命令,所以需要分号进行分割。
if命令亦可以有一个else命令的分支,它可以在条件失败的时候运行。
NUM_ORDERS=`ls -1 | wc -l`
if [ “$NUM_ORDERS” -lt “$CUTOFF” ] ; then
printf “%s\n” “Too few orders...but will process them anyway”
else
printf “%s\n” “Starting to process the orders”
fi
if命令内部可以嵌套if命令。
NUM_ORDERS=`ls -1 | wc -l`
if [[ $NUM_ORDERS -lt $TOOFEW ]] ; then
printf “%s\n” “Too few orders...but will process them anyway”
else
if [[ $NUM_ORDERS -gt $TOOMANY ]] ; then
printf “%s\n” “There are many orders.Processing may take a long time”
else
printf “%s\n” “Starting to process the orders”
fi
fi
if不可以交叉嵌套,即:里面的if必须完全在外部if命令内。
为了实现多分支,if命令可以有elif分支,elif命令是else if的简写,它可以减少不必要的嵌套。elif命令的最后可以在最后加一个else命令,他在所有条件都没有中的时候执行。有了这些知识,你可以重写上面的示例:
NUM_ORDERS=`ls -1 | wc -l`
if [ “$NUM_ORDERS” -lt “$TOOFEW” ] ; then
printf “%s\n” “Too few orders...but will process them anyway”
elif [ “$NUM_ORDERS” -gt “$TOOMANY” ] ; then
printf “%s\n” “There are many orders.Processing may take a long time”
else
printf “%s\n” “Starting to process the orders”
fi
if命令也可以不和test命令一起使用,它可以根据命令返回的状态码进行执行相关的任务。
if rm “$TEMPFILE” ; then
printf “%s\n” “$SCRIPT:temp file deleted”
else
printf “%s - status code %d\n” \
“$SCRIPT:$LINENO: unable to delete temp file” $? 2>&
fi
在if命令中嵌入复杂的命令会使脚本语言难读且难以调试。你应该避免这样做。在这个例子中,如果rm命令运行失败,则它先显示自己的提示信息,接着显示脚本中的信息。尽管在if命令内部也可以声明变量,但是它很难确定那个变量存在,那个不存在。
case命令
case命令进行模板匹配测试,如果值和某个模板匹配,则执行相应的命令。变量逐个进行测试。
和elif命令不同,测试的状态码来自同一个命令,case测试变量的值。如果测试字符串的值,case命令比elif命令更好。
每一个case分支都必须用一对分号(;;)进行分割。如果没有分号,Bash会执行下一个分支并报错。
printf “%s -> “ “1 = delete, 2 = archive.Please choose one”
read REPLY
case “$REPLY” in
1) rm “$TEMPFILE” ;;
2) mv “$TEMPFILE” “$TEMPFILE.old” ;;
*) printf “%s\n” “$REPLY was not one of the choices” ;;
esac
星号表示所有没有匹配模板的条件所执行的任务。虽然这是可选的,但是好的设计应该有一个这样的写法,即使里面是一个空语句(:)也是好的。
模板匹配规则遵循globbing规则,参考前一张杰的内容。例如:竖条可以分开多个模板。
case同其他计算机语言不一样,不会跟着执行。当一个选择了一个条件,则其他case不会执行。
while循环
有几个命令都可以实现重复执行一组命令。
while命令根据测试条件执行封闭在while命令中命令组。如果命令失败,则在while命令中的命令组不执行。
printf “%s\n” “Enter the names of companies or type control-d”
while read -p “Company ?” COMPANY; do
if test -f “orders_$COMPANY.txt” ; then
printf “%s\n” “There is an order file from this company”
else
printf “%s\n” “There are no order files from this company”
fi
done
while命令使用done命令结束。不是你也许认为的elihw这样的命令。
使用true命令作为测试条件,while命令会无限循环下去,因为true总是返回成功,循环无疑会一直下去。
printf “%s\n” “Enter the names of companies or type quit”
while true ; do
read -p “Company ?” COMPANY
if [ “$COMPANY” = “quit” ] ; then
break
elif test -f “orders_$COMPANY.txt” ; then
printf “%s\n” “There is an order file from this company”
else
printf “%s\n” “There are no order files from this company”
fi
done
一个while循环可以使用break命令提前停止。在到达break命令后,Bash会跳出循环并执行循环外的第一条命令。
break后面可以跟着一个数字,表示跳出几层循环。例如:
break 2
跳出2层循环。
和break对应的是continue命令,它会对后面的命令忽略,从头开始从新循环。continue命令后面也可以跟一个数字表示跳到哪一层的循环。
until循环
和while循环对应的是until循环命令,until循环是直到测试条件成功才停止执行封闭在until语句中命令组,其他基本上和until命令相同。它相当于while!。
until test -f “$INVOICE_FILE” ; do
printf “%s\n” “Waiting for the invoice file to arrive...”
sleep 30
done
将false和until一起使用可以建立无限循环,break和continue命令同样也可以用于until循环命令。
for循环命令
标准的伯恩for in loop是变量在这儿文件。for命令将一系列值分别放入变量中然后执行包含的命令。
for FILE_PREFIX in order invoice purchase_order; do
if test -f “$FILE_PREFIX””_vendor1.txt” ; then
printf “%s\n” “There is a $FILE_PREFIX file from vendor 1...”
fi
done
如果in后面的参数没有,则for在外壳脚本中参数中进行循环。
break和continue命令可以用于for循环。
因为其他外壳的特性,for循环不是通用的。
嵌入let命令(((..)))
let命令判断如果表达式是0则返回状态码1,如果表达式不为0,则返回0。和test命令可以使用一对方括号来表示更容易阅读一样,let命令也有更容易阅读的表示,使用双括号。
下面的列表7.1示例使用了for循环嵌入let命令的表达方式:
列表7.1
#!/bin/bash
# forloop.sh: Count from 1 to 9
for (( COUNTER=1; COUNTER<10; COUNTER++ )) ; do
printf “The counter is now %d\n” “$COUNTER”
done
exit 0
当循环开始时,执行双括号中的第一个表达式,每次循环开始执行第三个表达式,并检查第二个表达式,当第二个表达式返回false,循环结束。
$ bash forloop.sh
The counter is now 1
The counter is now 2
The counter is now 3
The counter is now 4
The counter is now 5
The counter is now 6
The counter is now 7
The counter is now 8
The counter is now 9
命令组({..})
命令可以使用大括号组合到一个组内。
ls -1 | {
while read FILE ; do
echo “$FILE”
done
}
在本实例中,ls命令的结果成为组命令的输入。
$ test -f orders.txt && { ls -l orders.txt ; rm orders.txt; } \
|| printf “no such file”
如果文件orders.txt存在,文件显示出来,接着被删除。否则显示“no such file”。在大括号中的命令需要分号进行分割。
命令也可以使用子外壳进行分组,子外壳将在第九章进行讨论。
report.bash:报表格式化
report.bash是一个用来给销售数字建立报表的脚本程序。销售数字文件有产品名称、本国销售数、外国销售数来组成。例如:report.bash把下面的报表
binders 1024 576
pencils 472235
rules 311797
stencils 846 621
转换为
Report created on Thu Aug 22 18:27:07 EDT 2002 by kburtch
Sales Report
ProductCountryForeignTotalAverage
——————————
binders10245761600800
pencils472235707353
rules3117971108554
stencils8466211467733
——————————
Total number of products: 4
End of report
列表7.2 report.bash
!/bin/bash
#
# report.bash: simple report formatter
#
# Ken O. Burtch
# CVS: $Header$
# The report is read from DATA_FILE.It should contain
# the following columns:
#
#Column 1: PRODUCT = Product name
#Column 2: CSALES= Country Sales
#Column 3: FSALES= Foreign Sales
#
# The script will format the data into columns, adding total and
# average sales per item as well as a item count at the end of the
# report.
# Some Linux systems use USER instead of LOGNAME
if [ -z “$LOGNAME” ] ; then# No login name?
declare –rx LOGNAME=”$USER”# probably in USER
fi
shopt -s -o nounset
# Global Declarations
declare -rx SCRIPT=${0##*/}# SCRIPT is the name of this script
declare -rx DATA_FILE=”report.txt”# this is raw data for the report
declare -iITEMS=0# number of report items
declare -iLINE_TOTAL=0# line totals
declare -iLINE_AVG=0# line average
declarePRODUCT# product name from data file
declare -iCSALES# country sales from data file
declare -iFSALES# foreign sales from data file
declare -rx REPORT_NAME=”Sales Report” # report title
# Sanity Checks
if test ! -r “$DATA_FILE” ; then
printf “$SCRIPT: the report file is missing—aborting\n” >&2
exit 192
fi
# Generate the report
printf “Report created on %s by %s\n” “`date`” “$LOGNAME”
printf “\n”
printf “%s\n” “$REPORT_NAME”
printf “\n”
printf “%-12s%12s%12s%12s%12s\n” “Product” “Country” “Foreign” “Total” “Average”
printf “%-12s%12s%12s%12s%12s\n” “——” “——” “——” “——” “——”
{ while read PRODUCT CSALES FSALES ; do
let “ITEMS+=1”
LINE_TOTAL=”CSALES+FSALES”
LINE_AVG=”(CSALES+FSALES)/2”
printf “%-12s%12d%12d%12d%12d\n” “$PRODUCT” “$CSALES” “$FSALES” \
“$LINE_TOTAL” “$LINE_AVG”
done } < $DATA_FILE
# Print report trailer
printf “%-12s%12s%12s%12s%12s\n” “——” “——” “——” “——” “——”
printf “Total number of products: %d\n” “$ITEMS”
printf “\n”
printf “End of report\n”
exit 0