您的位置:首页 > 运维架构 > Shell

使用Bash编写Linux Shell脚本-7.复合命令

2010-09-20 16:23 721 查看

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 472
235

rules 311
797

stencils 846 621

转换为

Report created on Thu
Aug 22 18:27:07 EDT 2002 by kburtch

Sales Report

Product
Country
Foreign
Total

Average

——

——
——
——
——

binders
1024
576
1600
800

pencils
472
235
707
353

rules
311
797
1108
554

stencils
846
621
1467
733

——

——
——
——
——

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 -i

ITEMS=0
# number
of report items

declare -i

LINE_TOTAL=0
# line
totals

declare -i

LINE_AVG=0
# line
average

declare

PRODUCT
#
product name from data file

declare -i

CSALES
#
country sales from data file

declare -i

FSALES

# 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: