13种Shell逻辑与算术,能写出5种算你赢!

语言: CN / TW / HK

相较于最初的 Bourne shell,现代 bash 版本的最大改进之一体现在算术方面。早期的 shell 版本没有内建的算术功能,哪怕是给变量加1,也得调用单独的程序来完成。

1、算术方法一: $(( ))

只要都是整数运算,就可以在 $(( )) 的算术表达式内使用所有的标准运算符。还有一个额外的运算符:可以用** 进行幂运算,如下:

COUNT=$((COUNT + 5 + MAX * 2))

或者:

MAX=$((2**8))

$(( )) 表达式内不需要使用空格,不过在运算符和操作数两边加上空格也无妨(但 ** 必须写在一起)。但是 = 两边绝不能出现空格,这和 bash 变量赋值的规则一样。如果你按以下方式写:

COUNT = $((COUNT+5))   # 注意 = 号两边多了空格,可不像你想的那样!

那么,bash 会尝试运行一个名为 COUNT 的程序,其第一个参数为 =,第二个参数为 $COUNT 与 5 之和。记住,别在赋值号两边加空格!

另一个怪异之处是,通常出现在 shell 变量前表示取值的 $ 符号(如 $COUNT 或 $MAX)在双括号内部是不需要的。例如,我们可以写:

$((COUNT + 5 + MAX * 2))

shell 变量前并没有 $ 符号,实际上,外部的 $ 应用于整个表达式。但如果用到了位置参数(如 $2),那么 $ 还是少不了的,因为只有这样才能区分位置参数与数字常量(如 2)。以下是一个示例。

COUNT=$((COUNT + $2 + OFFSET))

也可以用逗号运算符形成级联赋值,如下图:

echo $(( X+=5 , Y*=3 ))

该表达式执行两次赋值操作,然后由 echo 显示出第二个子表达式的结果(因为逗号运算符返回其第二个操作数的值)。

2、算术方法二:let

除去使用$(())可进行算术运算外,还可以使用let语句,如下:

let COUNT=COUNT+5

同$(())一样,在使用变量时不需要使用$符号。但是,当我们需要使用let进行COUNT=$((COUNT + 5 + MAX * 2))格式的运算时,需要使用到引号‘’,如下:

let COUNT+='5+MAX*2'

let 语句和 $(( )) 语法的另一处重要区别在于两者处理空白字符(空格字符)的方式不同。对 let 语句来说,要么添加引号,要么赋值运算符(=)和其他运算符两边不能出现空格。必须将运算符和操作数放在一起形成一个单词。以下两种写法都没问题。

let i=2+2
let "i = 2 + 2"

$(( )) 语法就宽松多了,它允许各种空白字符出现在双括号内。这种写法不易出错,代码的可读性也要好得多,是我们执行 bash 整数运算时的首选方式。

3、bash中的赋值运算符

file

4、条件分支if

条件判断,逻辑分支是任何一个语言都会遇到的问题,bash中同其他语言类似,都是使用if进行条件判断,如下:

if [ $# -lt 3 ]
then
 printf "%b" "Error. Not enough arguments.\n"
 printf "%b" "usage: myscript file1 op file2\n"
 exit 1
fi

或者:

if (( $# < 3 ))
then
 printf "%b" "Error. Not enough arguments.\n"
 printf "%b" "usage: myscript file1 op file2\n"
 exit 1
fi

以下是一个带有 elif(bash 中的 else-if)和 else 子句的完整if 语句。如下:

if (( $# < 3 ))
then
 printf "%b" "Error. Not enough arguments.\n"
 printf "%b" "usage: myscript file1 op file2\n"
 exit 1
elif (( $# > 3 ))
then
 printf "%b" "Error. Too many arguments.\n"
 printf "%b" "usage: myscript file1 op file2\n"
 exit 2
else
 printf "%b" "Argument count correct. Proceeding...\n"
fi

关于if,我们有两个问题需要明白,分别是:

  • if 语句的基本结构
  • if 表达式的不同语法(括号或方括号,运算符或选项)

5、if的基本结构

按照 bash 手册页中的描述,if 语句的一般形式如下所示。

if list; then list; [ elif list; then list; ] ... [ else list; ]
fi

[ 和 ] 用于划分语句中的可选部分(例如,有些 if 语句中就没有else 子句)。我们先来看看不带任何可选部分的 if 语句。

最简单的 if 语句形式如下所示。

if list; then list; fi

在 bash 中,和换行符一样,分号的作用也是结束某个语句。我们可以用分号将解决方案部分中的示例塞进更少的行中,但使用换行符的可读性更好。then list 的存在看起来是有意义的,其中的语句在 if 条件为真的情况下执行(我们也可以从其他编程语言中猜测出来)。但是,if list 算是怎么回事?难道不应该是 if expression 吗?

没错,但这是 shell,一个命令处理器。它的主要任务就是执行命令。因此,if 后面的 list 就是放置命令列表的地方。你可能会问,决定分支走向(then 子句或 else 子句)的是什么呢?答案是list 中最后一个命令的返回值

我们通过一个有点奇怪的示例来说明这一点。如下:

$ cat trythis.sh // 查看脚本内容,如下所示

if ls; pwd; cd $1; 

then

 echo success

else

 echo failed

fi

// 执行脚本,传递一个参数
$ bash ./trythis.sh /tmp

在这个奇怪的脚本中,shell 会在选择分支前执行 3 个命令(ls、pwd、cd),其中 cd 命令的参数是调用该脚本时所提供的第一个命令行参数。如果没有提供参数,那就只执行 cd,返回到主目录中。结果会是怎样?你可以自己试试。最终是显示“success”还是“failed”,取决于 cd 命令是否执行成功。在示例中,cd 是 if语句命令列表中的最后一个命令。如果 cd 执行失败,就转到 else子句;但如果执行成功,则选择 then 子句。

6、if中的 [] 和 (())

我们一起来看下面的例子:

if test $# -lt 3
then
 echo try again.
fi

前面讲到if后面是list是命令列表,虽然此处不是命令列表,但有没有从中看出起码类似于单个 shell 命令(内建命令 test 接受参数并比较参数值)的东西?

在本章开头,我们给出的第一个示例中开头的 if [ $# -lt 3 ] 看起来很像test 命令。这是因为 [ 其实只是相同命令的不同名称而已。(出于可读性和美观方面的考虑,调用 [ 时还要求将 ] 作为最后一个参数。)因此,对于该语法,if 语句中的表达式其实就是一个只包含单个命令(test 命令)的列表。

在早期的 Unix 中,test 是一个独立的可执行文件,[ 只是指向该文件的链接。现在两者仍以可执行文件的形式存在,但bash 也将它们实现为内建命令。

那么 if (( $# < 3 )) 又是什么意思?

双括号是复合命令的一种。因为它会对其中的算术表达式求值,所以能在 if 语句中派上用场。这是一处比较新的 bash 改进 ,专门用于有 if 语句的场合。

可用于 if 语句的这两种语法之间的重要区别在于测试的表达方式及其能够测试的对象种类

双括号仅限于算术表达式,方括号还可以测试文件特性,但后者的算术测试语法远不如前者方便,尤其是用括号将表达式划分成若干子表达式时。

当我们使用 [ ] 时,一定要注意空格是必须存在的,如下图:

if [ -d "/opt/" ]

7、测试文件的特性

为了提高脚本的稳健性,你希望在读取输入文件前先检查该文件是否存在;另外,还想在写入输出文件前确认其是否具备写权限,在用cd 切换目录前看看到底有没有这个目录。这些该如何在 bash 脚本中实现呢?如下所示:

#!/usr/bin/env bash
# 实例文件:checkfile
#
DIRPLACE=/tmp
INFILE=/home/yucca/amazing.data
OUTFILE=/home/yucca/more.results

if [ -d "$DIRPLACE" ] // 判断是否目录
then
    cd $DIRPLACE
    if [ -e "$INFILE" ] // 判断文件是否存在
    then
        if [ -w "$OUTFILE" ] // 判断文件是否拥有写权限
        then
        	doscience < "$INFILE" >> "$OUTFILE"
        else
       		echo "cannot write to $OUTFILE"
        fi
    else
    	echo "cannot read from $INFILE"
    fi
else
	echo "cannot cd into $DIRPLACE"
fi

将各种文件名引用全都放入了引号,以防路径名中包含空格。在上面的例子,我们使用了测试文件是否是目录(-d)、文件是否存在(-e)、文件是否有写权限(-w),我们也可以测试一些别的文件特性,其中有 3 个特性要用到双目运算符(接受两个文件名)。

  • FILE1 -nt FILE2 是否更新(检查文件的修改时间)。现有文件要比不存在的文件“新”。
  • FILE1 -ot FILE2 是否更旧。同样,不存在的文件要比现有文件“旧”。
  • FILE1 -ef FILE2 具有相同设备和 inode 编号(即便由不同链接所指向,也视为相同的文件)

前面使用的-e、-d、-w都属于单目运算符,其形式为 option filename,例如,if [ -e myfile ]

8、测试多个特性

前面,我们测试每个特性都是使用单独一个if语句,那么我们测试多个特性时,必须嵌套if语句吗?

使用 -a(逻辑与)和 -o(逻辑或)运算符将多个测试条件组合成一个表达式。例如:

if [ -r $FILE -a -w $FILE ]

该 if 语句会测试指定文件是否可读并且可写。

测试时,为啥不加上-e呢?因为所有的文件测试条件都隐含了该文件存在的测试,所以测试文件可读性时不用测试文件是否存在。如果文件不存在,自然也就不可读。这些逻辑运算符(-a 表示 AND,-o 表示 OR)可用于所有的测试条件,并不局限于文件测试。

同一个语句中可以出现多个 AND/OR。你可能要用括号来获得正确的优先级,比如 a and (b or c),但一定要记得在括号前加上反斜杠或将括号放进引号,以消除其特殊含义。如下:

if [ -r "$FN" -a \( -f "$FN" -o -p "$FN" \) ]

9、测试字符串特性

你希望在使用字符串前先检查一下它们的值。这些字符串可以是用户输入、读入的文件或传入脚本的环境变量。如何用 bash 脚本实现呢?

你可以在 if 语句中使用单方括号形式的 test 命令进行一些简单的测试,其中包括检查变量是否包含文本以及两个变量中的字符串是否相同。如下脚本所示:

# 使用命令行参数
VAR="$1"
#
# if [ "$VAR" ]这种形式通常也管用,但并不是一种好的写法,加上-n会更清晰

if [ -n "$VAR" ]
then
	echo has text
else
	echo zero length
fi

if [ -z "$VAR" ]
then
	echo zero length
else echo has text
fi

长度为 0 的变量有两种:设置为空串的变量和不存在的变量。示例中的测试并不区分这两种情况。它只关心变量中是否有字符存在。

重要的是要将 $VAR 放进引号,否则测试会被一些怪异的用户输入干扰。如果 $VAR 的值是 x -a 7 -lt 5 且没有使用引号,那么下列语句:

if [ -z $VAR ]

就会变成(在变量扩展之后):

if [ -z x -a 7 -lt 5 ]

10、测试等量关系

你想要检查两个 shell 变量是否相等,但是存在两种测试运算符:-eq 和 =(或 ==)。该用哪个呢?

你需要的比较类型决定了该用哪种运算符。

  • 如果是进行数值比较,可以使用 -eq 运算符。
  • 如果是进行字符串比较,则使用 =(或 ==)运算符。

下面,我们通过一个简单的脚本例子来演示,如下:

#
# 老生常谈的字符串与数值比较
#

VAR1=" 05 "
VAR2="5"
printf "%s" "do they -eq as equal? "
if [ "$VAR1" -eq "$VAR2" ]
then
	echo YES
else
	echo NO
fi

printf "%s" "do they = as equal? "

if [ "$VAR1" = "$VAR2" ]
then
	echo YES
else
	echo NO
fi

如果,我们运行脚本,则会得到如下结果:

$ ./脚本名
do they -eq as equal? YES
do they = as equal? NO
$

尽管两个变量的数值相等(5),但从字符角度来看,前导字符 0 和空白字符意味着这两个字符串并不相同。

= 和 == 都可以使用,但 = 符合 POSIX 标准,可移植性更好。

使用if,我们可以在脚本中进行分支判断。但是对于系统而言,循环同分支一样是常见需求。所以Shell一样支持循环操作

11、循环一段时间

对于算术条件,使用 while 循环:

while (( COUNT < MAX )) // 判断条件是否成立
do // 语法要求,以do开始
 some stuff
 let COUNT++
done // 语法要求,以done结束

对于文件系统相关的条件:

while [ -z "$LOCKFILE" ]
do
 some things
done

第一个 while 语句中的双括号界定了算术表达式,这很像 shell 变量赋值中用到的 $(( ))。双括号内出现的变量名表示取值。也就是说,不需要写成 $VAR,直接在括号中使用 VAR就行了。

while [ -z"$LOCKFILE" ] 中的方括号和 if 语句中的一样,等同于使用 test 命令。

使用(( )) 时,shell 会对其中的表达式求值,如果结果为非0,那么 (( )) 就返回 0;如果结果为 0,则返回 1。这意味着我们可以像 Java 或 C 程序员那些书写表达式,但 while 语句沿用的仍旧是 bash 那一套,视 0 为真。实际上,这意味着我们可以编写一个无限循环:

while (( 1 ))
do

 ...dosomething

done

12、循环若干次

如果需要循环够一定次数。可以使用 while 循环,在计数时进行测试,不过编程语言中的 for 循环正是针对这种情况设计的。那么,如何在 bash 中实现呢?

使用 for 循环语法的一种特例,看起来和 C 语言中的差不多,但使用的是双括号。

for (( i=0 ; i < 10 ; i++ )) ; do echo $i ; done

在早期的 shell 版本中,for 循环只能按照固定的列表项进行迭代。和文件名之类的打交道时,shell 脚本是面向单词的,就此而言,这算得上是一个不错的创新。但如果需要计数,用户会发现自己可能写出了如下代码。

for i in 1 2 3 4 5 6 7 8 9 10
do
 echo $i
done

看起来还行,尤其是循环次数不多时。可是说实话,换成 500 次循环可就不好使了。

bash 2.04 版开始引入一种 for 循环的变体,语法与 C 语言类似。其一般形式如下所示。

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

双括号表明这是算术表达式,在其中引用变量时,不用加 $(但 $1等位置参数除外),只要是 bash 中出现双括号的地方,均是如此。该表达式是整数表达式,可以使用包括逗号(用于在一个表达式中放入多个操作)在内的大量运算符。

for (( i=0, j=0 ; i+j < 10 ; i++, j++ ))
do
 echo $((i*j))
done

for 循环先初始化了两个变量($i 和 $j),然后在第二个更复杂的子表达式中对 $i 和 $j 求和,接着判断是否小于 10。第三个子表达式再次用逗号运算符累加这两个变量。

13、在循环中使用浮点值

带有算术表达式的 for 循环只能执行整数运算。如果是浮点值,该怎么办呢?

如果系统提供了 seq 命令,则可以用它来生成浮点值。

for fp in $(seq 1.0 .01 1.1)
do
 echo $fp; other stuff too
done

seq 命令会生成一系列浮点值,每行一个。该命令的参数依次是起始值、增量、结束值。$() 在子 shell 中执行命令,返回结果中的换行符会被空白字符替换,因此,就 for 循环而言,每个值都是字符串。

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!