该文章根据 the unix workbench 中的 Bash Programming
进行汉化处理并作出自己的整理,并参考 Bash 脚本教程 和 BashPitfalls 相关内容进行补充修正。一是我对 Bash 的学习记录,二是对大家学习 Bash 有更好的帮助。如对该博文有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。本篇博文可能比较冗长,请耐心阅读和学习。
Bash 是 Unix 系统和 Linux 系统的一种 Shell(命令行环境),是目前绝大多数 Linux 发行版的默认 Shell。Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境。Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。其次,Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为脚本,这个也是本篇重点讲解的部分。
既然学的是 Bash ,那么必然是任何 Linux 发行版。至于用什么编辑器都可以。不过我个人建议使用VSCode + Bash Extension Pack
进行学习,因为它有纠错功能,并指出一些不合适的写法,我也会在本篇末也会介绍一些。我羽夏使用的就是我建议使用的工具,我也默认你使用的是它,如果用其他的自己酌情参考。下面开始进入正题:
好,在阳光明媚、微风和煦(bushi)的一天,我们信心满满的开始了 Bash 的学习旅程。熟练的打开 VSCode ,进入 Bash 工作区,然后新建了一个名字叫math.sh
的干净纯洁的文件,然后输入了以下内容:
#!/bin/bash # File: math.sh
注意,你不要复制和粘贴这些行到文件中,尽管你应该准确地输入我输入的的内容。写代码时你应该尽可能多地自己练习,因为写代码是一个实践性十分强的项目,不能眼高手低。这两行都以#
开头,在 Bash 编程语言中,在#
之后键入的任何内容都将被忽略(除非位于花括号之间,但这仅在非常特定的情况下)。#
允许你在代码中进行注释,以便以后的你理解你当下写的代码,也可以使其他人更快地知道你的程序是如何设计的。
但是,上面的内容的第一行代码具有特殊的含义,虽然它是注释。该行被称为 Shebang 行。脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行,而这一行以#!
字符开头,正如上面展示的。
#!
后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh
或/bin/bash
。
#!/bin/sh
或者
#!/bin/bash
#!
与脚本解释器之间有没有空格,都是可以的。如果 Bash 解释器不放在目录/bin
,脚本就无法执行了。为了保险,可以写成下面这样。
#!/usr/bin/env bash
上面命令使用env
命令,这个命令总是在/usr/bin
目录),返回 Bash 可执行文件的位置,从而避免了这个问题。
Shebang 行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。举例来说,脚本是script.sh
,有 Shebang 行的时候,可以直接调用执行。
wingsummer@wingsummer-PC ~ → ./script.sh
上面例子中,script.sh
是脚本文件名。脚本通常使用.sh
后缀名,不过这不是必需的。如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
wingsummer@wingsummer-PC ~ → /bin/sh ./script.sh
或者
wingsummer@wingsummer-PC ~ → bash ./script.sh
注意,“只要指定了 Shebang 行的脚本,可以直接执行”这句话有个前提条件,就是脚本需要有执行权限,否则这行也是没作用的。
Bash 编程语言可以完成非常基本的算法,现在你在 VSCode 打开了math.sh
这个文件,我们开始输入下面内容:
#!/usr/bin/env bash # File: math.sh expr 5 + 2 expr 5 - 2 expr 5 * 2 expr 5 / 2
保存,并在终端去执行它,你将会得到如下结果:
7 3 10 2
让我们分析一下刚才创建的 Bash 脚本中发生了什么。Bash 按照从文件的第一行到最后一行的顺序执行程序。expr
命令可用于计算 Bash 表达式。表达式只是一个有效的 Bash 代码字符串,在运行时会生成一个结果。您已经熟悉的加法(+)、减法(-)和乘法(*)的算术运算符的工作方式与您预期的一样。请注意:在进行乘法运算时,需要转义星号字符,否则 Bash 会认为您正在尝试创建正则表达式。由于5 / 2 = 2.5
,除法运算符(/)的工作方式与预期不同。Bash 进行整数除法,这意味着一个数除以另一个数的结果总是向下舍入到最接近的整数。让我们看一下命令行上的几个示例:
expr 1 / 3 expr 10 / 3 expr 40 / 21 expr 40 / 20
另一个你可能不熟悉的数值运算符是模运算符(%)。模运算符返回整数除法后的余数。在整数除法中,如果A / B = C
,A % B = D
,那么B * C + D = A
。让我们看看命令行上的一些示例:
expr 1 % 3 expr 10 % 3 expr 40 % 21 expr 40 % 20
然后是它的执行结果:
1 1 19 0
注意,当一个数完全可被另一个数整除时,模的结果为零。如果你想做更复杂的数学运算,比如分数和小数,那么我强烈建议将echo
和名为bc
的台式计算器程序结合起来。打开一个名为bigmath.sh
的新文件并输入以下内容:
#!/usr/bin/env bash # File: bigmath.sh echo "22 / 7" | bc -l echo "4.2 * 9.15" | bc -l echo "(6.5 / 0.5) + (6 * 2.2)" | bc -l
如下是计算结果:
3.14285714285714285714 38.430 26.20000000000000000000
为了在计算中使用十进制数,可以使用-l
标志将任何数学字符串传输到bc
。
#
后面写的任何东西都是注释,Bash 不会执行。expr
命令执行简单的算术运算。echo
将字符串表达式传输到bc
中,执行更复杂的数学运算。bc
的帮助手册。bc
交互控制台进行一些数运算。bc
。# 1: wingsummer@wingsummer-PC ~ → man bc # 2:略 # 3: wingsummer@wingsummer-PC ~ → echo "1+8" > test.txt wingsummer@wingsummer-PC ~ → bc test.txt bc 1.07.1 Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. 9
Bash 变量分成环境变量和自定义变量两类。环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。使用env
命令或printenv
命令,可以显示所有环境变量。创建变量的时候,变量名必须遵守下面的规则:
_
如果遵循这些规则,你就可以避免意外地覆盖存储在环境变量中的数据。
可以使用等号(=)将数据分配给变量。存储在变量中的数据可以是字符串或数字。现在让我们在命令行上创建一个变量:
myvar="hello world"
变量名在等号的左侧,存储在该变量中的数据在等号的右侧。请注意,等号两边都没有空格,分配变量时不允许使用空格:
myvar = "hello world" #错误!
为了打印变量(也称为变量值)中的数据,我们可以使用echo
。要检索变量的值,必须在变量名称前使用美元符号$
:
wingsummer@wingsummer-PC ~ → myvar="helloworld" wingsummer@wingsummer-PC ~ → echo $myvar helloworld
通过使用let
命令,可以使用算术运算符修改变量的值:
wingsummer@wingsummer-PC ~ → myvar=5 wingsummer@wingsummer-PC ~ → let newvar=$myvar wingsummer@wingsummer-PC ~ → echo $newvar 6
有时,您可能希望像在命令行上一样运行命令,并将该命令的结果存储在变量中。我们可以通过将命令用美元符号和括号$())
括起来来实现这一点。这种语法称为命令替换。执行该命令,然后替换为运行该命令所产生的字符串。例如,如果我们想在获取math.sh
文件的行数:
math_lines=$(cat math.sh | wc -l) echo $math_lines
带美元符号的变量名也可以在其他字符串中使用,以便将变量的值插入字符串:
wingsummer@wingsummer-PC ~ → myvar="world" wingsummer@wingsummer-PC ~ → echo "hello $myvar" hello world
在编写 Bash 脚本时,可以自由使用一些变量。让我们创建一个名为vars.sh
的新文件。使用下面的代码:
#!/usr/bin/env bash # File: vars.sh echo "Script arguments: $*" echo "First arg: $1. Second arg: $2." echo "Number of arguments: $#"
现在,让我们尝试以几种不同的方式运行脚本:
wingsummer@wingsummer-PC ~ → bash vars.sh Script arguments: First arg: . Second arg: . Number of arguments: 0
wingsummer@wingsummer-PC ~ → bash vars.sh red Script arguments: red First arg: red. Second arg: . Number of arguments: 1
wingsummer@wingsummer-PC ~ → bash vars.sh red blue Script arguments: red blue First arg: red. Second arg: blue. Number of arguments: 2
wingsummer@wingsummer-PC ~ → bash vars.sh red blue green Script arguments: red blue green First arg: red. Second arg: blue. Number of arguments: 3
你的脚本可以像命令行程序一样接受参数。脚本的第一个参数存储在$1
中,第二个参数存储在$2
中……但如果脚本的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。传递给脚本的所有参数的数组存储在$*
中,我们将在本章后面讨论如何处理数组。传递给脚本的参数总数存储在$#
中。既然知道如何将参数传递给脚本,我们就可以开始编写自己的命令行工具了!
下面我们继续扩展一下这个特殊的变量:
变量 | 含义 |
---|---|
$0 |
脚本文件名 |
$1 -$9 |
对应脚本的第一个参数到第九个参数 |
$# |
参数的总数 |
$* |
全部的参数,参数之间使用变量$IFS 值的第一个字符分隔,默认为空格,但是可以自定义 |
$@ |
全部的参数,参数之间使用空格分隔 |
$? |
上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败 |
$$ |
当前 Shell 的进程 ID |
$_ |
上一个命令的最后一个参数 |
$! |
最近一个后台执行的异步命令的进程 ID |
$- |
当前 Shell 的启动参数 |
上面的特殊变量我就不一一做示例验证了,感兴趣的话可以自己试试。
前面我们简单提过读取变量的值是前面加一个$
,下面我们继续讨论读取变量这个事情。
读取变量的时候,直接在变量名前加上$
就可以了,但变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况,如下所示:
wingsummer@wingsummer-PC ~ → myvar="hello" wingsummer@wingsummer-PC ~ → echo $myvar_world wingsummer@wingsummer-PC ~ → echo ${myvar}_world hello_world
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
wingsummer@wingsummer-PC ~ → myvar=USER wingsummer@wingsummer-PC ~ → echo ${!myvar} wingsummer
如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取。示例如下:
wingsummer@wingsummer-PC ~ → a="1 2 3" wingsummer@wingsummer-PC ~ → echo $a 1 2 3 wingsummer@wingsummer-PC ~ → echo "$a" 1 2 3
这个也将会在篇末继续讨论。
如果我使用了一个变量,我突然不想要了咋整。我们可以使用unset
命令:
wingsummer@wingsummer-PC ~ → echo $myvar USER wingsummer@wingsummer-PC ~ → unset myvar wingsummer@wingsummer-PC ~ → echo $myvar
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。有时我们有一种使用情况,我在一个 Shell 声明的变量,我想让它的子 Shell 也能用,我们可以将该变量进行导出:
NAME=foo export NAME
上面命令输出了变量NAME。变量的赋值和输出也可以在一个步骤中完成。
export NAME=value
上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME
。
对于参数,如果我想获取,如果没有返回默认值,这个我们通过 Shell 如何实现呢?如下是表格总结:
语法 | 含义 |
---|---|
${varname:-word} |
如果变量varname 存在且不为空,则返回它的值,否则返回word 。它的目的是返回一个默认值,比如${count:-0} 表示变量count 不存在时返回0 |
${varname:=word} |
如果变量varname 存在且不为空,则返回它的值,否则将它设为word ,并且返回word 。它的目的是设置变量的默认值,比如${count:=0} 表示变量count 不存在时返回0 ,且将count 设为0 |
${varname:+word} |
如果变量名存在且不为空,则返回word ,否则返回空值。它的目的是测试变量是否存在,比如${count:+1} 表示变量count 存在时返回1(表示true ),否则返回空值 |
${varname:?message} |
如果变量varname 存在且不为空,则返回它的值,否则打印出varname: message ,并中断脚本的执行。如果省略了message ,则输出默认的信息parameter null or not set. 。它的目的是防止变量未定义,比如${count:?"undefined!"} 表示变量count 未定义时就中断执行,抛出错误,返回给定的报错信息undefined! |
声明变量的方式不仅仅本篇介绍的,还有declare
和readonly
,由于感觉并不太常用就不介绍了,如果想详细了解请阅读 Bash 变量 相应的部分。
$
访问。declare
和readonly
声明变量。# 1 num1=$1 num2=$2 echo $(($num1+$num2)) # 2 str1=$1 str2=$2 echo "$str1" echo "$str2" echo "$str1$str2" #3 num1=$# num2=$1 echo $(($num1*$num2))
由于前面的内容仅仅讲解了简单的算术运算作为入门,下面开始进行介绍算术运算:
(())
可以进行整数的算术运算,如下所示。
wingsummer@wingsummer-PC ~ → echo $((5+5)) 10
上面的$
的作用就读取算术运算的结果的意思,我们还可以拆成下面的代码:
wingsummer@wingsummer-PC ~ → ((NUM = 5+5)) wingsummer@wingsummer-PC ~ → echo $NUM 10
它会自动忽略内部的空格,所以下面的写法都正确,得到同样的结果。
((2+2)) #写法1 (( 2+2 )) #写法2 (( 2 + 2 )) #写法3
它不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0,命令就算执行成功,至于是否成功我们可以使用环境变量$?
进行获取,这个会在之后的部分进行讲解。
(())
支持的算术运算符如下:
+
:加法-
:减法*
:乘法/
:除法(整除)%
:余数**
:指数++
:自增运算(前缀或后缀)--
:自减运算(前缀或后缀) 自增运算好自减运算的规则和C/C++
是一样的,作为前缀是先运算后返回值,作为后缀是先返回值后运算。我们可以作出如下测试:
wingsummer@wingsummer-PC ~ → i=0 wingsummer@wingsummer-PC ~ → echo $i 0 wingsummer@wingsummer-PC ~ → echo $((i++)) 0 wingsummer@wingsummer-PC ~ → echo $i 1 wingsummer@wingsummer-PC ~ → echo $((++i)) 2 wingsummer@wingsummer-PC ~ → echo $i 2
Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。
下面是一些例子:
wingsummer@wingsummer-PC ~ → echo $((0xff)) 255 wingsummer@wingsummer-PC ~ → echo $((2#11111111)) 255
$(())
支持以下的二进制位运算符。
<<
:位左移运算,把一个数字的所有位向左移动指定的位。>>
:位右移运算,把一个数字的所有位向右移动指定的位。&
:位的与运算,对两个数字的所有位执行一个AND
操作。|
:位的或运算,对两个数字的所有位执行一个OR
操作。~
:位的反运算,对一个数字的所有位取反。^
:位的异或运算,对两个数字的所有位执行一个XOR
操作。如下是一些例子:
wingsummer@wingsummer-PC ~ → echo $((16>>2)) 4 wingsummer@wingsummer-PC ~ → echo $((16<<2)) 64 wingsummer@wingsummer-PC ~ → echo $((17&3)) 1 wingsummer@wingsummer-PC ~ → echo $((17|3)) 19 wingsummer@wingsummer-PC ~ → echo $((17^3)) 18
$(())
支持以下的逻辑运算符:
<
:小于>
:大于<=
:小于或相等>=
:大于或相等==
:相等!=
:不相等&&
:逻辑与||
:逻辑或!
:逻辑否expr1?expr2:expr3
:三元条件运算符。若表达式expr1
的计算结果为真,则执行表达式expr2
,否则执行表达式expr3
。如下是几个例子:
wingsummer@wingsummer-PC ~ → echo $((3 > 2)) 1 wingsummer@wingsummer-PC ~ → echo $(( (3 > 2) || (4 <= 1) )) 1 wingsummer@wingsummer-PC ~ → a=0 wingsummer@wingsummer-PC ~ → echo $((a<1 ? 1 : 0)) 1 wingsummer@wingsummer-PC ~ → echo $((a>1 ? 1 : 0)) 0
算术表达式$(())
可以执行赋值运算,先看一个例子:
wingsummer@wingsummer-PC ~ → echo $((a=1)) 1 wingsummer@wingsummer-PC ~ → echo $a 1
上面例子中,a=1
对变量a
进行赋值。这个式子本身也是一个表达式,返回值就是等号右边的值。
$())
支持的赋值运算符,有以下这些:
parameter = value
:简单赋值parameter += value
:等价于parameter = parameter + value
parameter -= value
:等价于parameter = parameter – value
parameter *= value
:等价于parameter = parameter * value
parameter /= value
:等价于parameter = parameter / value
parameter %= value
:等价于parameter = parameter % value
parameter <<= value
:等价于parameter = parameter << value
parameter >>= value
:等价于parameter = parameter >> value
parameter &= value
:等价于parameter = parameter & value
parameter |= value
:等价于parameter = parameter | value
parameter ^= value
:等价于parameter = parameter ^ value
下面是一个例子:
wingsummer@wingsummer-PC ~ → foo=5 wingsummer@wingsummer-PC ~ → echo $((foo*=2)) 10
如果在表达式内部赋值,可以放在圆括号中,否则会报错。
wingsummer@wingsummer-PC ~ → echo $(( a<1 ? (a+=1) : (a-=1) ))
逗号,
在$(())
内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值,例子如下:
wingsummer@wingsummer-PC ~ → echo $((foo = 1 + 2, 3 * 4)) 12 wingsummer@wingsummer-PC ~ → echo $foo 3
上述命令逗号前后两个表达式都会执行,然后返回后一个表达式的值12
。
如果你正在为自己或其他人制作Bash程序,那么获得用户输入的一种方法就是指定用户提供给您的程序的参数。您还可以通过使用read
命令暂时停止程序的执行,要求用户在命令行中键入字符串。让我们编写一个小脚本,从中可以了解read
命令的工作原理:
#!/usr/bin/env bash # File: letsread.sh echo "Type in a string and then press Enter:" read response echo "You entered: $response"
然后运行该脚本:
Type in a string and then press Enter: |
上面的|
表示光标的位置,我们输入Hello!
,然后回车:
Type in a string and then press Enter: Hello! You entered: Hello!
read
命令提示用户键入字符串,用户提供的字符串存储在脚本中指定给read
命令的变量中,下面我们来看一下它的高级用法,首先得了解它的使用和参数,read命令的格式如下。
read [-options] [variable...]
具体参数总结如下:
参数 | 含义 |
---|---|
-t | 设置超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行 |
-p | 指定用户输入的提示信息 |
-a | 把用户的输入赋值给一个数组,从零号位置开始 |
-n | 指定只读取若干个字符作为变量值,而不是整行读取 |
-e | 允许用户输入的时候,使用readline 库提供的快捷键,比如自动补全。 |
-r | 不把用户输入的反斜杠字符解释为转义字符 |
-s | 使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息 |
当然read
的参数并不仅仅这些,剩下的就不太常用了,具体例子可以自己写进行测试,这里由于篇幅就不进行了。
read
存储用户在变量中提供的字符串。read -r -p "请输入形容词:" adj read -r -p "请输入名词:" n read -r -p "请输入动词:" v echo "$v $adj $v"
在编写计算机程序时,您的程序能够根据参数、文件和环境变量等输入做出决策通常很有用。Bash 提供了创建类似于数学方程的逻辑表达式的机制。可以对这些逻辑表达式求值,直到它们为真或假。事实上,true
和false
都是简单的 Bash 命令。现在我们测试一下:
true false
貌似看起来他们差不多。为了了解它们是如何工作的,我们需要稍微了解一下 Unix 的特性。每当在命令行上执行程序时,通常会发生以下两种情况之一:要么命令执行成功,要么出现错误。就错误而言,程序可能会出现很多错误,Unix 可以根据发生的错误类型采取不同的操作。例如,如果我输入了终端中不存在的命令名,那么我将看到一个错误:
this_command_does_not_exist
由于该命令不存在,它会创建一种特定类型的错误,该错误由程序的退出状态指示。程序的退出状态是一个整数,表示程序是否成功执行或是否发生错误。上次程序运行的退出状态存储在$?
中。我们可以通过echo
查看最后一个程序的退出状态:
echo $?
这个特定的退出状态向 Shell 发出指示,它应该向控制台打印一条错误消息。成功运行的程序的退出状态是什么?我们来看看:
echo I will succeed. echo $?
它的输出如下:
I will succeed. 0
因此,成功程序的退出状态为0
。现在我们来看一下true
和false
的退出状态:
wingsummer@wingsummer-PC ~ → true wingsummer@wingsummer-PC ~ → echo $? 0 wingsummer@wingsummer-PC ~ → false wingsummer@wingsummer-PC ~ → echo $? 1
如您所见,true
的退出状态为0
,false
的退出状态为1
。由于这些程序没有做很多其他事情,所以可以将true
定义为始终具有0
退出状态的程序,将false
定义为始终具有1
退出状态的程序。
在讨论逻辑运算符时,了解这些程序的退出状态很重要:AND
运算符&&
和OR
运算符||
。AND
和OR
运算符可用于在命令行上有条件地执行程序。当一个程序的执行取决于另一个程序的退出状态时,就会发生条件执行。例如,对于AND
运算符,只有当&&
左侧的程序的退出状态为0
时,才会执行&&
右侧的程序。让我们来看一些小例子:
true && echo "Program 1 was executed." false && echo "Program 2 was executed."
由于false
的退出状态为1
,因此echo "Program 2 was executed."
不会被执行,因此不会为该命令向控制台打印任何内容。可以将多个和运算符链接在一起,如下所示:
false && true && echo Hello echo 1 && false && echo 3 echo Athos && echo Porthos && echo Aramis
在由AND
运算符连接在一起的一系列程序中,程序右侧任何非零退出状态的程序都不会执行。OR
运算符||
遵循一组类似的原则。||
右侧的命令只有在左侧的命令失败,因此退出状态不是0
时才会执行。让我们看看它是如何工作的:
true || echo "Program 1 was executed." false || echo "Program 2 was executed."
结果只执行了echo "Program 2 was executed."
,因为false
的退出状态为非零。你可以组合多个OR
运算符,以便只执行退出状态为0
的第一个程序:
false || echo 1 || echo 2 echo 3 || false || echo 4 echo Athos || echo Porthos || echo Aramis
可以在命令中组合AND
和OR
运算符,这些命令从左到右求值:
echo Athos || echo Porthos && echo Aramis echo Gaspar && echo Balthasar || echo Melchior
通过组合AND
和OR
运算符,可以精确控制执行某些命令的条件。
使 Bash 脚本能够做出决策非常有用。条件执行允许您根据某些程序是成功还是失败来控制执行这些程序的情况,但您也可以构造条件表达式,这些表达式是等价于true
或false
的逻辑语句。条件表达式要么比较两个值,要么询问关于一个值的问题。条件表达式总是在双中括号[[]]
之间,它们要么使用逻辑标志,要么使用逻辑运算符。例如,有几个逻辑标志可用于比较两个整数。如果我们想知道一个整数是否大于另一个整数,我们可以使用-gt
,即大于。在命令行中输入以下简单条件表达式:
[[ 4 -gt 3 ]]
上面的逻辑表达式是这样的:4
是否大于3
?没有结果被打印到控制台,所以让我们检查该表达式的退出状态:
wingsummer@wingsummer-PC ~ → echo $? 0
该程序的退出状态似乎为0
,与true
的退出状态相同。这个条件表达式表示[[ 4 -gt 3 ]]
等价于true
,我们当然知道这在逻辑上是一致的,4
实际上是大于3
的。让我们看看如果我们翻转表达式会发生什么,我们问3
是否大于4
:
[[ 3 -gt 4 ]]
同样,控制台上没有打印任何内容,因此我们将查看退出状态:
wingsummer@wingsummer-PC ~ → echo $? 1
显然3
不大于4
,所以这个错误的逻辑表达式导致退出状态为1
,这与false
的退出状态相同。因为它们具有相同的退出状态[[ 3 -gt 4 ]]
和false
本质上是等价的。为了快速测试条件表达式的逻辑值,我们可以使用AND
和OR
运算符,以便表达式在为真时打印t
,在为假时打印f
:
[[ 4 -gt 3 ]] && echo t || echo f [[ 3 -gt 4 ]] && echo t || echo f
这是一个小技巧,可以用来快速查看逻辑表达式的结果值。这些二进制逻辑表达式比较两个值,但也有一元逻辑表达式只查看一个值。例如,可以使用-e
逻辑标志测试文件是否存在:
wingsummer@wingsummer-PC ~ → cd ~ # 假设我们的 math.sh 在该目录下 wingsummer@wingsummer-PC ~ → [[ -e math.sh ]] && echo t || echo f t
大多数情况下,在编写 Bash 脚本时,您不会比较两个原始值,也不会试图找出关于一个原始值的信息,而是希望创建一个关于变量中包含的值的逻辑语句。变量的行为就像逻辑表达式中的原始值。让我们看几个例子:
number=7 [[ $number -gt 3 ]] && echo t || echo f [[ $number -gt 10 ]] && echo t || echo f [[ -e $number ]] && echo t || echo f
7
大于3
,尽管它不大于10
,而且这个目录中没有名为7
的文件。还有其他几种不同的逻辑标志,如下表格所示:
标志 | 含义 | 示例 |
---|---|---|
-gt | 大于 | [[ $planets -gt 8 ]] |
-ge | 大于等于 | [[ $votes -ge 270 ]] |
-eq | 等于 | [[ $fingers -eq 10 ]] |
-ne | 不等于 | [[ $pages -ne 0 ]] |
-le | 小于等于 | [[ $candles -le 9 ]] |
-lt | 小于 | [[ $wives -lt 2 ]] |
-e | 文件是否存在 | [[ -e $taxes_2016 ]] |
-d | 文件夹是否存在 | [[ -d $photos ]] |
-z | 字符串长度是否为零 | [[ -z $name ]] |
-n | 字符串长度是否非零 | [[ -n $name ]] |
在继续下一节之前,请尝试在命令行中使用这些标志。除了逻辑标志,还有逻辑运算符。最有用的逻辑运算符之一是正则表达式匹配运算符=~
。正则表达式匹配运算符将字符串与正则表达式进行比较,如果字符串与正则表达式匹配,则表达式等价于true
,否则等价于false
。让我们用两种不同的方法测试这个操作符:
[[ rhythms =~ [aeiou] ]] && echo t || echo f my_name=sean [[ $my_name =~ ^s.+n$ ]] && echo t || echo f
还有NOT
运算符!
,它反转任何条件表达式的值。NOT
运算符将真表达式转换为假表达式,反之亦然。让我们来看几个使用NOT
运算符的示例:
[[ 7 -gt 2 ]] && echo t || echo f [[ ! 7 -gt 2 ]] && echo t || echo f [[ 6 -ne 3 ]] && echo t || echo f [[ ! 6 -ne 3 ]] && echo t || echo f
下面是一些有用的逻辑运算符的列表,以供参考:
标志 | 含义 | 示例 |
---|---|---|
=~ | 匹配正则表达式 | [[ $consonants =~ [aeiou] ]] |
= | 判断字符串是否相同 | [[ $password = "pegasus" ]] |
!= | 判断字符串是否不同 | [[ $fruit != "banana" ]] |
! | 取反 | [[ ! "apple" =~ ^b ]] |
条件表达式非常强大,因为可以使用它们来控制正在编写的 Bash 程序的执行方式。Bash 编程中的一个基本构造是IF
语句。IF
语句中编写的代码只有在特定条件为true
时才会执行,否则代码将被跳过。让我们用IF
语句编写一个小程序:
#!/usr/bin/env bash # File: simpleif.sh echo "Start program" if [[ $1 -eq 4 ]] then echo "You entered $1" fi echo "End program"
首先,这个程序将打印Start program
,然后IF
语句将检查条件表达式[[ $1 -eq 4 ]]
是否为真。只有将4
作为脚本的第一个参数时,才是真。如果条件表达式为true
,那么它将执行在then
和fi
之间的代码,否则它将跳过该代码。最后,程序将打印End program
。让我们尝试以几种不同的方式运行这个 Bash 程序:
wingsummer@wingsummer-PC ~ → bash simpleif.sh Start program End program wingsummer@wingsummer-PC ~ → bash simpleif.sh 77 Start program End program wingsummer@wingsummer-PC ~ → bash simpleif.sh 4 Start program You entered 4 End program
直到最后,由于该脚本的第一个参数是4
,4
等于4
,因此执行了IF
语句中的代码。可以将IF
语句与ELSE
语句配对。ELSE
语句仅在IF
语句计算的条件表达式为false
时运行。让我们创建一个使用ELSE
语句的简单程序:
#!/usr/bin/env bash # File: simpleifelse.sh echo "Start program" if [[ $1 -eq 4 ]] then echo "Thanks for entering $1" else echo "You entered: $1, not what I was looking for." fi echo "End program"
我们继续相同的操作:
wingsummer@wingsummer-PC ~ → bash simpleifelse.sh 4 Start program Thanks for entering 4 End program wingsummer@wingsummer-PC ~ → bash simpleifelse.sh 3 Start program You entered: 3, not what I was looking for. End program
当第一个参数是4
时,条件表达式[[ $1 -eq 4]]
为true
,因此IF
语句中的代码运行,而ELSE
语句中的代码未运行。当参数改为3
时,条件表达式[[ $1 -eq 4]]
为为false
,因此ELSE
语句中的代码运行,IF
语句中的代码未运行。
在IF
和ELSE
语句之间,还可以使用ELIF
语句。这些语句的行为类似于IF
语句,除非它们仅在前面的IF
和ELIF
语句都计算值为假,条件表达式时才被计算。让我们使用ELIF
创建一个简短的程序:
#!/usr/bin/env bash # File: simpleelif.sh if [[ $1 -eq 4 ]] then echo "$1 is my favorite number" elif [[ $1 -gt 3 ]] then echo "$1 is a great number" else echo "You entered: $1, not what I was looking for." fi
我们如法炮制:
wingsummer@wingsummer-PC ~ → bash simpleelif.sh 4 4 is my favorite number wingsummer@wingsummer-PC ~ → bash simpleelif.sh 5 5 is a great number wingsummer@wingsummer-PC ~ → bash simpleelif.sh 2 You entered: 2, not what I was looking for.
由于篇幅,我这里就不细说了。我们还可以组合条件执行、条件表达式和IF/ELIF/ELSE
语句,条件执行运算符&&
和||
可用于IF
或ELIF
语句。让我们来看一个在IF
语句中使用这些运算符的示例:
#!/usr/bin/env bash # File: condexif.sh if [[ $1 -gt 3 ]] && [[ $1 -lt 7 ]] then echo "$1 is between 3 and 7" elif [[ $1 =~ "Jeff" ]] || [[ $1 =~ "Roger" ]] || [[ $1 =~ "Brian" ]] then echo "$1 works in the Data Science Lab" else echo "You entered: $1, not what I was looking for." fi
现在,让我们用几个不同的参数来测试这个脚本:
wingsummer@wingsummer-PC ~ → bash condexif.sh 2 You entered: 2, not what I was looking for. wingsummer@wingsummer-PC ~ → bash condexif.sh 4 4 is between 3 and 7 wingsummer@wingsummer-PC ~ → bash condexif.sh 6 6 is between 3 and 7 wingsummer@wingsummer-PC ~ → bash condexif.sh Jeff Jeff works in the Data Science Lab wingsummer@wingsummer-PC ~ → bash condexif.sh Brian Brian works in the Data Science Lab wingsummer@wingsummer-PC ~ → bash condexif.sh Sean You entered: Sean, not what I was looking for.
条件执行操作符的工作方式与它们在命令行上的工作方式相同。如果整个条件表达式的计算结果等效于true
,则执行If
语句中的代码,否则将跳过它。
最后,我们应该注意,IF/ELIF/ELSE
语句可以嵌套在其他IF
语句中。下面是一个带有嵌套语句的程序的小示例:
#!/usr/bin/env bash # File: nested.sh if [[ $1 -gt 3 ]] && [[ $1 -lt 7 ]] then if [[ $1 -eq 4 ]] then echo "four" elif [[ $1 -eq 5 ]] then echo "five" else echo "six" fi else echo "You entered: $1, not what I was looking for." fi
这里就不测试了,通过IF
语句,我们可以实现比较强大的脚本。
true
的退出状态为0
,false
的退出状态为1
。&&
和 OR ||
,您可以使用它们根据退出状态控制执行相应的命令。[[]]
内。如果包含true
断言,则退出状态为0
;如果包含false
断言,则退出状态为1
。IF
语句计算条件表达式。如果表达式为true
,则执行If
语句中的代码,否则将跳过它。ELIF
和ELSE
语句也有助于控制 Bash 程序的流,IF
语句可以嵌套在其他IF
语句中。大写开头
。偶数
;如果是奇数,则打印奇数
。今天是星期五
。(提示:看一下 date
程序帮助)。# 1 echo "请输入英文单词" read -r INPUT if [[ $INPUT =~ ^[A-Z] ]] then echo "大写开头" fi # 2 num=$1 if ((num % 2 == 0)); then echo "偶数" else echo "奇数" fi #3 num1=$1 num2=$2 if [[ $num1 =~ [[:digit:]] ]] && [[ $num2 =~ [[:digit:]] ]] ;then echo $((num1+num2)) else echo "$1 $2" fi # 4 day=$(date +%w) if ((day==5));then echo "今天是星期五" fi
上面都是比较简单的编写带有条件判断语句脚本的基础知识,当然不能仅仅会IF
语句,下面我们对条件判断进行拓展知识:
if
是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下:
if commands; then commands [elif commands; then commands...] [else commands] fi
if
和then
写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。除了多行的写法,if结构也可以写成单行。
if true; then echo 'hello world'; fi if false; then echo "It's true."; fi
if
结构的判断条件,一般使用test
命令,它有三种形式。
# 写法一 test expression # 写法二 [ expression ] # 写法三 [[ expression ]]
上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。
上面的expression
是一个表达式。这个表达式为真,test
命令执行成功,返回值为0
;表达式为假,test
命令执行失败,返回值为1
。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。下面把test
命令的三种形式,用在if
结构中,判断一个文件是否存在:
# 写法一 if test -e /tmp/foo.txt ; then echo "Found foo.txt" fi # 写法二 if [ -e /tmp/foo.txt ] ; then echo "Found foo.txt" fi # 写法三 if [[ -e /tmp/foo.txt ]] ; then echo "Found foo.txt" fi
if
关键字后面,跟的是一个命令。这个命令可以是test
命令,也可以是其他命令。命令的返回值为0
表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。常用的判断表达式有下面这些:
以下表达式用来判断文件状态:
[ -a file ]
:如果file
存在,则为true
。[ -b file ]
:如果file
存在并且是一个块(设备)文件,则为true
。[ -c file ]
:如果file
存在并且是一个字符(设备)文件,则为true
。[ -d file ]
:如果file
存在并且是一个目录,则为true
。[ -e file ]
:如果file
存在,则为true
。[ -f file ]
:如果file
存在并且是一个普通文件,则为true
。[ -g file ]
:如果file
存在并且设置了组ID
,则为true
。[ -G file ]
:如果file
存在并且属于有效的组ID
,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -k file ]
:如果 file 存在并且设置了它的“sticky bit”,则为true。[ -L file ]
:如果file
存在并且是一个符号链接,则为true
。[ -N file ]
:如果file
存在并且自上次读取后已被修改,则为true
。[ -O file ]
:如果file
存在并且属于有效的用户ID
,则为true
。[ -p file ]
:如果file
存在并且是一个命名管道,则为true
。[ -r file ]
:如果file
存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果file
存在且其长度大于零,则为true
。[ -S file ]
:如果file
存在且是一个网络socket
,则为true
。[ -t fd ]
:如果fd
是一个文件描述符,并且重定向到终端,则为true
。这可以用来判断是否重定向了标准输入/输出/错误。[ -u file ]
:如果file
存在并且设置了setuid
位,则为true
。[ -w file ]
:如果file
存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果file
存在并且可执行(有效用户有执行/搜索权限),则为true
。[ file1 -nt file2 ]
:如果FILE1
比FILE2
的更新时间最近,或者FILE1
存在而FILE2
不存在,则为true
。[ file1 -ot file2 ]
:如果FILE1
比FILE2
的更新时间更旧,或者FILE2
存在而FILE1
不存在,则为true
。[ FILE1 -ef FILE2 ]
:如果FILE1
和FILE2
引用相同的设备和inode
编号,则为true
。下面是一个比较完整的示例:
#!/bin/bash FILE=~/.bashrc if [ -e "$FILE" ]; then if [ -f "$FILE" ]; then echo "$FILE is a regular file." fi if [ -d "$FILE" ]; then echo "$FILE is a directory." fi if [ -r "$FILE" ]; then echo "$FILE is readable." fi if [ -w "$FILE" ]; then echo "$FILE is writable." fi if [ -x "$FILE" ]; then echo "$FILE is executable/searchable." fi else echo "$FILE does not exist" exit 1 fi
上面代码中,$FILE
要放在双引号之中,这样可以防止变量$FILE
为空,从而出错。因为$FILE
如果为空,这时[ -e $FILE ]
就变成[ -e ]
,这会被判断为真。而$FILE
放在双引号之中,[ -e "$FILE" ]
就变成[ -e "" ]
,这会被判断为假。
以下表达式用来判断字符串:
[ string ]
:如果string
不为空(长度大于0),则判断为真。
[ -n string ]
:如果字符串string
的长度大于零,则判断为真。
[ -z string ]
:如果字符串string
的长度为零,则判断为真。
[ string1 = string2 ]
:如果string1
和string2
相同,则判断为真。
[ string1 == string2 ]
等同于[ string1 = string2 ]
。
[ string1 != string2 ]
:如果string1
和string2
不相同,则判断为真。
[ string1 '>' string2 ]
:如果按照字典顺序string1
排列在string2
之后,则判断为真。
[ string1 '<' string2 ]
:如果按照字典顺序string1
排列在string2
之前,则判断为真。
注意:test
命令内部的>
和<
,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。下面是一个示例。
#!/bin/bash ANSWER=maybe if [ -z "$ANSWER" ]; then echo "There is no answer." >&2 exit 1 fi if [ "$ANSWER" = "yes" ]; then echo "The answer is YES." elif [ "$ANSWER" = "no" ]; then echo "The answer is NO." elif [ "$ANSWER" = "maybe" ]; then echo "The answer is MAYBE." else echo "The answer is UNKNOWN." fi
上面代码中,首先确定$ANSWER
字符串是否为空。如果为空,就终止脚本,并把退出状态设为1
。注意,这里的echo
命令把错误信息There is no answer.
重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER
字符串不为空,就判断它的值是否等于yes
、no
或者maybe
。
注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ]
,否则变量替换成字符串以后,test
命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ]
,这时会判断为真。如果放在双引号之中,[ -n "" ]
就判断为假。
下面的表达式用于判断整数:
[ integer1 -eq integer2 ]
:如果integer1
等于integer2
,则为true
。
[ integer1 -ne integer2 ]
:如果integer1
不等于integer2
,则为true
。
[ integer1 -le integer2 ]
:如果integer1
小于或等于integer2
,则为true
。
[ integer1 -lt integer2 ]
:如果integer1
小于integer2
,则为true
。
[ integer1 -ge integer2 ]
:如果integer1
大于或等于integer2
,则为true
。
[ integer1 -gt integer2 ]
:如果integer1
大于integer2
,则为true
。
下面是一个用法的例子:
#!/bin/bash INT=-5 if [ -z "$INT" ]; then echo "INT is empty." >&2 exit 1 fi if [ $INT -eq 0 ]; then echo "INT is zero." else if [ $INT -lt 0 ]; then echo "INT is negative." else echo "INT is positive." fi if [ $((INT % 2)) -eq 0 ]; then echo "INT is even." else echo "INT is odd." fi fi
上面例子中,先判断变量$INT
是否为空,然后判断是否为0
,接着判断正负,最后通过求余数判断奇偶。
Bash 还提供了(())
作为算术条件,进行算术运算的判断:
if ((3 > 2)); then echo "true" fi
上面代码执行后,会打印出true
。注意,算术判断不需要使用test
命令,而是直接使用(())
结构。这个结构的返回值,决定了判断的真假。如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
wingsummer@wingsummer-PC ~ → if ((1)); then echo "It is true."; fi It is true. wingsummer@wingsummer-PC ~ → if ((0)); then echo "It is true."; else echo "it is false."; fi It is false.
上面例子中,((1))
表示判断成立,((0))
表示判断不成立。算术条件(())
也可以用于变量赋值:
wingsummer@wingsummer-PC ~ → if (( foo = 5 ));then echo "foo is $foo"; fi foo is 5
上面例子中,(( foo = 5 ))
完成了两件事情。首先把5
赋值给变量foo
,然后根据返回值5
,判断条件为真。注意,赋值语句返回等号右边的值,如果返回的是0,则判断为假。
wingsummer@wingsummer-PC ~ → if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi It is false.
下面是用算术条件改写的数值判断脚本:
#!/bin/bash INT=-5 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then if ((INT == 0)); then echo "INT is zero." else if ((INT < 0)); then echo "INT is negative." else echo "INT is positive." fi if (( ((INT % 2)) == 0 )); then echo "INT is even." else echo "INT is odd." fi fi else echo "INT is not an integer." >&2 exit 1 fi
只要是算术表达式,都能用于(())
语法。
case
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下:
case expression in pattern ) commands ;; pattern ) commands ;; ... esac
上面代码中,expression
是一个表达式,pattern
是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号;;
结尾。
#!/bin/bash echo -n "输入一个1到3之间的数字(包含两端)> " read character case $character in 1 ) echo 1 ;; 2 ) echo 2 ;; 3 ) echo 3 ;; * ) echo 输入不符合要求 esac
上面例子中,最后一条匹配语句的模式是*
,这个通配符可以匹配其他字符和没有输入字符的情况,类似if
的else
部分。下面是另一个例子:
#!/bin/bash OS=$(uname -s) case "$OS" in FreeBSD) echo "This is FreeBSD" ;; Darwin) echo "This is Mac OSX" ;; AIX) echo "This is AIX" ;; Minix) echo "This is Minix" ;; Linux) echo "This is Linux" ;; *) echo "Failed to identify this OS" ;; esac
上面的例子判断当前是什么操作系统。case
的匹配模式还可以使用各种通配符,下面是一些例子:
a )
:匹配a
。a|b )
:匹配a
或b
。[[:alpha:]] )
:匹配单个字母。??? )
:匹配3个字符的单词。*.txt )
:匹配.txt
结尾。* )
:匹配任意输入,通过作为case
结构的最后一个模式。然后我们看一下示例代码:
#!/bin/bash echo -n "输入一个字母或数字 > " read character case $character in [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character" ;; [0-9] ) echo "输入了数字 $character" ;; * ) echo "输入不符合要求" esac
上面例子中,使用通配符[[:lower:]] | [[:upper:]]
匹配字母,[0-9]
匹配数字。Bash 4.0
之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0
之后,允许匹配多个条件,这时可以用;;&
终止每个条件块:
#!/bin/bash # test.sh read -n 1 -p "Type a character > " echo case $REPLY in [[:upper:]]) echo "'$REPLY' is upper case." ;;& [[:lower:]]) echo "'$REPLY' is lower case." ;;& [[:alpha:]]) echo "'$REPLY' is alphabetic." ;;& [[:digit:]]) echo "'$REPLY' is a digit." ;;& [[:graph:]]) echo "'$REPLY' is a visible character." ;;& [[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;& [[:space:]]) echo "'$REPLY' is a whitespace character." ;;& [[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;& esac
执行上面的脚本,会得到下面的结果。
wingsummer@wingsummer-PC ~ → test.sh Type a character > a 'a' is lower case. 'a' is alphabetic. 'a' is a visible character. 'a' is a hexadecimal digit.
可以看到条件语句结尾添加了;;&
以后,在匹配一个条件之后,并没有退出case
结构,而是继续判断下一个条件。
由于本篇篇幅原因,暂时介绍这些,剩下的重要的知识点将会在下一篇继续。