在本 Bash 编程系列文章的第二篇中,了解逻辑运算符和 shell 扩展。
作者注:本系列文章最初发表于 Opensource.com。《原始文章》由两个系列组成,其余的以独立文章形式发表。这些文章都已合并为一个系列,并为此目的进行了更新和修改。
简介
Bash 是一种强大的编程语言,非常适合在命令行和 shell 脚本中使用。本系列文章(基于我的 三卷 Linux 自学课程)探讨了在命令行界面 (CLI) 上使用 Bash 作为编程语言。
第二篇文章研究了文件、字符串、数值和各种逻辑运算符的类型,这些运算符提供了执行流控制逻辑以及 Bash 中不同类型的 shell 扩展。逻辑运算符是在程序中做出决策并根据这些决策执行不同指令集的基础。(这有时称为流程控制。)
逻辑运算符
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 filename | 如果文件存在则为真;它可以为空或包含一些内容,但只要它存在,它就为真 |
-b filename | 如果文件存在并且是块特殊文件(如硬盘驱动器 /dev/sda 或 /dev/sda1)则为真 |
-c filename | 如果文件存在并且是字符特殊文件(如 TTY 设备 /dev/TTY1)则为真 |
-d filename | 如果文件存在并且是目录则为真 |
-e filename | 如果文件存在则为真;这与上面的 -a 相同 |
-f filename | 如果文件存在并且是常规文件(而不是目录、设备特殊文件或链接等)则为真 |
-g filename | 如果文件存在并且是 set-group-id、SETGID 则为真 |
-h filename | 如果文件存在并且是符号链接则为真 |
-k filename | 如果文件存在并且其“sticky”位已设置则为真 |
-p filename | 如果文件存在并且是命名管道 (FIFO) 则为真 |
-r filename | 如果文件存在并且可读,即已设置其读取位,则为真 |
-s filename | 如果文件存在并且大小大于零,则为真;如果文件存在但大小为零,则返回假 |
-t fd | 如果文件描述符 fd 已打开并且引用终端,则为真 |
-u filename | 如果文件存在并且其 set-user-id 位已设置,则为真 |
-w filename | 如果文件存在并且可写,则为真 |
-x filename | 如果文件存在并且可执行,则为真 |
-G filename | 如果文件存在并且由有效组 ID 拥有,则为真 |
-L filename | 如果文件存在并且是符号链接则为真 |
-N filename | 如果文件存在并且自上次读取以来已被修改,则为真 |
-O filename | 如果文件存在并且由有效用户 ID 拥有,则为真 |
-S filename | 如果文件存在并且是套接字,则为真 |
file1 -ef file2 | 如果 file1 和 file2 引用相同的设备和 iNode 编号,则为真 |
file1 -nt file2 | 如果 file1 比 file2 新(根据修改日期),或者如果 file1 存在而 file2 不存在,则为真 |
file1 -ot file2 | 如果 file1 比 file2 旧,或者如果 file2 存在而 file1 不存在,则为真 |
例如,首先测试文件是否存在
$ 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.
接下来,创建一个名为 TestFile1 的文件用于测试。目前,它不需要包含任何数据
$ touch TestFile1
在简短的 CLI 程序中,更改 $File 变量的值而不是文件名的文本字符串很容易在多个位置进行
$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 exists.
现在,运行测试以确定文件是否存在并且具有非零长度,这意味着它包含数据。您要测试三种情况:1. 文件不存在;2. 文件存在并且为空;3. 文件存在并且包含数据。因此,您需要更复杂的一组测试——在 if-elif-else 结构中使用 elif 子句来测试所有条件
[student@test1 testdir]$ File="TestFile1" ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
在这种情况下,文件存在但不包含任何数据。添加一些数据并重试
$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
TestFile1 exists and contains data.
这可行,但它仅对三种可能情况中的一种特定情况真正准确。添加一个 else 子句,以便您可以更准确一些,并删除该文件,以便您可以完全测试此新代码
$ 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.
现在创建一个空文件进行测试
$ 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.
向文件中添加一些内容并再次测试
$ 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 子句以区分不存在的文件和空文件
$ 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.
$ 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.
现在,您有一个 Bash CLI 程序,可以测试这三种不同的情况……但可能性是无限的。
如果您像在可以保存在文件中的脚本中那样安排程序语句,则更容易看到更复杂的复合命令的逻辑结构。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
这种复杂的逻辑对于大多数 CLI 程序来说太长了。虽然任何 Linux 或 Bash 内置命令都可以在 CLI 程序中使用,但随着 CLI 程序变得更长更复杂,创建存储在文件中的脚本并在现在或将来随时执行更有意义。
字符串比较运算符
字符串比较运算符能够比较字母数字字符字符串。这些运算符只有几个,如图 2 所示。
运算符 | 描述 |
---|---|
-n string | 如果字符串的长度为非零,则为真 |
string1 == string2 或 string1 = string2 | 如果字符串相等则为真;对于 POSIX 一致性,应将单个 = 与 test 命令一起使用。与 [[ 命令一起使用时,这将执行如上所述的模式匹配(复合命令)。 |
string1 != string2 | 如果字符串不相等则为真 |
string1 < string2 | 如果 string1 在词汇上排序在 string2 之前(指所有字母数字和特殊字符的特定于语言环境的排序序列),则为真 |
string1 > string2 | 如果 string1 在词汇上排序在 string2 之后,则为真 |
首先,看一下字符串长度。比较中 $MyVar 周围的引号必须存在,比较才能工作。(您应该仍然在 ~/testdir 中工作。)
$ MyVar="" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
$ MyVar="Random text" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
您也可以这样做
$ MyVar="Random text" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar contains data.
$ MyVar="" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar is zero length
有时您可能需要知道字符串的确切长度。这不是比较,但它与之相关。不幸的是,没有简单的方法来确定字符串的长度。有几种方法可以做到这一点,但我认为使用 expr(evaluate expression,求值表达式)命令是最容易的。阅读 expr 的手册页,了解有关它可以执行的操作的更多信息。请注意,您正在测试的字符串或变量周围需要引号。
$ MyVar="" ; expr length "$MyVar"
0
$ MyVar="How long is this?" ; expr length "$MyVar"
17
$ expr length "We can also find the length of a literal string as well as a variable."
70
关于比较运算符,我在脚本中进行了大量测试,以确定两个字符串是否相等(即相同)。我使用此比较运算符的非 POSIX 版本
$ 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
$ 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.
自己进行更多实验以尝试这些运算符。
数值比较运算符
数值运算符在两个数值参数之间进行比较。与其他运算符类一样,大多数运算符都易于理解。
运算符 | 描述 |
---|---|
arg1 -ne arg2 | 如果 arg1 不等于 arg2,则为真 |
arg1 -lt arg2 | 如果 arg1 小于 arg2,则为真 |
arg1 -le arg2 | 如果 arg1 小于或等于 arg2,则为真 |
arg1 -gt arg2 | 如果 arg1 大于 arg2,则为真 |
arg1 -ge arg2 | 如果 arg1 大于或等于 arg2,则为真 |
以下是一些简单的示例。第一个实例将变量 $X 设置为 1,然后测试 $X 是否等于 1。在第二个实例中,X 设置为 0,因此比较为假。
$ X=1 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X equals 1
$ X=0 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X does not equal 1
自己尝试更多实验。
其他运算符
这些其他运算符显示是否设置了 shell 选项或 shell 变量是否具有值,但它不会发现变量的值,而只是它是否具有值。
运算符 | 描述 |
---|---|
-v varname | 如果 shell 变量 varname 已设置(已分配值),则为真 |
-R varname | 如果 shell 变量 varname 已设置并且是名称引用,则为真 |
自己进行实验以尝试这些运算符。
扩展
Bash 支持多种类型的扩展和替换,这些扩展和替换可能非常有用。根据 Bash 手册页,Bash 有七种形式的扩展。本文研究了其中的五种:波浪号扩展、算术扩展、路径名扩展、大括号扩展和命令替换。
大括号扩展
大括号扩展是一种生成任意字符串的方法。此工具在下面用于创建大量文件,以便使用特殊模式字符进行实验。大括号扩展可用于生成任意字符串列表,并将它们插入到封闭静态字符串中的特定位置或静态字符串的任一端。这可能难以可视化,因此最好直接进行操作。
首先,这是大括号扩展的作用
$ echo {string1,string2,string3}
string1 string2 string3
好吧,这没什么帮助,不是吗?但是看看当您稍微不同地使用它时会发生什么
$ echo "Hello "{David,Seth,Jim,Chris,Don}.
Hello David. Hello Seth. Hello Jim. Hello Chris. Hello Don.
这看起来像有用的东西——它可以节省大量打字。现在试试这个
$ echo b{ed,olt,ar}s
beds bolts bars
我可以继续下去,但你明白我的意思了。
波浪号扩展
可以说,最常见的扩展是波浪号 (~) 扩展。当您在 cd ~/Documents 等命令中使用它时,Bash shell 会将其扩展为用户完整主目录的快捷方式。
使用这些 Bash 程序来观察波浪号扩展的效果
$ echo ~
/home/student
$ echo ~/Documents
/home/student/Documents
$ Var1=~/Documents ; echo $Var1 ; cd $Var1
/home/student/Documents
路径名扩展
路径名扩展是扩展文件通配符模式(使用字符 ? 和 *)为与模式匹配的目录的完整名称的别称。文件通配符是指特殊模式字符,这些字符在执行各种操作时可以非常灵活地匹配文件名、目录和其他字符串。这些特殊模式字符允许匹配字符串中指定位置的单个、多个或特定字符。
- ? — 匹配字符串中指定位置的任何单个字符
- * — 匹配字符串中指定位置的零个或多个任何字符
此扩展应用于匹配的目录名称。要查看其工作原理,请确保 testdir 是当前工作目录 (PWD),并从纯列表开始(我的主目录的内容将与您的不同)
$ ls
chapter6 cpuHog.dos dmesg1.txt Documents Music softlink1
chapter7 cpuHog.Linux dmesg2.txt Downloads Pictures Templates
testdir cpuHog.mac dmesg3.txt file005 Public testdir
cpuHog Desktop dmesg.txt link3 random.txt testdir1
现在列出以 Do、testdir/Documents 和 testdir/Downloads 开头的目录
Documents:
Directory01 file07 file15 test02 test10 test20 testfile13
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:
好吧,这并没有达到我想要的目的。它列出了以 Do 开头的目录的内容。要仅列出目录而不列出其内容,请使用 -d 选项。
$ ls -d Do*
Documents Downloads
在这两种情况下,Bash shell 都将 Do* 模式扩展为与该模式匹配的两个目录的名称。但是,如果也有与该模式匹配的文件呢?
$ touch Downtown ; ls -d Do*
Documents Downloads Downtown.txt
这也显示了该文件。因此,任何与该模式匹配的文件也会扩展为它们的完整名称。
命令替换
命令替换是一种扩展形式,它允许将一个命令的 STDOUT 数据流用作另一个命令的参数;例如,作为要在循环中处理的项目列表。Bash 手册页说:“命令替换允许命令的输出替换命令名称。”(准确,但有点晦涩。)
此替换有两种形式,`command` 和 $(command)。在使用反引号 (`) 的较旧形式中,在命令中使用反斜杠 (\) 会保留其字面意思。但是,当它在较新的括号形式中使用时,反斜杠会将其意义作为特殊字符。另请注意,括号形式仅使用单括号来打开和关闭命令语句。
我经常在命令行程序和脚本中使用此功能,其中一个命令的结果可以在循环中用作另一个命令的参数。
从一个非常简单的示例开始,该示例使用此扩展的两种形式(再次,确保 testdir 是 PWD)
$ echo "Todays date is `date`"
Todays date is Sun Apr 7 14:42:46 EDT 2019
$ echo "Todays date is $(date)"
Todays date is Sun Apr 7 14:42:59 EDT 2019
seq 实用程序的 -w 选项向生成的数字添加前导零,以便它们都具有相同的宽度,即无论值如何,数字位数都相同。这使得按数字顺序对它们进行排序更容易。
seq 实用程序用于生成数字序列
$ seq 5
1
2
3
4
5
$ echo `seq 5`
1 2 3 4 5
现在您可以做一些更有用的事情,例如创建大量空文件用于测试
$ for I in $(seq -w 5000) ; do touch file-$I ; done
在此用法中,语句 seq -w 5000 生成从 1 到 5,000 的数字列表。通过将命令替换用作 for 语句的一部分,数字列表被 for 语句用于生成文件名的数字部分。
算术扩展
Bash 可以执行整数数学运算,但这相当繁琐(您很快就会看到)。算术扩展的语法是 $((arithmetic-expression)),使用双括号来打开和关闭表达式。
算术扩展的工作方式类似于 shell 程序或脚本中的命令替换;从表达式计算出的值将替换表达式,以供 shell 进一步评估。
再次,从一些简单的东西开始
$ echo $((1+1))
2
$ Var1=5 ; Var2=7 ; Var3=$((Var1*Var2)) ; echo "Var 3 = $Var3"
Var 3 = 35
以下除法的结果为零,因为结果将是小于 1 的小数值
$ Var1=5 ; Var2=7 ; Var3=$((Var1/Var2)) ; echo "Var 3 = $Var3"
Var 3 = 0
这是一个简单的计算,我经常在脚本或 CLI 程序中执行,它告诉我 Linux 主机中我拥有的虚拟内存总量。free 命令不提供该数据
$ 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 扩展。
本系列的下一篇文章将探讨循环的使用,以执行各种类型的迭代操作。
留下评论