Bash 3 流程控制
1 条件判断
1.1 值判断 case
case 结构用于多值判断,可以为每个值指定对应的命令。它的语法如下:
case expression in
pattern)
commands ;;
pattern)
commands ;;
# ...
# *)
# commands ;;
esac
expression
是一个表达式, pattern
是表达式的值或者一个匹配模式。多条语句可匹配多个值,每条以 ;;
结尾。最后的 *)
是一个匹配模式,这个通配符可以匹配其他字符和没有输入字符的情况。
给一个例子:
#!/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 的匹配模式可以使用各种通配符,可以参考 Bash 学习笔记 7 模式拓展 一节。这里给一些例子:
a)
: 匹配a。a|b)
:匹配 a 或 b。[[:alpha:]])
: 匹配单个字母。???)
:匹配 3 个字符的单词。*.txt)
: 匹配 .txt 以结尾的字符。*)
: 匹配任意输入,通过作为 case 结构的最后一个模式。
#!/bin/bash
echo -n "Input a alphabetic or number >"
read character
case $character in
[[:lower:]] | [[:upper:]])
echo "输入了字母 $character" ;;
[0-9])
echo "输入了数字 $character" ;;
*)
echo "输入不符合要求" ;;
esac
Bash 4.0 之前,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
执行上面的脚本,会得到下面的结果。
$ test.sh Type a character > a 'a' is lower case. 'a' is alphabetic. 'a' is a visible character. 'a' is a hexadecimal digit.
1.2 复杂判断 if
if 用于判断最常见,写法为:
if commands; then commands [elif commands; then commands...] [else commands] fi
除了多行的写法,if结构也可以写成单行。
if true; then echo 'hello world'; fi
if 结构的判断条件,一般使用 test 命令,有三种形式:
# 写法一
test expression
# 写法二, 注意要有空格
[ expression ]
# 写法三, 支持正则判断, 注意要有空格
[[ expression ]]
1.3 判断表达式
1.3.1 文件判断
以下表达式用来判断文件状态:
[ -a file ]
: 如果 file 存在,为 true。[ -e file ]
: 如果 file 存在,为 true。[ -d file ]
: 如果 file 存在且是一个目录,为 true。[ -f file ]
: 如果 file 存在且是一个普通文件,为 true。[ -s file ]
: 如果 file 存在且其长度大于零,为 true。[ -b file ]
: 如果 file 存在且是一个块 (设备) 文件,为 true。[ -c file ]
: 如果 file 存在且是一个字符 (设备) 文件,为 true。[ -p file ]
: 如果 file 存在且是一个命名管道,为 true。[ -S file ]
: 如果 file 存在且是一个网络 socket,为 true。[ -h file ]
: 如果 file 存在且是符号链接,为 true。[ -L file ]
: 如果 file 存在且是符号链接,为 true。[ -O file ]
: 如果 file 存在且属于有效的用户 ID,为 true。[ -k file ]
: 如果 file 存在且设置了它的 sticky bit,为 true。[ -u file ]
: 如果 file 存在且设置了 setuid 位,为 true。[ -g file ]
: 如果 file 存在且设置了组 ID,为 true。[ -G file ]
: 如果 file 存在且属于有效的组 ID,为 true。[ -N file ]
: 如果 file 存在且自上次读取后已被修改,为 true。[ -r file ]
: 如果 file 存在且可读 (当前用户有可读权限),为 true。[ -w file ]
: 如果 file 存在并且可写 (当前用户拥有可写权限),为 true。[ -x file ]
: 如果 file 存在并且可执行 (有效用户有执行 / 搜索权限),为 true。[ -t fd ]
: 如果 fd 是文件描述符,并且重定向到终端,为 true。可判断标准输入 / 输出 / 错误是否被重定向。[ file1 -nt file2 ]
: 如果 file1 比 file2 的更新时间最近,或者 file1 存在而 file2 不存在,为 true。[ file1 -ot file2 ]
: 如果 file1 比 file2 的更新时间更旧,或者 file2 存在而 file1 不存在,为 true。[ file1 -ef file2 ]
: 如果 file1 和 file2 引用相同的设备和 inode 编号,为 true。
1.3.2 字符串判断
以下表达式用来判断字符串。
[ string ]
: 字符串不为空 (长度大于0) ,为 true。[ -n string ]
: 字符串长度大于 0,为 true。[ -z string ]
: 字符串长度为 0,为 true。[ string1 = string2 ]
: 两字符串相同,为 true。[ string1 == string2 ]
: 等同于[ string1 = string2 ]
。[ string1 != string2 ]
: 两字符串不相同,为 true。[ string1 '>' string2 ]
: 如果按照字典顺序 string1 排列在 string2 之后,为 true。[ string1 '<' string2 ]
: 如果按照字典顺序 string1 排列在 string2 之前,为 true。
注意, test
命令内部的 >
和 <
必须用引号引起来,或者是用反斜杠转译。否则按重定向操作符处理。
注意,字符串变量要放在双引号中,比如 [ -n "$COUNT" ]
,否则 test 命令可能会报错,提示参数过多 (因为原始字符串变量中含空格)。
另外,如果字符串变量不放在双引号之中,在变量为空时,命令会变成 [ -n ]
,表达式判断为 true。如果放在双引号之中, [ -n "" ]
表达式判断 false。
1.3.3 整数判断
下面的表达式用于判断整数。
[ 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。
1.3.4 正则判断
[[ expression ]]
这种判断形式,支持正则表达式。
[[ string =~ regex ]]
regex
是一个正则表示式, =~
是正则比较运算符。
给一个例子:
#!/bin/bash
INT=-5
# 判断变量 INT 的字符串形式是否满足 ^-?[0-9]+$ 的正则模式
# 如果满足就表明它是一个整数
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi
1.3.5 算术判断
算术判断不使用 test 命令,而是直接使用 ((...)) 结构。只要是算术表达式,都能用于 ((...)) 语法,详见 Bash 学习笔记 2 变量操作 中 整数运算 一节。
给一个例子:
# 代码执行后输出 true
if ((3 > 2)); then
echo "true"
fi
如果算术计算的结果是非零值,则表示判断成立,跟命令的返回值正好相反。
# ((1))表示判断成立 $ if ((1)); then echo "It is true."; fi It is true. # ((0))表示判断不成立。 $ if ((0)); then echo "It is true."; else echo "it is false."; fi It is false.
1.3.6 基础逻辑判断
逻辑运算的对象为几个 test 表达式的结果。
AND
: 符号&&
,也可使用参数-a
。OR
: 符号||
,也可使用参数-o
。NOT
: 符号!
。
使用否定操作符 !
时,最好用圆括号确定转义的范围。
# test 命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。
if [ ! ($INT -ge $MIN_VAL -a $INT -le $MAX_VAL) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
1.3.7 命令组合
1.3.7.1 分号
分号是命令的结束符,使得多个命令依次执行:
$ clear; ls
先执行 clear 命令,执行完成后,再执行 ls 命令。
使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
命令的返回值为最后一个命令的返回值。
1.3.7.2 && 和 ||
&& 和 || 可以更好地控制多个命令之间的继发关系。
# 如果 Command1 运行成功,则继续运行 Command2,否则不执行 Command2 Command1 && Command2 # 如果 Command1 运行失败,则继续运行 Command2,否则不执行 Command2 Command1 || Command2
进行三目运算的用法是 command1 && command2 || command3
。
但很多人把 command1 && command2 || command3
错误地理解为:
if command1;then
command2
else
command3
fi
下面的命令很好的指出错误的原因:
$ date && echo "It's sunny today" || echo "the weather is terrible" Thu Aug 20 11:09:35 EDT 2015 It's sunny today $ date && "It's sunny today" || echo "the weather is terrible" Thu Aug 20 11:10:45 EDT 2015 -bash: It's sunny today: command not found the weather is terrible # 这行本不该出现
||
会判断前一条命令的状态返回值,如果为 false,就执行后面的语句。
在这里 "It’s sunny today"
命令返回了 false,于是后面 echo "the weather is terrible"
就执行了。
使用 command1 && { command2; echo -n;} || commadn3
可以解决这个问题:
- 如果
command1
返回 true,||
就只会接受来自echo -n
的返回值 true 不执行command3
。 - 如果
command1
返回 false,{ command2; echo -n;}
被跳过,||
判断command1
返回 false,执行command3
。
左大括号 {
的右侧必须有一个空格。右大括号 }
前,最后一条命令后的 ;
不能少。
$ date && { "It's sunny today"; echo -n;} || echo "the weather is terrible" Wed Jan 26 16:33:34 CST 2022 -bash: It's sunny today: command not found
2 循环
2.1 while 循环
while 循环的条件被判断为真,就不断执行循环体。
# 写法 1
while condition; do
commands
done
# 写法 2
while condition
do
commands
done
# 写法 3
while condition; do commands; done
2.2 until 循环
until 用得比较少,为方便理解通常全部使用 while 循环。
until 循环与 while 循环相反,在条件被判断为真之前,循环体一直被执行。
until condition; do
commands
done
until condition
do
commands
done
until condition; do commands; done
2.3 for ... in 循环
for...in 循环用于遍历列表的每一项。
for variable in list; do
commands
done
for variable in list
do
commands
done
for variable in list; do commands; done
2.4 for 循环
for循环还支持 C 语言的循环语法。
for ((expression1; expression2; expression3)); do
commands
done
给一个例子:
for ((i=0; i<5; i=i+1)); do
echo $i
done
条件部分可以省略:
for ((;;)); do
read var
if [ "$var" = "." ]; then
break
fi
done
# 不过一般都这么写
while true; do
read var
if [ "$var" = "." ]; then
break
fi
done
2.5 break 和 continue
Bash 提供了 break
和 continue
两个内部命令,用来跳出循环。
break 命令终止循环,执行循环块之后的语句,即不再执行剩下的循环。
continue 命令终止本轮循环,开始执行下一轮循环。
2.6 select 结构
select 结构主要用来生成简单的菜单。它的语法与 for...in 循环基本一致。
select var in list; do
commands
done
Bash 会对select依次进行下面的处理。
- select 生成一个菜单,菜单每行的内容是
数字编号
与list 的一项
。 - Bash 提示用户选择一项,输入它的编号。Bash 会将该项的内容保存到指定的变量 var,该项的编号存入环境变量
$REPLY
中。 - 如果用户只输入了回车键,Bash 会重复上面的过程。
- 执行命令体 commands。
- 执行结束后,回到第一步,重复这个过程。
select 可以与 case 结合,针对不同项,执行不同的命令。
#!/bin/bash
echo "Which Operating System do you like?"
select os in Ubuntu LinuxMint Windows8 Windows10 WindowsXP; do
case $os in
"Ubuntu"|"LinuxMint")
echo "I also use $os."
;;
"Windows8" | "Windows10" | "WindowsXP")
echo "Why don't you try Linux?"
;;
*)
echo "Invalid entry."
break
;;
esac
done
$ ./test.sh Which Operating System do you like? 1) Ubuntu 2) LinuxMint 3) Windows8 4) Windows10 5) WindowsXP #? 1 I also use Ubuntu. #? 2 I also use LinuxMint. #? 3 Why don't you try Linux? #? 4 Why don't you try Linux? #? 5 Why don't you try Linux? #? 6 Invalid entry.
3 函数
3.1 写法和用法
函数 (function) 与别名 (alias) 的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
函数总是在当前 Shell 执行,而脚本在子 Shell 中执行。
如果函数与脚本同名,函数会优先执行。函数与别名同名,别名优先执行。
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
调用函数时,直接写函数名,参数跟在函数名后面。
给一个例子:
#!/bin/bash
hello() {
# $1 表示传入函数的第一个参数
echo "Hello $1"
}
hello world
在命令行调用函数:
$ hello world Hello world
unset -f functionName
删除一个函数。
declare -f
输出当前 Shell 定义的所有函数名和函数体。输出顺序为函数名的字母表顺序。
declare -F
输出已经定义的函数名,不含函数体。
declare -f functionName
查看单个函数的定义。
3.2 参数变量
函数体内可以使用参数变量,获取函数参数。
函数的参数变量,与脚本参数变量是一致的。
$1~$9
: 函数的第一个到第9个的参数。$0
: 函数所在的脚本名。$#
: 函数的参数总数。$@
: 函数的全部参数,参数之间使用空格分隔。$*
: 函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,可以自定义。
如果函数的参数多于 9 个,那么第 10 个参数可以用 ${10}
的形式引用,以此类推。
3.3 变量的作用域
函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。
函数体内不仅可以声明全局变量,还可以修改全局变量。
函数体内用 local 命令来声明局部变量。
3.4 返回值
return
命令将值返回给调用者。函数执行到这条命令,就不再执行之后的函数体。
function func_return_value {
return 10
}
如果在命令行直接执行函数,下一个命令可以用 $?
拿到返回值。
$ func_return_value $ echo "Value returned by function is: $?" Value returned by function is: 10
return 后面不跟参数,只用于返回也是可以的。此时返回值为 0。
function name {
commands
return
}