本文主要对Shell中的流程控制语句进行简单总结,另外本文所使用的Linux环境为CentOS Linux release 8.2.2004
,所使用的Shell为bash 5.1.0(1)-release
。
一、if
语法:
# 写法一,if和then写在一行
# 分隔符;在任何命令结尾都可以使用,命令单独成行时可省略
if test-commands1; then
commands1;
[elif test-commands2; then
commands2;]
……
[else
other-commands;]
fi
# 写法二
if test-commands1
then
commands1
[elif test-commands2
then
commands2]
……
[else
other-commands]
fi
# 单行写法
if test-commands1; then commands1; [elif test-commands2; then commands2;]… [else other-commands;] fi
执行流程说明:首先执行test-commands1,如果其返回状态为0,则执行commands1。如果test-commands1返回状态非0,并且有elif
子句,则依次执行每个elif
的test-commands,如果其退出状态为0,则执行其对应的命令。如果有else
子句,并且最后的if
或elif
子句的最后一个命令退出状态为非0,则执行other-commands。整个命令的返回状态是最后一个被执行的命令的退出状态,如果没有一个条件为真,则返回0。
示例如下:
#!/bin/bash
read -p "输入家庭年收入 > " income
# 根据输入的家庭年收入,输出对应的级别
if ((income>50000000)); then
echo "巨富"
elif ((income>=10000000)); then
echo "豪富"
elif ((income>=5000000)); then
echo "中富"
elif ((income>=1000000)); then
echo "小富"
elif ((income>=300000)); then
echo "高产"
elif ((income>=150000)); then
echo "中产"
elif ((income>=80000)); then
echo "小康"
elif ((income>=30000)); then
echo "穷"
elif ((income>=10000)); then
echo "贫困"
else
echo "赤贫"
fi
输入30000000,执行结果:
输入120000,执行结果:
二、case
语法:
# 写法一
case word in
[ [(] pattern [| pattern]…)
command-list
;;]
……
esac
# 写法二
case word in
[ [(] pattern [| pattern]…)
command-list ;;]
……
esac
# 写法三
case word in
[ [(] pattern [| pattern]…) command-list ;;]
……
esac
# 单行写法
case word in [ [(] pattern [| pattern]…) command-list ;;]… esac
执行流程说明:case会选择性地执行与word所匹配的第一个模式(pattren)对应的command-list。|
用来分隔多个模式,)
用来结束模式列表。模式列表和其对应的command-list合起来称为子句,case
子句的数量不限,每个子句都必须由;;
、;&
或;;&
结束。最先匹配的模式决定了要执行的command-list。通常习惯使用*
作为最后一个模式来定义默认情况,因为该模式总是匹配的。这里的word在匹配之前要经历波浪号扩展、参数扩展、命令替换、算术扩展以及引用去除(quote removal)。每个模式也都要经历波浪号扩展、参数扩展、命令替换以及算术扩展。如果没有任何模式与之匹配,则整个case
返回状态为0,否则返回状态为最后一个被执行的command-list的退出状态。
如果启用了shell的
nocasematch
选项,则在匹配时不考虑字母的大小写。
以;;
、;&
、;;&
结束子句的区别:如果用;;
来结束子句,则在匹配了第一个模式后就不会再匹配其他模式。如果用;&
代替;;
,如果下面还有其他子句,则会继续执行下一个子句对应的command-list。如果用;;&
&代替;;
,如果下面还有其他子句,则会测试下一个子句的模式(如果有的话),并在匹配成功时执行相对应的command-list。
模式中*
匹配任何字符串,?
匹配任意单个字符,[…]
匹配方括号中任一字符,由连字符-
分隔的一对字符是范围表达式,使用当前语言区域(locale)设置的排序序列和字符集中任何在这两个字符之间(包括这两个字符)的字符都会被匹配。如果在[
之后的第一个字符是!
或~
,则匹配没有出现在方括号中的任一字符,如果要匹配-
,可以把它放在方括号第一个或最后一个位置,如果要匹配]
,可以把它放在方括号第一个位置。
使用case
根据输入的一个整数输出对应星期的英文单词的示例如下:
#!/bin/bash
read -p "输入1-7的整数 > " num
# 根据输入的整数(范围1到7),输出对应星期的英文单词
case ${num} in
(1)
echo "Monday"
;;
(2)
echo "Tuesday"
;;
(3)
echo "Wednesday"
;;
(4)
echo "Thursday"
;;
(5)
echo "Friday"
;;
(6)
echo "Saturday"
;;
(7)
echo "Sunday"
;;
(*)
echo "Invalid Data"
;;
esac
执行结果:
使用case
根据输入的菜单选项或菜名进行点菜的示例如下:
#!/bin/bash
# 模拟一份简单菜单
echo "#####菜单#####
1.青椒肉丝
2.麻婆豆腐
3.辣子鸡丁
4.鱼香肉丝
5.水煮肉片
##############"
# 循环输入菜单选项或菜名进行点菜,输入0或点菜结束退出循环以结束点菜
while read -p "请点菜 > " choice
do
case ${choice} in
1 | "青椒肉丝")
echo "青椒肉丝一份"
;;
2 | "麻婆豆腐")
echo "麻婆豆腐一份"
;;
3 | "辣子鸡丁")
echo "辣子鸡丁一份"
;;
4 | "鱼香肉丝")
echo "鱼香肉丝一份"
;;
5 | "水煮肉片")
echo "水煮肉片一份"
;;
0 | "点菜结束")
echo "点菜结束,稍后上菜"
break
;;
*)
echo "请选择菜单中的菜品"
;;
esac
done
执行结果:
使用case
根据输入的一个字符输出字符对应的类型的示例如下:
#!/bin/bash
read -n 1 -p "输入一个字符 > " char
# 根据输入的字符输出字符的类型(是英文字母、数字还是其他字符)
case ${char} in
[a-zA-Z])
echo -e "\n英文字母" ;;
[0-9])
echo -e "\n数字" ;;
*)
echo -e "\n其他字符" ;;
esac
输入K,执行结果:
输入6,执行结果:
case
子句以;;
结束,在匹配了第一个模式之后不再继续匹配下面的模式;case
子句以;&
结束,在匹配了第一个模式之后会继续执行之后的子句对应的命令,不管是否匹配这些子句中的模式;case
子句以;;&
结束,在匹配了第一个模式之后会继续匹配下面的模式,只执行匹配的模式对应的命令。示例如下:
#!/bin/bash
read -p "输入一个字符串 > " str
echo -e "\ncase子句以;;结束"
case ${str} in
h?o) echo "以h开头o结尾的字符串,中间只能为任意一个字符" ;;
h*o) echo "以h开头o结尾的字符串" ;;
[e-m][c-e]*) echo "以字符e到m的开头,字符c到e为第二个字符的字符串" ;;
*) echo "任意字符串" ;;
esac
echo -e "\ncase子句以;&结束"
case ${str} in
h?o) echo "以h开头o结尾的字符串,中间只能为任意一个字符" ;&
h*o) echo "以h开头o结尾的字符串" ;&
[e-m][c-e]*) echo "以字符e到m的开头,字符c到e为第二个字符的字符串" ;&
*) echo "任意字符串" ;&
esac
echo -e "\ncase子句以;;&结束"
case ${str} in
h?o) echo "以h开头o结尾的字符串,中间只能为任意一个字符" ;;&
h*o) echo "以h开头o结尾的字符串" ;;&
[e-m][c-e]*) echo "以字符e到m的开头,字符c到e为第二个字符的字符串" ;;&
*) echo "任意字符串" ;;&
esac
输入hao,执行结果:
输入haeo,执行结果:
三、while
语法:
while test-commands
do
commands
done
#单行写法
while test-commands; do commands; done
执行流程说明:只要test-commands的退出状态为0,就执行commands。其返回状态是commands中最后一个被执行的命令的退出状态,如果commands没有被执行则返回0。
计算1加到100的和的示例如下:
#!/bin/bash
i=1
sum=0
while ((i<=100))
do
((sum+=i,i++))
done
echo "1加到100的和为${sum}"
执行结果:
四、until
语法:
until test-commands
do
commands
done
# 单行写法
until test-commands; do commands; done
执行流程说明:只要test-commands的退出状态为非0,就执行commands。其返回状态是commands中最后一个被执行的命令的退出状态,如果commands没有被执行则返回0。
计算1加到100的和的示例如下示例如下:
#!/bin/bash
i=1
sum=0
until ((i>100))
do
((sum+=i,i++))
done
echo "1加到100的和为${sum}"
执行结果:
五、for
语法:
# 第一种形式
for name [in [words …] ]
do
commands
done
# 第一种形式单行写法
for name [ [in [words …] ] ; ] do commands; done
# 第二种形式
for (( expr1; expr2; expr3 ))
do
commands
done
# 第二种形式单行写法
for (( expr1; expr2; expr3 )); do commands; done
执行流程说明:
- 第一种形式:将words扩展成一个列表,将结果列表中的每个元素都赋值给name并执行一次commands。如果没有
in words
,则依次对每个位置参数都执行一次commands,就像指定了in $@
一样。其返回状态是commands中最后一个被执行的命令的退出状态。如果对单词的扩展没得到任何元素,则不执行任何命令,返回状态为0。 - 第二种形式:首先对算术表达式expr1进行求值,然后反复对算术表达式expr2进行求值,直到它的求值结果为0。每当expr2求值结果为非0值时,则执行一次commands并对算术表达式expr3进行求值。如果省略了任何表达式,则其表现就像表达式求值结果为1一样。其返回值是commands中最后一个被执行的命令的退出状态,如果任何一个表达式都是无效的,则返回false。
第一种形式的for循环可以直接指定要循环的列表,多个值之间以空格分隔。示例如下:
#!/bin/bash
echo "循环打印1,2,3,4,5,6"
for i in 1 2 3 4 5 6
do
echo ${i}
done
# 变量i在for循环外依然有效,其值为最后一次循环赋的值
echo "循环中i的最后一个值为${i}"
# 变量i也允许修改
i=1
echo "现在i的值为${i}"
echo "循环打印Bourne Shell,BourneAgain Shell,C Shell,Korn Shell"
# 列表中的值包含空格,可以用双引号引起来(推荐)
for var1 in "Bourne Shell" "BourneAgain Shell" "C Shell" "Korn Shell"
do
echo ${var1}
done
echo "循环中var1的最后一个值为${var1}"
var1="Bourne Shell"
echo "现在var1的值为${var1}"
echo "循环打印a'b,c'd,e\"f,g\"h"
# 如果列表中的值包含'或",可以通过反斜杠\转义或引号引起来
for var2 in a\'b "c'd" e\"f 'g"h'
do
echo ${var2}
done
echo "循环中var2的最后一个值为${var2}"
var2="a'b"
echo "现在var2的值为${var2}"
执行结果:
第一种形式的for循环可以通过{start..end}
的方式指定要循环的列表,{start..end}
指定列表取值范围。示例如下:
#!/bin/bash
sum=0
for i in {1..100}
do
((sum+=i))
done
echo "1加到100的和为${sum}"
执行结果:
第一种形式的for循环可以使用变量来指定要循环的列表。示例如下:
#!/bin/bash
echo "以空格分隔每个单词"
list="Bourne Shell BourneAgain Shell C Shell Korn Shell"
for var1 in ${list}
do
echo ${var1}
done
echo "以,分隔每个单词"
list="Bourne Shell,BourneAgain Shell,C Shell,Korn Shell"
# Shell会把$IFS中的每个字符作为分隔符,在单词拆分(word splitting)时按照这些字符进行拆分
# 如果IFS未设置,它的值默认为空格符、制表符、换行符
# 将IFS的默认值保存在变量IFS_OLD中
IFS_OLD=${IFS}
# 将IFS的值修改为,,只以,作为分隔符
IFS=,
for var2 in ${list}
do
echo ${var2}
done
# 将IFS恢复为IFS的默认值
IFS=${IFS_OLD}
执行结果:
第一种形式的for循环可以指定数组中的所有下标或元素为要循环的列表来遍历数组。示例如下:
#!/bin/bash
declare -A user=(["username"]="zs123" ["password"]="a123456" ["age"]=25 ["email"]="zs123@xxx.com")
echo "直接遍历数组元素的值"
for v in ${user[@]}
do
echo ${v}
done
echo "通过下标遍历数组"
for i in ${!user[@]}
do
echo "${i} : ${user[${i}]}"
done
执行结果:
第一种形式的for循环可以通过命令替换指定命令执行结果为要循环的列表。示例如下:
#!/bin/bash
# 将输入重定向读取的内容重定向输出到文件中
cat > test.txt << EOF
Expand words, and execute commands
once for each member
in the resultant list, with name
bound to the current member.
EOF
echo "以IFS的默认值分隔每个单词"
for word in $(cat test.txt)
do
echo ${word}
done
echo "只以换行符分隔每个单词"
# Shell会把$IFS中的每个字符作为分隔符,在单词拆分(word splitting)时按照这些字符进行拆分
# 如果IFS未设置,它的值默认为空格符、制表符、换行符
# 将IFS的默认值保存在变量IFS_OLD中
IFS_OLD=${IFS}
# 将IFS的值修改为换行符,只以换行符作为分隔符
IFS=$'\n'
for line in $(cat test.txt)
do
echo ${line}
done
# 将IFS恢复为IFS的默认值
IFS=${IFS_OLD}
执行结果:
第一种形式的for循环可以通过文件名扩展所匹配的文件列表为要循环的列表。
文件名扩展:单词拆分后,除非设置了
set
命令-f
选项,否则Bash会在每个单词中扫描字符*
、?
、[
。如果找到其中一个字符,并且没有被引用(quote),则该单词会被视为一个模式(pattern),并被一个与该模式匹配的文件名列表按字母顺序排序的方式所替换。如果未找到匹配的文件名并且禁用了shell的nullglob
选项,则不处理该单词。如果启用了shell的nullglob
选项并且未找到匹配的文件名,则删除该单词。
示例如下:
#!/bin/bash
echo "循环打印/root目录下的所有文件并判断它是目录还是普通文件"
# /root/*匹配/root目录下的所有文件
for file1 in /root/*
do
if [[ -d ${file1} ]]
then
echo "${file1}是一个目录"
elif [[ -f ${file1} ]]
then
echo "${file1}是一个普通文件"
fi
done
echo "循环打印所有以b或c开头的后缀名为.sh的文件"
# [bc]*.sh匹配所有以b或c开头的后缀名为.sh的文件
for file2 in [bc]*.sh
do
echo ${file2}
done
echo "循环打印所有只有单个字符的后缀名为.sh的文件"
# ?.sh匹配所有只有单个字符的后缀名为.sh的文件
for file3 in ?.sh
do
echo ${file3}
done
执行结果:
第一种形式的for循环如果没有指定in words
,则默认$@
为要循环的列表。示例如下:
#!/bin/bash
function func1() {
for parameter1
do
echo ${parameter1}
done
}
function func2() {
for parameter2 in $@
do
echo ${parameter2}
done
}
echo "省略列表时循环打印的结果"
func1 1 3 5 7 9
echo "指定\$@时循环打印的结果"
func2 2 4 6 8 10
执行结果:
第二种形式的for循环计算1加到100的和的示例如下:
#!/bin/bash
sum=0
for (( i=1; i<=100; i++ ))
do
((sum+=i))
done
echo "1加到100的和为${sum}"
执行结果:
第二种形式的for循环中的三个表达式中的任意一个都可以省略,其表现就像表达式求值结果为1一样。这里以三个表达式都省略为例:
#!/bin/bash
sum=0
i=1
# 省略了第二个表达式,循环就为无限循环,可以在循环体内使用break命令强制结束循环
for (( ; ; ))
do
if ((i>100))
then
break
fi
((sum+=i,i++))
done
echo "1加到100的和为${sum}"
执行结果:
六、select
select结构使生成菜单变得很简单。语法:
select name [in words …]
do
commands
done
# 单行写法
select name [in words …]; do commands; done
执行流程说明:words将被扩展并生成一个项目列表,扩展的单词集合将被打印到标准错误输出流上,每个单词前面都会加上一个序数。如果省略in words
,就打印位置参数,就像指定了in $@
一样。之后将显示PS3
提示符并从标准输入读取一行输入。如果输入行包含一个与所显示的其中一个单词对应的数字,则name的值就被设置为该单词。如果输入为空,则重新显示单词列表(项目列表)和提示符。如果输入了EOF
,则select
将会结束。输入任何其他值都会导致name被置为空值。读取的输入行保存在变量REPLY
中。每次选择之后都会执行commands,直到一个break
命令被执行为止,此时select
执行结束。
使用select
生成一个简单菜单并进行选择的示例如下:
#!/bin/bash
echo "#####菜单#####"
# 输入的时候按下Ctrl+D组合键可以退出select
select name in "青椒肉丝" "麻婆豆腐" "辣子鸡丁" "鱼香肉丝" "水煮肉片"
do
echo ${name}
done
执行结果:
使用select
生成菜单并结合case
根据菜单选项进行点菜的示例如下:
#!/bin/bash
echo "#####菜单#####"
# 使用select生成菜单选项并结合case根据菜单选项进行点菜
# 最后一个菜单选项为退出选项,通过break命令退出select以结束点菜
select name in "青椒肉丝" "麻婆豆腐" "辣子鸡丁" "鱼香肉丝" "水煮肉片" "点菜结束"
do
case ${name} in
"青椒肉丝")
echo "${name}一份"
;;
"麻婆豆腐")
echo "${name}一份"
;;
"辣子鸡丁")
echo "${name}一份"
;;
"鱼香肉丝")
echo "${name}一份"
;;
"水煮肉片")
echo "${name}一份"
;;
"点菜结束")
echo "${name},稍后上菜"
break
;;
*)
echo "请选择菜单中的菜品"
;;
esac
done
执行结果:
七、break
break [n]
使用内置命令break
可以从for
、while
、until
或select
循环中退出。如果给定n,则退出n层循环,n必须大于或等于1。除非n小于1,否则返回状态为0。
if
、then
、elif
、else
、fi
、case
、esac
、until
、while
、for
、select
、in
、do
、done
都是Shell保留字。而break
和continue
是Shell内置命令。
使用break
退出单层循环的示例如下:
#!/bin/bash
for (( i=1; ; i++ ))
do
if ((i>9))
then
# 退出外层循环
break
fi
for (( j=1; ; j++ ))
do
if ((j>i))
then
# 退出内层循环
break
fi
echo -n "${i}*${j}=$((i*j)) "
done
echo ""
done
执行结果:
使用break
退出多层循环的示例如下:
#!/bin/bash
for (( i=1; ; i++ ))
do
for (( j=1; ; j++ ))
do
if ((i>9))
then
# 退出2层循环
break 2
fi
if ((j>i))
then
# 退出内层循环
break
fi
echo -n "${i}*${j}=$((i*j)) "
done
echo ""
done
执行结果:
八、continue
continue [n]
使用内置命令continue
可以跳过for
、while
、until
或select
的本次循环,继续执行下一次循环。如果给定n,则跳过n层循环中的本次循环(n层循环的每一层都会跳过本次循环),继续执行下一次循环。n必须大于或等于1。除非n小于1,否则返回状态为0。
使用continue
跳过单层循环的本次循环的示例如下:
#!/bin/bash
for (( i=1; i<=9; i++ ))
do
if ((i%2==0))
then
# 跳过外层循环的本次循环
continue
fi
for (( j=1; j<=i; j++ ))
do
if ((j%3==0))
then
# 跳过内层循环的本次循环
continue
fi
echo -n "${i}*${j}=$((i*j)) "
done
echo ""
done
执行结果:
使用continue
跳过多层循环的本次循环的示例如下:
#!/bin/bash
for (( i=1; i<=9; i++ ))
do
for (( j=1; j<=i; j++ ))
do
if ((j%3==0))
then
# 跳过2层循环的本次循环
continue 2
fi
echo -n "${i}*${j}=$((i*j)) "
done
echo ""
done
echo ""
执行结果: