使用bash编写Linux shell脚本--复合命令

时间:2022-04-22
本文章向大家介绍使用bash编写Linux shell脚本--复合命令,主要内容包括命令状态码、if 命令、case 命令、while 循环、until 循环、for 循环命令、嵌入 let 命令((( .. )))、命令组( {..} )、report.bash :报表格式化、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

除了最简单的脚本,你很少想要执行每一个命令。执行一组命令或者重复执行一组命令若干次比执行单个命令更加有助。复合命令是将命令封装在一组其他命令中。

从可读性来说,封装后的命令使用缩进格式将会使复合命令的代码清晰并便于阅读。管理员曾经抱怨过我的缩进比标准的缩进少了一个空格(我必须使用尺子在屏幕上测量才能确定此事),我认为这不是什么问题,但是他说,当输入 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