李守中

Bash 3 流程控制

Table of Contents

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 提供了 breakcontinue 两个内部命令,用来跳出循环。

break 命令终止循环,执行循环块之后的语句,即不再执行剩下的循环。

continue 命令终止本轮循环,开始执行下一轮循环。

2.6 select 结构

select 结构主要用来生成简单的菜单。它的语法与 for...in 循环基本一致。

select var in list; do
    commands
done

Bash 会对select依次进行下面的处理。

  1. select 生成一个菜单,菜单每行的内容是 数字编号list 的一项
  2. Bash 提示用户选择一项,输入它的编号。Bash 会将该项的内容保存到指定的变量 var,该项的编号存入环境变量 $REPLY 中。
  3. 如果用户只输入了回车键,Bash 会重复上面的过程。
  4. 执行命令体 commands。
  5. 执行结束后,回到第一步,重复这个过程。

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
}


Last Update: 2023-05-25 Thu 14:21

Contact: [email protected]     Generated by: Emacs 27.1 (Org mode 9.3)

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议