Bash 编程入门:逻辑运算符和 Shell 扩展

在本 Bash 编程系列文章的第二篇中,了解逻辑运算符和 Shell 扩展。
203 位读者喜欢这篇文章。
Women in computing and open source v5

kris krüg

Bash 是一种强大的编程语言,非常适合在命令行和 Shell 脚本中使用。本三部分系列文章(基于我的三卷 Linux 自学课程)探讨了如何将 Bash 作为命令行界面 (CLI) 上的编程语言使用。

第一篇文章探讨了一些使用 Bash 进行的简单命令行编程,包括使用变量和控制运算符。第二篇文章研究了文件、字符串、数字和杂项逻辑运算符的类型,这些运算符提供了执行流控制逻辑和 Bash 中不同类型的 Shell 扩展。本系列的第三篇也是最后一篇文章将探讨 forwhileuntil 循环,这些循环可以实现重复操作。

逻辑运算符是程序中做出决策并根据这些决策执行不同指令集的基础。这有时称为流程控制。

逻辑运算符

Bash 具有大量的逻辑运算符,可用于条件表达式。if 控制结构的最基本形式是测试条件,然后在条件为真时执行程序语句列表。运算符有三种类型:文件运算符、数字运算符和非数字运算符。如果条件满足,则每个运算符返回真 (0);如果条件不满足,则返回假 (1)。

这些比较运算符的功能语法是一个或两个参数,运算符放在方括号内,后跟如果条件为真则执行的程序语句列表,以及如果条件为假则执行的可选程序语句列表

if [ arg1 operator arg2 ] ; then list
or
if [ arg1 operator arg2 ] ; then list ; else list ; fi

比较中的空格是必需的,如所示。单方括号 [] 是传统的 Bash 符号,等效于 test 命令

if test arg1 operator arg2 ; then list

还有一种更新的语法,它提供了一些优势,并且一些系统管理员更喜欢它。这种格式与其他版本的 Bash 和其他 Shell(如 ksh(Korn Shell))的兼容性稍差。它看起来像

if [[ arg1 operator arg2 ]] ; then list

文件运算符

文件运算符是 Bash 中一组强大的逻辑运算符。图 1 列出了 Bash 可以对文件执行的 20 多个不同的运算符。我在我的脚本中经常使用它们。

运算符 描述
-a 文件名 如果文件存在则为真;它可以是空的或有一些内容,但只要它存在,这将为真
-b 文件名 如果文件存在并且是块特殊文件(如硬盘驱动器,例如 /dev/sda/dev/sda1)则为真
-c 文件名 如果文件存在并且是字符特殊文件(如 TTY 设备,例如 /dev/TTY1)则为真
-d 文件名 如果文件存在并且是目录则为真
-e 文件名 如果文件存在则为真;这与上面的 -a 相同
-f 文件名 如果文件存在并且是常规文件(与目录、设备特殊文件或链接等相反)则为真
-g 文件名 如果文件存在并且是 set-group-idSETGID 则为真
-h 文件名 如果文件存在并且是符号链接则为真
-k 文件名 如果文件存在并且其“粘滞”位已设置则为真
-p 文件名 如果文件存在并且是命名管道 (FIFO) 则为真
-r 文件名 如果文件存在并且可读,即已设置其读取位,则为真
-s 文件名 如果文件存在并且大小大于零则为真;如果文件存在但大小为零,则返回假
-t fd 如果文件描述符 fd 已打开并且引用终端则为真
-u 文件名 如果文件存在并且其 set-user-id 位已设置则为真
-w 文件名 如果文件存在并且可写则为真
-x 文件名 如果文件存在并且可执行则为真
-G 文件名 如果文件存在并且由有效组 ID 拥有则为真
-L 文件名 如果文件存在并且是符号链接则为真
-N 文件名 如果文件存在并且自上次读取以来已被修改则为真
-O 文件名 如果文件存在并且由有效用户 ID 拥有则为真
-S 文件名 如果文件存在并且是套接字则为真
文件 1 -ef 文件 2 如果 文件 1 和 文件 2 引用相同的设备和 iNode 编号则为真
文件 1 -nt 文件 2 如果 文件 1 比 文件 2 新(根据修改日期),或者如果 文件 1 存在而 文件 2 不存在则为真
文件 1 -ot 文件 2 如果 文件 1 比 文件 2 旧,或者如果 文件 2 存在而 文件 1 不存在则为真

图 1:Bash 文件运算符

例如,首先测试文件是否存在

[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 does not exist.
[student@studentvm1 testdir]$

接下来,创建一个名为 TestFile1 的文件进行测试。目前,它不需要包含任何数据

[student@studentvm1 testdir]$ touch TestFile1

更改 $File 变量的值而不是此短 CLI 程序中多个位置的文件名的文本字符串很容易

[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 exists.
[student@studentvm1 testdir]$

现在,运行测试以确定文件是否存在并且具有非零长度,这意味着它包含数据。您要测试三个条件:1. 文件不存在;2. 文件存在但为空;3. 文件存在并包含数据。因此,您需要一组更复杂的测试 — 在 if-elif-else 结构中使用 elif 节来测试所有条件

[student@studentvm1 testdir]$ File="TestFile1" ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
[student@studentvm1 testdir]$

在本例中,文件存在但不包含任何数据。添加一些数据并重试

[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$

这可行,但它仅对于三种可能的条件之一真正准确。添加一个 else 节,以便您可以更准确一些,并删除该文件,以便您可以完全测试此新代码

[student@studentvm1 testdir]$ File="TestFile1" ; rm $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.

现在创建一个空文件进行测试

[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.

向文件中添加一些内容并再次测试

[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 exists and contains data.

现在,添加 elif 节以区分不存在的文件和空文件

[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and is empty.
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$

现在,您有一个 Bash CLI 程序,可以测试这三种不同的条件……但可能性是无限的。

如果您像在脚本中一样更像地排列程序语句,则更容易看到更复杂的复合命令的逻辑结构,您可以将其保存在文件中。图 2 显示了它的外观。if-elif-else 结构的每个节中程序语句的缩进有助于阐明逻辑。

File="TestFile1"
echo "This is $File" > $File
if [ -s $File ]
   then
   echo "$File exists and contains data." 
elif [ -e $File ]
   then 
   echo "$File exists and is empty."
else 
   echo "$File does not exist."
fi

图 2:命令行程序重写为脚本中的外观

对于大多数 CLI 程序来说,如此复杂的逻辑过于冗长。虽然任何 Linux 或 Bash 内置命令都可以在 CLI 程序中使用,但随着 CLI 程序变得更长更复杂,创建存储在文件中的脚本并在现在或将来随时执行更有意义。

字符串比较运算符

字符串比较运算符可以比较字母数字字符的字符串。只有少数几个这样的运算符,如图 3 所示。

运算符 描述
-z 字符串 如果字符串的长度为零则为真
-n 字符串 如果字符串的长度为非零则为真
字符串 1 == 字符串 2



字符串 1 = 字符串 2
如果字符串相等则为真;对于 POSIX 一致性,应将单个 = 与 test 命令一起使用。当与 [[ 命令一起使用时,这将执行如上所述的模式匹配(复合命令)。
字符串 1 != 字符串 2 如果字符串不相等则为真
字符串 1 < 字符串 2 如果 字符串 1 在 字符串 2 之前按字典顺序排序,则为真 (指所有字母数字和特殊字符的特定于区域设置的排序序列)
字符串 1 > 字符串 2 如果 字符串 1 在 字符串 2 之后按字典顺序排序,则为真

图 3:Bash 字符串逻辑运算符

首先,查看字符串长度。比较中 $MyVar 周围的引号必须在那里,比较才能起作用。(您应该仍然在 ~/testdir 中工作。)

[student@studentvm1 testdir]$ MyVar="" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.

您也可以这样做

[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar contains data.
[student@studentvm1 testdir]$ MyVar="" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar is zero length

有时您可能需要知道字符串的确切长度。这不是比较,但它与之相关。不幸的是,没有简单的方法来确定字符串的长度。有几种方法可以做到这一点,但我认为使用 expr(评估表达式)命令是最容易的。阅读 expr 的手册页,了解更多关于它可以做什么的信息。请注意,您要测试的字符串或变量周围需要引号。

[student@studentvm1 testdir]$ MyVar="" ; expr length "$MyVar"
0
[student@studentvm1 testdir]$ MyVar="How long is this?" ; expr length "$MyVar"
17
[student@studentvm1 testdir]$ expr length "We can also find the length of a literal string as well as a variable."
70

关于比较运算符,我在脚本中进行了大量测试,以确定两个字符串是否相等(即,相同)。我使用了此比较运算符的非 POSIX 版本

[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello World" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 matches Var2
[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello world" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 and Var2 do not match.

自己做更多实验以尝试这些运算符。

数字比较运算符

数字运算符在两个数字参数之间进行比较。与其他运算符类一样,大多数运算符都易于理解。

运算符 描述
参数 1 -eq 参数 2 如果 参数 1 等于 参数 2 则为真
参数 1 -ne 参数 2 如果 参数 1 不等于 参数 2 则为真
参数 1 -lt 参数 2 如果 参数 1 小于 参数 2 则为真
参数 1 -le 参数 2 如果 参数 1 小于或等于 参数 2 则为真
参数 1 -gt 参数 2 如果 参数 1 大于 参数 2 则为真
参数 1 -ge 参数 2 如果 参数 1 大于或等于 参数 2 则为真

图 4:Bash 数字比较逻辑运算符

以下是一些简单的示例。第一个实例将变量 $X 设置为 1,然后测试 $X 是否等于 1。在第二个实例中,X 设置为 0,因此比较不为真。

[student@studentvm1 testdir]$ X=1 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X equals 1
[student@studentvm1 testdir]$ X=0 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X does not equal 1
[student@studentvm1 testdir]$

自己尝试更多实验。

杂项运算符

这些杂项运算符显示是否设置了 Shell 选项或 Shell 变量是否具有值,但它不会发现变量的值,只是它是否具有值。

运算符 描述
-o optname 如果启用了 Shell 选项 optname 则为真(请参阅 Bash 手册页中 Bash set 内置的 -o 选项描述下的选项列表)
-v varname 如果设置了 Shell 变量 varname(已分配值)则为真
-R varname 如果设置了 Shell 变量 varname 并且是名称引用则为真

图 5:杂项 Bash 逻辑运算符

自己做实验以尝试这些运算符。

扩展

Bash 支持多种类型的扩展和替换,这些扩展和替换可能非常有用。根据 Bash 手册页,Bash 有七种形式的扩展。本文着眼于其中的五种:波浪号扩展、算术扩展、路径名扩展、大括号扩展和命令替换。

大括号扩展

大括号扩展是一种生成任意字符串的方法。(此工具在下面用于创建大量文件,以便使用特殊模式字符进行实验。)大括号扩展可用于生成任意字符串列表,并将它们插入到封闭静态字符串中的特定位置或静态字符串的任一端。这可能很难可视化,因此最好直接执行它。

首先,这是大括号扩展的作用

[student@studentvm1 testdir]$ echo {string1,string2,string3}
string1 string2 string3

好吧,这没什么帮助,不是吗?但看看当你以稍微不同的方式使用它时会发生什么

[student@studentvm1 testdir]$ echo "Hello "{David,Jen,Rikki,Jason}.
Hello David. Hello Jen. Hello Rikki. Hello Jason.

这看起来像一些有用的东西 — 它可以节省大量打字时间。现在试试这个

[student@studentvm1 testdir]$ echo b{ed,olt,ar}s
beds bolts bars

我可以继续,但你明白了。

波浪号扩展

可以说,最常见的扩展是波浪号 (~) 扩展。当您在 cd ~/Documents 等命令中使用它时,Bash Shell 会将其扩展为用户完整主目录的快捷方式。

使用这些 Bash 程序来观察波浪号扩展的效果

[student@studentvm1 testdir]$ echo ~
/home/student
[student@studentvm1 testdir]$ echo ~/Documents
/home/student/Documents
[student@studentvm1 testdir]$ Var1=~/Documents ; echo $Var1 ; cd $Var1
/home/student/Documents
[student@studentvm1 Documents]$

路径名扩展

路径名扩展是一个花哨的术语,用于将文件全局模式(使用字符 ?*)扩展为与模式匹配的目录的完整名称。文件全局指的是特殊模式字符,这些字符在执行各种操作时,可以在匹配文件名、目录和其他字符串时提供极大的灵活性。这些特殊模式字符允许匹配字符串中的单个、多个或特定字符。

  • ? — 仅匹配字符串中指定位置的任何一个字符
  • * — 匹配字符串中指定位置的零个或多个任何字符

此扩展应用于匹配的目录名称。要查看其工作原理,请确保 testdir 是当前工作目录 (PWD),并从纯列表开始(我的主目录的内容将与您的不同)

[student@studentvm1 testdir]$ ls 
chapter6  cpuHog.dos    dmesg1.txt  Documents  Music       softlink1  testdir6    Videos
chapter7  cpuHog.Linux  dmesg2.txt  Downloads  Pictures    Templates  testdir
testdir  cpuHog.mac    dmesg3.txt  file005    Public      testdir    tmp
cpuHog     Desktop       dmesg.txt   link3      random.txt  testdir1   umask.test
[student@studentvm1 testdir]$

现在列出以 Dotestdir/Documentstestdir/Downloads 开头的目录

Documents:
Directory01  file07  file15        test02  test10  test20      testfile13  TextFiles
Directory02  file08  file16        test03  test11  testfile01  testfile14
file01       file09  file17        test04  test12  testfile04  testfile15
file02       file10  file18        test05  test13  testfile05  testfile16
file03       file11  file19        test06  test14  testfile09  testfile17
file04       file12  file20        test07  test15  testfile10  testfile18
file05       file13  Student1.txt  test08  test16  testfile11  testfile19
file06       file14  test01        test09  test18  testfile12  testfile20

Downloads:
[student@studentvm1 testdir]$

好吧,这没有达到你想要的效果。它列出了以 Do 开头的目录的内容。要仅列出目录而不列出其内容,请使用 -d 选项。

[student@studentvm1 testdir]$ ls -d Do*
Documents  Downloads
[student@studentvm1 testdir]$

在这两种情况下,Bash Shell 都将 Do* 模式扩展为与该模式匹配的两个目录的名称。但是,如果也有与该模式匹配的文件呢?

[student@studentvm1 testdir]$ touch Downtown ; ls -d Do*
Documents  Downloads  Downtown
[student@studentvm1 testdir]$

这也显示了该文件。因此,与该模式匹配的任何文件也会扩展为它们的完整名称。

命令替换

命令替换是一种扩展形式,它允许将一个命令的 STDOUT 数据流用作另一个命令的参数;例如,作为要在循环中处理的项目列表。Bash 手册页说:“命令替换允许命令的输出替换命令名称。” 我发现如果有点晦涩,这也很准确。

这种替换有两种形式,`命令`$(命令)。在使用反引号 (`) 的旧形式中,在命令中使用反斜杠 (\) 会保留其字面意思。但是,当它在较新的括号形式中使用时,反斜杠会将其含义作为特殊字符。另请注意,括号形式仅使用单括号来打开和关闭命令语句。

我经常在命令行程序和脚本中使用此功能,其中一个命令的结果可以用作另一个命令的参数。

从一个非常简单的示例开始,该示例使用此扩展的两种形式(再次,确保 testdir 是 PWD)

[student@studentvm1 testdir]$ echo "Todays date is `date`"
Todays date is Sun Apr  7 14:42:46 EDT 2019
[student@studentvm1 testdir]$ echo "Todays date is $(date)"
Todays date is Sun Apr  7 14:42:59 EDT 2019
[student@studentvm1 testdir]$

seq 实用程序的 -w 选项将前导零添加到生成的数字中,以便它们都具有相同的宽度,即,无论值如何,数字位数都相同。这使得按数字顺序对它们进行排序更容易。

seq 实用程序用于生成数字序列

[student@studentvm1 testdir]$ seq 5
1
2
3
4
5
[student@studentvm1 testdir]$ echo `seq 5`
1 2 3 4 5
[student@studentvm1 testdir]$

现在您可以做一些更有用的事情,例如创建大量空文件进行测试

[student@studentvm1 testdir]$ for I in $(seq -w 5000) ; do touch file-$I ; done

在此用法中,语句 seq -w 5000 生成从 1 到 5,000 的数字列表。通过将命令替换用作 for 语句的一部分,数字列表由 for 语句用于生成文件名的数字部分。

算术扩展

Bash 可以执行整数数学运算,但这相当麻烦(您很快就会看到)。算术扩展的语法是 $((算术表达式)),使用双括号打开和关闭表达式。

算术扩展的工作方式类似于 Shell 程序或脚本中的命令替换;从表达式计算出的值将替换表达式,以便 Shell 进行进一步评估。

再次,从一些简单的东西开始

[student@studentvm1 testdir]$ echo $((1+1))
2
[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1*Var2)) ; echo "Var 3 = $Var3"
Var 3 = 35

以下除法结果为零,因为结果将是小于 1 的十进制值

[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1/Var2)) ; echo "Var 3 = $Var3"
Var 3 = 0

这是一个我在脚本或 CLI 程序中经常做的简单计算,它告诉我 Linux 主机中我拥有多少总虚拟内存。free 命令不提供该数据

[student@studentvm1 testdir]$ RAM=`free | grep ^Mem | awk '{print $2}'` ; Swap=`free | grep ^Swap | awk '{print $2}'` ; echo "RAM = $RAM and Swap = $Swap" ; echo "Total Virtual memory is $((RAM+Swap))" ;
RAM = 4037080 and Swap = 6291452
Total Virtual memory is 10328532

我使用 ` 字符来分隔用于命令替换的代码段。

我主要使用 Bash 算术扩展来检查脚本中的系统资源量,然后根据结果选择程序执行路径。

总结

本文是关于 Bash 作为编程语言的系列文章的第二篇,探讨了 Bash 文件、字符串、数字和杂项逻辑运算符,这些运算符提供了执行流程控制逻辑和不同类型的 Shell 扩展。

本系列的第三篇文章将探讨循环的使用,以执行各种类型的迭代操作。

标签
David Both
David Both 是一位开源软件和 GNU/Linux 倡导者、培训师、作家和演讲者。自 1996 年以来,他一直从事 Linux 和开源软件方面的工作,自 1969 年以来一直从事计算机方面的工作。他是“系统管理员的 Linux 哲学”的坚定拥护者和传播者。

3 条评论

很棒的文章,非常有用。感谢分享!

很棒的文章!当我编写脚本时,这可能会成为我的首选资源之一(没有双关语的意思)。

还有一件事,关于命令替换,我一直使用旧形式的 `command`,但我发现这在 Markdown 中编写文档时会产生问题,因为刻度线用于表示代码,例如
`# ls -l`
因此,切换到较新形式的 $(command) 可以解决此格式冲突。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.