使用Bash编写Linux Shell脚本-6.表达式
转载自:http://blog.csdn.net/fox_lht/archive/2010/09/19/5894940.aspx
6. 表达式
表达式是一个计算公式,通过它可以得出一个值。 Bash 有几个内置的命令和函数是计算表达式,它们不是所有的都有相同的语法或特性。有时相同的表达式有不止一种的计算方法。还有一些指定的特性用于罕见的情形下。很少有编程人员记住所有的这些细小的差别。
在一次我和教授兼作家 E Ray Skilton 先生的对话中,我们讨论了下拉菜单是用于对命令做出选择而不是对记忆的辅助。他从他的 Atari 计算面前转过头来问我:“你是否因为学习了太多的计算机语言而开始忘记一些命令的语法了?”。我说:“从来没有”。他笑道:“你一定会的”。
扩展
在 Bash 中的表达式不仅是算术运算。因为变量是字符串,有些表达式使用简写符号来替换长字符串表示的值。这种替换处理称之为扩展 ,因为执行替换操作后,字符串被扩展为长字符串。
例如:路径名的模板匹配就是一个字符串扩展。星号、问号和其他的某些字符由它们所表示的文件名所替换,结果就展示了一个更长的、完整的字符串的值。
Bash 将扩展分为六类。外壳将它们按下面的顺序进行排列。
n 文件名使用大括号的扩展
n 路径名使用波浪号的扩展
n 美元符号、变量和算术的扩展
n 命令扩展(从左到右进行扩展)
n 单词分隔(根据空格或 IFS 变量的内容进行参数的分割)
n 路径名扩展(即:路径名模板匹配)
所有的这些扩展会在下面进行详细的讨论。
在脚本中如果没有注意命令执行的顺序会引起一些问题。假如你将一个波浪路径扩展分配给了一个变量:
$ ls -d ~/tmp
/home/kburtch/tmp
$ TEMP=”~/tmp”
$ ls $TEMP
~/tmp not found
尽管存在 ~/tmp 目录但是 ls 命令找不到这样的目录,但是 ls 命令可以在直接使用在命令行的波浪号扩展找到该目录。
这个问题是因为扩展的顺序引起的。由于变量扩展在波浪号扩展之后发生, Bash 首先尝试替换所有的波浪号,发现没有波浪号。然后进行变量替换,找到只有一个变量,它替换为 TEMP 的值。因为波浪号扩展已经结束了,波浪号被留在了命令里, ls 查找目录 ~ 中 tmp 目录。当然没有这个目录了。如果执行顺序可以反过来就不会有问题了。
这告诉我们不要再变量中使用波浪号扩展,直接使用 HOME 变量就可以了。
还有一些由命令处理的其他类型的扩展没有被例举出来,因为它们只有在 Bash 完成了它的六类扩展之后才由命令来执行。有两个通用的内置命令可以解释表达式。
test 命令可以大范围的进行条件测试并且表示条件为真或为假。 test 可以比较文件、字符串或数字。不要和 Linux 命令的 test 混淆了。
let 命令计算一个表达式并将结果分配给某个变量。
为了测试结果,你需要一个命令检查 test 结果并执行相应的动作。 if 命令是一个不错的选择。
基本的 if 命令
内置 if 命令执行一个命令并检查其结果,如果成功,执行另一组命令。本节只讨论 if 用于 test 的场景,其他用法在第 7 章中详细讨论。
基本 if 命令的语法如下所示:
if test arguments ; then
statements to run
fi
关键字 then 被认为是一个分隔命令,需要一个分号将其和 if 命令分隔开,关键字 fi 表示 if 命令的结束。
例如: test –f 命令检查某个文件是否存在:
if test -f ./report.out ; then
printf “The report file ./report.out exists!\n”
fi
如果文件存在将显示“ The report file ./report.out exists! ”消息。如果文件不存在, printf 命令被跳过,执行过程跳到 fi 之后。
File 表达式
内置命令 test 包含了各种文件测试,可以测试文件类型,文件的可访问性,比较文件的大小或文件的属性。下面是 Bash 文件测试的完整列表:
n -b file— 如果文件是一个块设备文件返回真。
n -c file— 如果文件是一个字符设备文件返回真。
n -d file— 如果文件是一个目录返回真。
n -e file— 如果文件存在返回真。
n -f file— 如果文件存在并是一个正规的文件返回真。
n -g file— 如果文件有 set-group-id 权限返回真。
n -h file (or -L file)— 如果文件是一个字符链接返回真。
n -k file— 如果文件有 sticky 权限返回真。
n -p file— 如果文件是一个管道返回真。
n -r file— 如果文件可读返回真。
n -s file— 如果文件存在并不空返回真。
n -S file— 如果文件是一个 socket 返回真
n -t fd— 如果文件描述符在终端上打开返回真。
n -u file— 如果文件设置了 set-user-id 权限返回真。
n -w file— 如果文件可被你的脚本写返回真。
n -x file— 如果文件可以被你的脚本执行返回真。
n -O file— 如果文件被你拥有返回真。
n -G file— 如果文件和你在同一个组返回真。
n -N file— 如果文件被更新了返回真。
n f1 -nt f2— 如果文件 f1 比文件 f2 新返回真。
n f1 -ot f2— 如果文件 f1 比文件 f2 旧返回真。
n f1 -ef f2— 如果文件是 f1 是文件 f2 的硬链接返回真。
文件测试通常用于脚本开始的完整性测试。它们可以用来检查所有的文件是否存在以及是否可读(必要时检查是否可写)。所有的命令必须是可执行的(如列表 6.1 所示)。
列表 6.1 archive.bash
#!/bin/bash
#
# archive.bash - Archive old order files
#
# Ken O. Burtch
# CVS: $Header$
shopt -s -o nounset
# Global Declarations
declare -rx SCRIPT=${0##*/} # SCRIPT is the name of this script
declare -rx who=”/usr/bin/who” # the who command - man 1 who
declare -rx TMP=”/tmp/temp.$$” # TMP is a temporary file
# Sanity Checks
if test ! -x “$who” ; then
printf “$SCRIPT:$LINENO: the command $who is not available — aborting” >&2
exit 192
fi
if test -f “$TMP” ; then
if test ! -w “$TMP” ; then
printf “$SCRIPT:$LINENO: the temp file $TMP exists and cannot “\
“ be overwritten — aborting” >&2
exit 192
fi
fi
这个脚本确保了 who 命令的可执行性并且 temp 文件名为 TMP ,并判断是否存在,如果存在就覆盖它。
多项测试
单个测试可以使用 and ( -a )和 or ( -o )组合到一起。前面临时文件的测试可以重新象下面这样写:
if test -f “$TMP” -a ! -w “$TMP” ; then
printf “$SCRIPT:$LINENO: the temp file $TMP exists and cannot”\
be overwritten — aborting” >&2
exit 192
fi
下面的写法也可以但是不完全相同:
if test -f “$TMP” && test ! -w “$TMP” ; then
printf “$SCRIPT:$LINENO: the temp file $TMP exists and cannot”\
“ be overwritten — aborting” >&2
exit 192
fi
使用 && 进行连接的测试比使用 -a 开关要慢,因此在 if 表达式中尽量使用 -a 开关。
也可以使用圆括号,但是它们必须使用反斜杠进行引导。圆括号在外壳中有特殊的含义。在某些情况下,它们是必须的。
有时 -a 或 -o 开关和非操作一起使用时可能会使程序员有点弄不明白,在大部分计算机语言中,非操作符作为一元操作符它的优先级很高,经常首先执行,但是在 Bash 中 -a 和 -o 的优先级比非操作高,看下面的操作将总会返回 false 。
if test ! -f “$TMP -o -f “$TMP” ; then
Bash 将这条命令解释为如果此文件既不存在也不存在。这是根据 POSIX 标准制定的怪异操作。为了得到想要的结果,你必须使用圆括号。
if test \( ! -f “$TMP” \) -o -f “$TMP” ; then
方括号也可以用于 test 命令,使用方括号可以是你的程序更加容易的阅读。
if [ -f “$TMP” -a ! -w “$TMP” ] ; then
printf “$SCRIPT:$LINENO: the temp file $TMP exists and cannot”\
“ be overwritten — aborting” >&2
exit 192
fi
方括号在所有的情况下都可以正确识别 test 命令。
Korn 外壳使用双方括号作为 test 命令的变更。 Bash 为了和 Korn 兼容也支持这种变更。 Korn 外壳测试在括号之间不执行单词分割或路径名扩展,因此对变量可以不用双引号进行环绕了。
if [[ -f $FILE ]] ; then
printf “%s\n” “The file exists”
fi
虽然它也有增强的模板匹配特性,但是和 Bash 的 test 相比,它还缺少其他的一些特性,除非你要讲 Korn 外壳脚本移植到 Bash ,你最好使用不要使用 Korn 外壳的 test 命令。
字符串
test 命令也可以进行字符串之间的比较:
n -z s—(zero length) 字符串是空的返回真。
n -n s (or just s)— 字符串不空返回真
n s1 = s2—s1 等于 s2 返回真。
n s1 != s2—s1 不等于 s2 返回真。
n s1 < s2—s1 小于 s2 返回真。
n s1 > s2—s1 大于 s2 返回真。
DAY=`date ‘+%a’`
if [ “$DAY” = “Mon” ] ; then
printf “The weekend is over...get to work!\n”
fi
-z 和 -n 开关时 =”” 和 !=”” 的简写。注意没有大于或等于、小于或等于的操作符。你必须使用 -a 开关组合两个操作来模拟这些操作。
如果使用其他的计算机语言,记住引号在 test 命令不是用于字符串描述而是处理特殊字符的。
因为小于和大于符号已经被 Bash 用于重定向命令,为了使 Bash 正确识别大于和小于号,在他们前面必须使用反斜杠进行引导。
COMPANY=”Athabasca”
if [ “$COMPANY” \< “M” ] ; then
printf “The company name begins with a letter less than M\n”
fi
通常用于字符串比较是外壳 flag 变量的测试。 flag 是用于表示一个特殊条件是否为真。它们可以提供一个方法来记住先前的测试。
任何对值都可以用于表示标志的条件,例如 TRUE 和 false 、或者 yes 和 no 。可是,当使用了没有想到的字符串时例如: no ,这也会产生一些模糊的条件。习惯上,变量可以包含任何字符串,条件都被认为是真。
WEEKEND=
DAY=`date ‘+%a’`
if [ “$DAY” = “Sat” -o “$DAY” = “Sun” ] ; then
WEEKEND=1
fi
为了得出是否为周末,使用 -n 和 -z 开关。
if [ -n “$WEEKEND” ] ; then
printf “%s” “The weekend is here”
else
printf “%s” “It isn’t the weekend”
fi
算术表达式
内置命令 let 执行算术运算。 let 命令希望要包含一个变量字符串,一个等号和一个计算表达式,结果被保存在变量中。
$ let “SUM=5+5”
$ printf “%d” “$SUM”
10
你不需要使用 $ 符号来扩展变量名, let 命令知道变量在等号的右边,它的值等于左边表达式的计算结果。
$ let “SUM=SUM+5”
$ printf “%d” “$SUM”
15
$ let “SUM=$SUM+5”
$ printf “%d” “$SUM”
20
在 let 命令中美元符号是一个可选项,但是在其他命令中并不是这样。
如果变量使用 -i 开关被声明为整型。 let 命令也是可选的。
$ SUM=SUM+5
$ printf “%d\n” $SUM
25
如果 SUM 是一个字符串变量,它会被分配字符串“ SUM+5 ”:
$ unset SUM
$ declare SUM=0
$ SUM=SUM+5
$ printf “%s\n” “$SUM”
SUM+5
任何特殊的字符出现在 let 命令表达式中必须使用引号,防止 Bash 扩展他们。
let 命令提供了四个基本算术运算,加减乘除,只有整数才被允许使用。
$ let “RESULT=5 - 2”
$ printf “5 minus 2 is %d\n” “$RESULT”
5 minus 2 is 3
$ let “RESULT=5 * 2”
$ printf “5 times 2 is %d\n” “$RESULT”
5 times 2 is 10
$ let “RESULT=5 / 2”
$ printf “5 divided by 2 is %d\n” “$RESULT”
5 divided by 2 is 2
$ let “RESULT=5 % 2”
$ printf “remainder of 5 divided by 2 is %d\n” “$RESULT”
remainder of 5 divided by 2 is 1
下面是一些算术操作符,它们按照优先顺序排列,许多和 C 语言相同:
- -,+— 一元的加和减。
- !,~— 逻辑非。
- *,/,%— 乘除和求余。
- +,-— 加和减。
- <<,>>— 位左移和位右移。
- <=,>=,<,>— 比较
- ==,!=— 等于和不等于
- &— 位与
- ^— 位异或
- |— 位或
- &&— 逻辑与
- ||— 逻辑或
- expr ? expr :— 条件表达式
- =,*=,/=,%=— 赋值
- +=,-=,<<=,>>=,&=,^=,|=— 自引用操作。
例如:求接近 10 的整数:
$ declare -i COST=5234
$ COST=\(COST+5\)/10*10
$ printf “%d\n” $COST
5230
括号必须使用反斜杠引导,防止外壳将它们认为是子外壳的引用。
let 命令也可以处理八进制和十六进制。
$ declare -i OCTAL=0
$ let “OCTAL=0775”
$ printf “%i\n” “$OCTAL”
509
操作在下一节进行描述。
逻辑表达式
在 let 命令中, true 表示为 1 , false 表示为 0 。不是 1 或 0 的值被认为是 true ,但是逻辑操作符本身只返回 1 或 0 。
注意逻辑真(一个大于 0 的值)和命令的成功(状态码是 0 )是不同的。从这个方面来说, test 和 let 是相反的。
为了在提示符下使用逻辑非,你必须关闭外壳历史选项否则 Bash 将!解释为历史查找请求。
$ let “RESULT=!0”
$ printf “logical negation of 0 is %d\n” “$RESULT”
logical negation of 0 is 1
$ let “RESULT=!1”
$ printf “logical negation of 1 is %d\n” “$RESULT”
logical negation of 1 is 0
$ let “RESULT=1 && 0”
$ printf “logical and of 1 with 0 is %d\n” “$RESULT”
logical and of 1 with 0 is 0
$ let “RESULT=1 || 0”
$ printf “logical or of 1 with 0 is %d\n” “$RESULT”
logical or of 1 with 0 is 1
没有逻辑异或操作符。
关系操作
和字符串比较不同, let 提供数字的所有比较方式。一般结果都是有限的,因为大部分比较都是 if 命令中 test 命令进行比较。而在逻辑表达式中,失败则返回 0 ;
$ let “RESULT=1 > 0”
$ printf “1 greater than 0 is %d\n” “$RESULT”
1 greater than 0 is 1
$ let “RESULT=1 >= 0”
$ printf “1 greater than or equal to 0 is %d\n” “$RESULT”
1 greater than or equal to 0 is 1
$ let “RESULT=1 < 0”
$ printf “1 less than 0 is %d\n” “$RESULT”
1 less than 0 is 0
$ let “RESULT=1 <= 0”
$ printf “1 less than or equal to 0 is %d\n” “$RESULT”
1 less than or equal to 0 is 0
$ let “RESULT=1 == 0”
$ printf “1 equal to 0 is %d\n” “$RESULT”
1 equal to 0 is 0
$ let “RESULT=1 != 0”
$ printf “1 equal to 0 is %d\n” “$RESULT”
1 equal to 0 is 1
位操作
还有一组位操作,如下所示:
$ let “RESULT=~5”
$ printf “bitwise negation of 5 is %d\n” “$RESULT”
bitwise negation of 5 is -6
$ let “RESULT=5 >> 2”
$ printf “5 left-shifted by 2 is %d\n” “$RESULT”
5 left-shifted by 2 is 1
$ let “RESULT=5 << 2”
$ printf “5 right-shifted by 2 is %d\n” “$RESULT”
5 right-shifted by 2 is 20
$ let “RESULT=5 & 3”
$ printf “bitwise and of 5 with 3 is %d\n” “$RESULT”
bitwise and of 5 with 3 is 1
$ let “RESULT=5 | 3”
$ printf “bitwise or of 5 with 3 is %d\n” “$RESULT”
bitwise or of 5 with 3 is 7
$ let “RESULT=5 ^ 3”
$ printf “bitwise exclusive-or of 5 with 3 is %d\n” “$RESULT”
bitwise exclusive-or of 5 with 3 is 6
自引用操作
自引用操作是变量本身参与运算的一种简写方式。例如: RESULT+=5 是 RESULT=RESULT+5 的简写。
$ let “RESULT=5”
$ let “RESULT+=5”
$ printf “The result is %d” “$RESULT”
The result is 10
还有一些自引用操作例如乘( *= )、除( /= )、求余( %/ )、减( -= )、右移( <<= )、左移( >>=)、位与( &= )、位异或( ^= )、位或( |= )。
但是有些自引用是不可能使用简写方式实现的。例如: RESULT=RESULT-10 可以表示为 RESULT-=10 ,但是 RESULT=10-RESULT 就不可以使用简写方式。
加一操作符( ++ )可以使变量加 1 ,减一操作符( -- )可以使变量减一。
$ let “CNT=0”
$ let “CNT++”
$ printf “%d\n” “$CNT”
1
在加一和减一不是单独用于表达式中时,操作符的位置是有区别的,如果在操作数之前,则加一计算在表达式计算之前,如果在操作数之后,则加一计算在表达式之后。
$ let “CNT=5”
$ let “PRODUCT=0”
$ let “PRODUCT=++CNT*5”
$ printf “%d\n” “$PRODUCT”
30
$ let “CNT=5”
$ let “PRODUCT=CNT++*5”
$ printf “%d\n” “$PRODUCT”
25
$ printf “%d\n” “$CNT”
6
通常,要避免这样使用表达式,因为可能会使你的脚本难于维护,如果加一或减一操作在单独一行就可以很容易理解了。
如果不使用 let 变量,自引用操作符不能用于整数变量(现在 linux 好像可以,我测试了一下没有报错)。
$ COST+=5
bash: COST+=5: command not found
let 命令的其他特性
圆括号可以直接使用
$ let “RESULT=(5+3)*2”
$ printf “The expression is %d” “$RESULT”
The expression is 16
在 let 命令中的多个变量赋值可以是一个操作符来完成。
$ let “TEST=TEST2=5”
$ printf “The results are %d and %d\n” “$TEST” “$TEST2”
The results are 5 and 5
let 命令可以计算不止一个表达式,几个小的赋值可以在一行里输入。
$ let “SUM=5+5” “SUM2=10+5”
在一行里进行多个赋值操作会导致可读性的降低。而每行只有一个会使脚本很容易理解。
条件操作符“?”是一个三元操作符,它是 if 语句的简写,根据左边的条件将结果分为两个。 let 命令中只对数字表达式起作用,而字符串没用。如下面的示例所示:
$ VALUE=5
$ let “RESULT=VALUE > 1 ? 1 : 0”
$ printf “%d\n” “$VALUE”
1
几个条件操作符可以链接在一起使用:
$ FILE_COUNT=`ls -1 | wc -l`
$ let “RESULT=FILE_COUNT==0 ? 0 : (FILE_COUNT%2 == 0 ? “\
“ FILE_COUNT/2 : FILE_COUNT/2+1)”
$ printf “The files will fit in a report with 2 columns %d high\n” “$RESULT”
The files will fit in a report with 2 columns 11 high
$ printf “%d” “$FILE_COUNT”
22
两个圆括号是 let 命令的另一种写法,它们通常用于嵌入到某个命令中。外壳将使用表达式的值替换两个圆括号。
declare -i X=5;
while (( X-- > 0 )) ; do
printf “%d\n” “$X”
done
本段脚本显示一个数字列表,从 4 到 0 ,嵌入的 let 命令让 X 每循环一次就减一操作。每次循环都要判断 X 是否为 0
tesperature.bash :将华氏温度转换为摄氏温度
为了复习一下 let 命令,请看下面这段程序 temperature.bash ,它的功能是一个将华氏温度转换为摄氏温度。
列表 6.2
#!/bin/bash
#
# temperature.bash: Convert Fahrenheit to Celsius
#
# Ken O. Burtch
# CVS $Header$
shopt -s -o nounset
declare -i FTEMP # Fahrenheit temperature
declare -i CTEMP # Celsius temperature
# Title
printf “%s\n” “Fahrenheit-Celsius Conversion”
printf “\n”
# Get the value to convert
read -p “Enter a Fahrenheit temperature: “ FTEMP
# Do the conversion
CTEMP=”(5*(FTEMP-32) )/9”
printf “The Celsius temperature is %d\n” “$CTEMP”
exit 0
算术测试
test 命令还可以比较数字,但是它使用和字符串不同的操作符。因为所有的外壳变量都是以字符串的形式进行存储,变量比较字符串还是比较数字取决于操作符。 Perl 的操作人员会发现和 Perl 的语法相同。如果字符串的操作符用于整数变量的比较 Bash 不会报错。
n n1 -eq n2— 如果 n1 和 n2 相同返回真
n n1 -ne n2— 如果 n1 和 n2 不同返回真
n n1 -lt n2— 如果 n1 小于 n2 返回真
n n1 -le n2— 如果 n1<=n2 返回真
n n1 -gt n2— 如果 n1>n2 返回真
n n1 -ge n2— 如果 n1>=n2 返回真
例如:假如 RESULT 是当前目录的文件数
$ RESULT=`ls -1 | wc -l`
$ printf “%d” “$RESULT”
22
$ test “$RESULT” -gt 20 && printf “%s” “There are a lot of files.”
There are a lot of files.
$ test “$RESULT” -le 20 && printf “There are few files”
$
这些比较和字符串比较不同,如下所示:
$ test “$RESULT” \< 3 && printf “As a string, the result is less than 3”
As a string, the result is less than 3
这个例子中,字符串“ 22 ”和字符“ 3 ”比较,显然字符“ 22 ”小于字符“ 3 ”,会显示出结果小于 3 。已经要选择正确的比较操作符。
模板匹配
Bash 的模板匹配称之为 globbing 。 globbing 用于匹配文件名,他也可以用于 Korn 外壳 test 命令的字符串匹配。
$ ls *.txt
notes.txt project_notes.txt
模板匹配是输入通配符的方式工作的, Bash 会尝试匹配一个字符串或文件名。
星号“ * ”表示 0~ 多个字符。 Korn 外壳的 test 命令能够使用星号匹配变量中的字符串:
COMPANY=”Athabasca”
if [[ $COMPANY = A* ]] ; then
printf “The company name begins with a letter a A\n”
fi
if [[ $COMPANY = Z* ]] ; then
printf “The company name begins with a letter a Z\n”
fi
当使用双引号时,这种匹配会不起作用。引号的作用是告诉 Korn 外壳不用解释特殊的字符。判断“ A* ”时,直接解释为字符处“ A* ”。
COMPANY=”Athabasca”
if [[ “$COMPANY” = “A*” ]] ; then
printf “The company name is A*\n”
fi
问号“?”是单个字符的通配符。
COMPANY=”AIC”
if [[ $COMPANY = A?? ]] ; then
printf “The company name is 3 characters beginning with A\n”
fi
你可以使用方括号“ [] ”来定义一组字符或某个范围的字符。
if [[ $COMPANY = [ABC]* ]] ; then
printf “The company name begins with a A, B or C\n”
fi
if [[ $COMPANY = [A-Z]* ]] ; then
printf “The company name begins with a letter an uppercase letter\n”
fi
if [[ $COMPANY = [A-Z0-9]* ]] ; then
printf “The company name begins with a letter an uppercase “\
“ letter or number\n”
fi
部分范围也是可以的。如果某个范围的开始字符没有,则这个范围是直接到这个字符的所有 ASCII 值。同样,如果结束字符没有则是从开始字符的所有 ASCII 字符。
如果方括号内的第一个字符时感叹号“!”或脱字符“ ^ ”,则这些字符不应包含在内。
如果使用了 shopt –s extglob 选项,则 Bash 支持几个额外的模板。
n ?(pattern-list)— 匹配 0 个或多个给定的模板
n *(pattern-list)— 匹配 0 个或多个给定的模板
n +(pattern-list)— 匹配 0 个或多个给定的模板
n @(pattern-list)— 精确匹配给定的模板
n !(pattern-list)— 除了给定的模板都可以匹配
COMPANY=”AAA Ballistics Ltd”
if [[ $COMPANY = +(A)*Ltd ]] ; then
printf “The company name begins with one or more A’s and finishes with Ltd\n”
fi
你可以使用竖杠“ | ”将模板列表进行区分开。
COMPANY=”Champion Ltd”
if [[ $COMPANY = Champion*@(Ltd|Corp|Inc) ]] ; then
printf “The company name is Champion with a standard business ending\n”
fi
Bash 还定义了一些通用范围的简写形式称之为 character classes :
n [:alnum:]— 文字和数字
n [:alpha:]— 按字母次序
n [:ascii:]—ASCII 字符
n [:blank:]—Space 或者 tab
n [:cntrl:]— 控制字符
n [:digit:]— 十进制数
n [:graph:]— 非空字符
n [:lower:]— 小写字符
n [:print:]— 非控制字符
n [:space:]— 空格
n [:upper:]— 大写字符
n [:xdigit:]— 十六进制字符
n [:punct:]— 标点符号字符
例如: [:lower:] 等于 [a-z] 。
COMPANY=”2nd Rate Solutions”
if [[ $COMPANY = [[:digit:]]*]] ; then
printf “Company name starts with a digit\n”
fi
对于多语言脚本, Bash 将使用等价类来匹配字符。如果某个字符被一对等号“ = ”包围,外壳将匹配这个字母或相关字符表的类似字母。
COMPANY=”EZ Consulting Services”
if [[ $COMPANY = [=E=]* ]] ; then
printf “Company name starts with an E (or a similar character)\n”
fi
这个判断将匹配和字符“ E ”类似的字符,例如:法语中的 É 。
比较符号可以使用 [.s.] 进行匹配, s 是要比较的符号。
除了 Korn 外壳的 test 命令之外,外壳的行为动作都是相同的。不是匹配字符串中的字符,而是匹配文件的字符。所有的这些特性,包括在扩展的 globbing 特性,都是一样的
$ ls *+(.c|.h)
actions.c coledit.c config.c dump.c form.c form.h main.c
(上面的代码在我的计算机上没有通过,提示 syntax error near unexpected token `(' )
唯一有区别的使用句号“ . ”。因为前导句号表示 linux 的隐藏文件,这些文件通常是看不见的,除非指定在模板匹配文件中。
$ ls .*+(.c|.h)
.hidden.c
因为 Bash 的扩展,所有的命令同样处理这些模板。
$ wc -l *+(.c|.h)
96 actions.c
201 coledit.c
24 config.c
103 dump.c
88 form.c
12 form.h
305 main.c
829 total
如果所有的模板都不匹配, Bash 则认为模板是命令要执行的文件名。
$ ls X*
X* not found
这种情况下,没有文件是 X 开头的。 Bash 将模板 X* 作为文件名传递给 ls 命令,当然找不到匹配。
Globbing 选项
有许多外壳选项都可以影响到模板匹配。设置 noglob 外壳选项可以关闭文件名模板( shopt –s –o noglob )。如果 nullglob 选项被设置(使用 shopt –s nullglob ),如果不匹配模板被丢弃。在先前的例子中, ls 会接受空参数并列出当前目录中所有的文件。你设置 nocaseglob 选项来关闭大小写的区分( shopt –s nocaseglob )。同样设置 dotglob 选项,你可以关闭文件特定的引导句号( shopt –s dotglob )。
这些外壳选项都会在全局起作用,应该小心使用。尽可能少的更改这种设置,并需要清楚的知道这些更改的后果。
GLOBIGNORE 变量也会影响到文件名是如何匹配的。这个变量有点像 PATH 变量,他也是使用冒号进行分割文件名列表, Bash 不会进行匹配。如果你看到在文件名中有模板符号,就把它加到 GLOBIGNORE 列表中,它会保证 Bash 将它们识别为文件而不是 globbing 表达式。
如果你需要更多复杂的模板识别或你需要将整个模板作为一个文件,你可以使用 grep 命令。第 11 、 12 章会详细介绍。
文件大括号扩展
使用大括号可以将一个文件名扩展为多个文件名,在大括号中,使用逗号进行列表的分割。大括号扩展典型用于文件名有不同的结尾。
$ printf “%s %s %s\n” “Files should be named:” orders{.txt,.out}
Files should be named: orders.txt orders.out
Bash 会先将第一行扩展为:
$ printf “%s %s %s\n” “Files should be named:” orders.txt orders.out
大括号里可以包含文件匹配字符。实际的文件匹配会在大括号扩展之后才会处理。
美元符号的替换
美元符号不仅仅用于变量的替换,还有其他一些作用,例如替换字符串的值和模拟其他 linux 命令的模拟,例如:wc 和 sed 。通过美元符号的扩展,外壳不必运行这些命令而且会更快。
ANSI C 换码扩展( $’ )
如果美元符号后面跟着一个单引号,字符串会使用相应的字符来替换 ANSI C 换码字符串。这些换码字符串类似于再 printf 命令中的换码字符串。
n \a— 报警声
n \b— 退格
n \cC— 控制字符 C ( 例如: G for control-G)
n \e— 换码字符
n \f— 换页
n \n— 换行
n \r— 回车
n \t— 水平制表符
n \v— 垂直制表符
n \\— 斜杠
n \’— 单引号
n \nnn— 八进制 ASCII 码字符
n \xnnn— 十六进制 ASCII 码字符
$ printf “%s\n” $’line 1\nline 2\nline 3’
line 1
line 2
line 3
本土化翻译( $” )
如果美元符号是一个双引号,字符串可以根据本地字符集进行翻译。
$ printf “%s\n” $”To a new locale”
To a new locale
本土化将在 18 章进行讨论。
变量名的通配符(! * )
如果大括号内使用感叹号开始使用星号结束,将显示所有以此开始的变量列表。
$ COMPANY=”Nightlight Inc.”
$ printf “%s\n” “${!COMP*}”
COMPANY
变量长度( # )
如果美元符号后面是一对大括号,大括号内是以 # 号开头,则返回变量内容的长度。
$ printf “%s\n” “${#COMPANY}”
15
井号后面是星号则返回传给外壳脚本的参数的个数。
$ printf “%s\n” “${#*}”
0
有点类似于“ $* ”。
缺省值( :- )
如果变量后面跟着冒号和减号,则变量后面跟着是这个变量的缺省值。
$ COMPANY=
$ printf “%s\n” “${COMPANY:-Unknown Company}”
Unknown Company
变量的实际值可以保持不变。
冒号也可以省略掉不用:
$ COMPANY=
$ printf “%s\n” “${COMPANY-Nightlight Inc.}”
$
指定缺省值( := )
如果变量后面跟着冒号和等号,则给空变量指定一个缺省值。
$ printf “%s\n” “${COMPANY:=Nightlight Inc.}”
Nightlight Inc.
$ printf “%s\n” “$COMPANY”
Nightlight Inc.
变量的实际值已经改变了。
去除冒号,则不会指定缺省值。
变量是否存在检查( :? )
如果变量后面跟着冒号和问号,则根据变量是否存在,显示不同的信息。信息不是必选的。
printf “Company is %s\n” \
“ ${COMPANY:?Error: Company has not been defined—aborting}”
如果没有冒号则不会进行检查。
覆盖缺省值 (:+)
如果变量后面跟着冒号和加号,则加好后面的字符串替换默认字符串。
$ COMPANY=”Nightlight Inc.”
$ printf “%s\n” “${COMPANY:+Company has been overridden}”
Company has been overridden
如果没有冒号,变量也被字符串所替换,变量本身的值不改变。
替换部分字符串( :n )
如果变量后面跟着一个冒号和数字,则返回该数字开始的一个子字符串,如果后面还跟着一个冒号和数字。则第一个数字表示开始的字符,后面数字表示字符的长度。
$ printf “%s\n” “${COMPANY:5}”
light Inc.
$ printf “%s\n” “${COMPANY:5:5}”
light
根据模板删除字串( % , # , %% , ## )
如果变量后面跟着井号,则返回匹配模板被删除后的字串。一个井号为最小可能性的匹配,两个井号为自大可能性的匹配。表达式返回模板右边的字符。
$ printf “%s\n” “${COMPANY#Ni*}”
ghtlight Inc.
$ printf “%s\n” “${COMPANY##Ni*}”
$ printf “%s\n” “${COMPANY##*t}”
Inc.
$ printf “%s\n” “${COMPANY#*t}”
light Inc.
使用百分号,表达式返回模板左边的字符
$ printf “%s\n” “${COMPANY%t*}”
Nightligh
$ printf “%s\n” “${COMPANY%%t*}”
Nigh
(在我的 Linux 系统中上述命令不起取用)
使用模板进行子字符串的替换( // )
如果变量后只有一个斜杠,则两个斜杠中间的字符串是要被替换的字符串,而第二个斜杠后面的字符串是要替换的字符串。如果变量后面跟着两个斜杠,则所有出现在两个斜杠中间的字符都要被替换为最后一个斜杠后面的字符。
$ printf “%s\n” “${COMPANY/Inc./Incorporated}”
Nightlight Incorporated
$ printf “You are the I in %s” “${COMPANY//i/I}”
You are the I in NIghtlIght Inc.
如果模板一 # 号开始,则匹配以模板开始的字符,如果模板以 % 号结尾,则匹配以模板结尾的字符。
$ COMPANY=”NightLight Night Lighting Inc.”
$ printf “%s\n” “$COMPANY”
NightLight Night Lighting Inc.
$ printf “%s” “${COMPANY//Night/NIGHT}”
NIGHTLight NIGHT Lighting Inc.
$ printf “%s” “${COMPANY//#Night/NIGHT}”
NIGHTLight Night Lighting Inc.
(我的 Linux 中这个不起作用)
如果没有指定新的值,则匹配的字符会被删除。
$ COMPANY=”Nightlight Inc.”
$ printf “%s\n” “${COMPANY/light}”
Night Inc.
也可以使用范围符号。例如:删除所有字符串中的标点符号,使用范围 [:punct:] 。
$ printf “%s” “${COMPANY//[[:punct:]]}”
Nightlight Inc
使用星号或 @ 符号替换变量会替换外壳脚本中所有的参数,同样,在数组中使用星号或 @ 符号也会替换数组中的所有元素。
命令的结果作为字符进行替换(( .. ))
在美元符号引导的变量使用小括号,可以在其中执行命令。这和在一对脱字符内引入命令有同样的效果。
$ printf “There are %d files in this directory\n” “$(ls -1 | wc -l)”
There are 28 files in this directory
$ printf “There are %d files in this directory\n” `ls -1 | wc -l`
There are 28 files in this directory
算术运算表达式的替换(“(( .. ))”)
使用两个小括号,可以在里面输入算术表达式。它们的作用和 let 命令的作用一样,但是使用双括号可以将结果替换进命令中。
$ ORDERS=50
$ COST=25
$ printf “There are $%d worth of orders in this directory\n” “$((ORDERS*COST))”
There are $1250 worth of orders in this directory
其他测试表达式
test 命令的 -o 开关可以探知某个外壳选项是否被设置。
if [ -o noglob ] ; then
printf “Globbing is off\n”
fi
在 Bash 中有许多类型的表达式,每一个都有自己的语法和操作顺序。学习了这么多,你是否已经忘却了许多不常使用的语法?下一章会更晦涩难懂。
mixer.bash:HTML Color Mixer
列表 6.3 中,是一个计算 HTML 中 Web 页的十六进制颜色码的代码,它用了许多本章讨论的概念,包括 test 命令、美元符号的替换和 let 命令。
列表 6.3
#!/bin/bash
#
# mixer.bash: HTML color mixer
#
# Ken O. Burtch
# CVS: $Header$
shopt -s -o nounset
declare -rx SCRIPT=${0##*/}
declare -i RED # amount of red in color
declare -i GREEN # amount of green in color
declare -i BLUE # amount of blue in color
# Title
printf “%s\n” “This program mixes color values for Web pages”
printf “\n”
# Choose percent or absolute
# If none is given, default to ‘p’
printf “%s\n” “Mix the color by (p)ercent or (a)bsolute amount?”
printf “%s\n” “The default is percentage.”
read -p “Select p or a: “ REPLY
REPLY=”${REPLY:=p}”
printf “\n”
# Read absolute values
if [ “$REPLY” = “a” ] ; then
read -p “How much red (0..255)?” RED
if [ $RED -gt 255 ] ; then
printf “$SCRIPT: too much red\n” >&2
exit 192
fi
if [ $RED -lt 0 ] ; then
printf “$SCRIPT: too little red\n” >&2
exit 192
fi
read -p “How much green (0..255)?” GREEN
if [ $GREEN -gt 255 ] ; then
printf “$SCRIPT: too much green\n” >&2
exit 192
fi
if [ $GREEN -lt 0 ] ; then
printf “$SCRIPT: too little green\n” >&2
exit 192
fi
read -p “How much blue (0..255)?” BLUE
if [ $BLUE -gt 255 ] ; then
printf “$SCRIPT: too much blue\n” >&2
exit 192
fi
if [ $BLUE -lt 0 ] ; then
printf “$SCRIPT: too little blue\n” >&2
exit 192
fi
fi
# Read percentage values and convert to absolute
if [ “$REPLY” = “p” ] ; then
read -p “How much red (0..100)?” RED
if [ $RED -gt 100 ] ; then
printf “$SCRIPT: too much red\n” >&2
exit 192
fi
if [ $RED -lt 0 ] ; then
printf “$SCRIPT: too little red\n” >&2
exit 192
fi
read -p “How much green (0..100)?” GREEN
if [ $GREEN -gt 100 ] ; then
printf “$SCRIPT: too much green\n” >&2
exit 192
fi
if [ $GREEN -lt 0 ] ; then
printf “$SCRIPT: too little green\n” >&2
exit 192
fi
read -p “How much blue (0..100)?” BLUE
if [ $BLUE -gt 100 ] ; then
printf “$SCRIPT: too much blue\n” >&2
exit 192
fi
if [ $BLUE -lt 0 ] ; then
printf “$SCRIPT: too little blue\n” >&2
exit 192
fi
RED=”255*RED/100”
GREEN=”255*GREEN/100”
BLUE=”255*BLUE/100”
fi
# Show the result
printf “HTML color code is #%x%x%x\n” “$RED” “$GREEN” “$BLUE”
exit 0
命令参考
test 命令开关
n -b file— 文件是一个块设备文件返回真
n -c file— 文件是一个字符设备文件返回真
n -d file— 文件是一个目录返回真
n -e file— 文件存在返回真
n f1 -ef f2— 文件 f1 是文件 f2 的硬链接文件返回真
n n1 -eq n2—n1 等于 n2 返回真
n -f file— 文件存在且是一个常规文件返回真
n n1 -ge n2—n1 大于等于 n2 返回真
n n1 -gt n2—n1 大于 n2 返回真
n -g file— 文件有 set-group-id 权限返回真
n -G file— 文件被你的组拥有返回真
n -h file (or -L file)— 文件是符号链接文件返回真
n -k file— 文件有 sticky 权限返回真
n n1 -le n2—n1 小于等于 n2 返回真
n n1 -lt n2—n1 小于 n2 返回真
n -n s (or just s)— 字符串不为空返回真
n -N file— 文件自上次阅读过后已被更新返回真
n n1 -ne n2— 文件 f1 不等于文件 f2 返回真
n f1 -nt f2— 文件 f1 比文件 f2 新返回真
n -O file— 文件被你拥有返回真
n f1 -ot f2— 文件 f1 比文件 f2 旧返回真
n -p file— 文件是管道文件返回真
n -r file— 文件可读返回真
n -s file— 文件存在且不为空返回真
n -S file— 文件是 socket 文件返回真
n -t fd— 文件描述符被终端打开返回真
n -u file— 文件有 set-user-id 设置权限返回真
n -w file— 文件可写返回真
n -x file— 文件可以执行返回真
n -z s— 字符串为空返回真
test 命令字符串判断测试
n s1 = s2—s1 等于 s2 返回真
n s1 != s2—s1 不等于 s2 返回真
n s1 < s2—s1 小于 s2 返回真
n s1 > s2—s1 大于 s2 返回真
字符类
n [:alnum:]— 字符和数字
n [:alpha:]— 字符
n [:ascii:]—ASCII 码
n [:blank:]— 空格和制表符
n [:cntrl:]— 控制字符
n [:digit:]— 十进制数
n [:graph:]— 非空字符
n [:lower:]— 小写字符
n [:print:]— 非控制字符
n [:punct:]— 标点符号字符
n [:space:]— 空格
n [:upper:]— 大写字符
n [:xdigit:]— 十六进制字符
ASCII C 换码扩展
n \a— 报警声
n \b— 退格
n \cC— 控制字符 C ( 例如: G for control-G)
n \e— 换码字符
n \f— 换页
n \n— 换行
n \r— 回车
n \t— 水平制表符
n \v— 垂直制表符
n \\— 斜杠
n \’— 单引号
n \nnn— 八进制 ASCII 码字符
n \xnnn— 十六进制 ASCII 码字符