这里我们记录一下 shell 中的 Control Flow,包括 if-else,case,for,while。

我们先来看 if-else,它的语法描述是这样的:

if command; then
    commands if condition true
elif command; then
    commands if condition true
else
    commands if condition false
fi

这里放在 if 或 elif 中的 command 指的是一个命令的返回值,换句话说,if 是根据 command 执行成功 还是失败来去作为条件去判断走哪个分支的。比如,我们经常用 if 来判断:如果一个目录不存在,那么我们 创建该目录,我们可以这样做:

if ! test -d /path/to/dir; then
    mkdir -p /path/to/dir
fi

这里,! commands 或者 ! (commands) 表示对该命令的返回值取反,如:

➜  guo-sj.github.io git:(main) ✗ ls
docs      README.md
➜  guo-sj.github.io git:(main) ✗ test -d docs
➜  guo-sj.github.io git:(main) ✗ echo $?
0
➜  guo-sj.github.io git:(main) ✗ ! test -d docs
➜  guo-sj.github.io git:(main) ✗ echo $?       
1
➜  guo-sj.github.io git:(main) ✗ ! (test -d docs) 
➜  guo-sj.github.io git:(main) ✗ echo $?
1
➜  guo-sj.github.io git:(main) ✗ 

这里值得一提的就是,test 命令在我们写 shell 脚本的条件判断时会经常用到,它的语法是:

test expression

以下是几种常用的 expression 的写法:

expression meaning
-d file 如果 file 存在且是一个目录,返回 True
-f file 如果 file 存在且是一个普通文件,返回 True
s1 = s2 对于 string s1 和 s2,如果两者相等,返回 True
s1 != s2 对于 string s1 和 s2,如果两者不相等,返回 True
s1 < s2 对于 string s1 和 s2,如果s1 比 s2 小,返回 True
s1 > s2 对于 string s1 和 s2,如果s1 比 s2 大,返回 True
n1 -eq n2 对于 integer n1 和 n2,如果两者相等,返回 True
n1 -ne n2 对于 integer n1 和 n2,如果两者不相等,返回 True
n1 -gt n2 对于 integer n1 和 n2,如果 n1 大于 n2,返回 True
n1 -ge n2 对于 integer n1 和 n2,如果 n1 大于等于 n2,返回 True
n1 -lt n2 对于 integer n1 和 n2,如果 n1 小于 n2,返回 True
n1 -le n2 对于 integer n1 和 n2,如果 n1 小于等于 n2,返回 True
! expression 如果 expression 的值是 False,返回 True
e1 -a e2 对于 expression e1 和 e2,e1 和 e2 同时成立的时候,返回 True,-a 表示 and
e1 -o e2 对于 expression e1 和 e2,e1 或 e2 成立的时候,返回 True,-o 表示 or

所以你看,test ! expression 和 ! test expression 是等价的,所以上述的我们判断目录是否存在的程序也可以这么写:

if test ! -d /path/to/dir; then
    mkdir -p /path/to/dir
fi

PS:test 和 [ ] 是等价的,其实 [ ] 就是 test 命令的语法糖,这个写程序的时候看个人喜好,我个人还是比较喜欢用 test :-)

好了,我们介绍完了 if,我们再来看下条件判断的另一种写法 -- case。case 的语法如下:

case word in
pattern)  commands ;;
pattern)  commands ;;
...
esac

case 语句会自上而下的比较 word 和 pattern,执行第一个匹配的 pattern 对应的 commands,然后退出。

pattern 的写法是 shell 支持的 wildcard,而不是我们熟悉的 regular expressions。

注意,每个 pattern 对应的 commands 是以两个分号结尾 -- ;;,这个一定要注意。

比如这个例子:

for i in `date`
do
case $i in
Sun) echo "got Sun";;
2025) echo "got 2025";; 
*) echo "default branch, got $i";;
esac
done

运行结果如下:

➜  guo-sj.github.io git:(main) ✗ date
Sun Sep 14 20:57:05 CST 2025
➜  guo-sj.github.io git:(main) ✗ for i in `date`
for> do
for> case $i in
for case> Sun) echo "got sun";;
for case> 2025) echo "got 2025";;
for case> *) echo "default branch, got $i";;
for case> esac
for> done
got sun
default branch, got Sep
default branch, got 14
default branch, got 20:56:51
default branch, got CST
got 2025
➜  guo-sj.github.io git:(main) ✗ 

这个例子也顺道引出了我们下一个要介绍的 for,for 的语法如下:

for i in list of words
do
    loop body, $i set to successive elements of list
done

for i   (List is implicitly all arguments to shell file, i.e., $*)
do
    loop body, $i set to successive arguments
done

第一种语法我们在上一个例子中已经说明了,这里我们简单说下第二个例子。如果我们没有在 for 循环中指定 list,那么它默认指的是所有传入的参数。如:

#cat illustrate_for.sh
for i
do
    echo $i
done

执行结果如下:

➜  guo-sj.github.io git:(main) ✗ chmod +x illustrate_for.sh 
➜  guo-sj.github.io git:(main) ✗ ./illustrate_for.sh 
➜  guo-sj.github.io git:(main) ✗ ./illustrate_for.sh 1 test guosj
1
test
guosj
➜  guo-sj.github.io git:(main) ✗  

讲完了 for,我们接下来看下 while 和 until,他们的语法如下:

while command
do
    loop body executed as long as command returns true
done

until command
do
    loop body executed as long as command returns false
done

emmm...,其实 while 和 until 刚好是相反的表达,两者也很容易做到相互转换,对于同一个 command 我们有:

  • while ! command <=> until command
  • while command <=> until ! command

如果想表达无限循环,语法是这样的:

while :
do
    commands
done