Shell 脚本编程(中级篇)

中级篇

一、结构化命令

基础篇的示例中,Shell 大多数情况下都以顺序的方式依次执行脚本中的每一条指令。
但是在实际情况中,很多程序都需要对脚本中命令的逻辑流进行控制。即根据某些条件和状态完成判断,再决定其后的程序该以怎样的规则执行。
Shell 脚本支持一部分结构化指令,如 if-thencase 等,用于修改程序的执行流程。

1. if-then

最基本的结构化语句即 if-then,其格式如下:

if command
then
    commands
fi

Bash Shell 中的 if-then 语句和其他编程语言中的类似语句稍有些不同,其作用如下:

  • 首先执行 if 后面跟随的命令 command
  • 如果该命令 command结束状态(exit status)为 0(即该命令执行成功),则 then 后面的命令依次被执行
  • 如果该命令 command 的结束状态为任何非 0 的值(该命令非正常退出),则不执行 then 后面的命令,Bash Shell 移动到脚本中的下一条命令(即 fi 后面的内容)

如下面的两个示例:

$ cat if1.sh
#!/bin/bash

if pwd
then
    echo "It worked"
fi
echo "We are outside the if statement"
$ ./if1.sh
/Users/starky/program/scripts/shell
It worked
We are outside the if statement
$ cat if2.sh
#!/bin/bash

if IamNotaCommand
then
    echo "It worked"
fi
echo "We are outside the if statement"
$ ./if2.sh
./if2.sh: line 3: IamNotaCommand: command not found
We are outside the if statement

在上面的示例中,if1.sh 脚本里 if 后面的命令(pwd)成功执行,则 then 后面的 echo 命令也跟着执行。
if2.sh 脚本里 if 后面的命令(IamNotaCommand)执行出错,则 then 后面的 echo 命令不会被执行。
无论如何,判断结束后都会继续执行 fi 后面的语句。

2. if-else-then

if-else-then 语句的格式为:

if command
then
    commands
else
    commands
fi

if 后面的命令执行后返回值为 0 时,则 then 后面的命令被执行。
如果 if 后面的命令执行后返回值不为 0,则执行 else 后面的命令。

示例(if3.sh)如下:

#!/bin/bash

testuser=starky
if ls -d /Users/$testuser
then
    echo "The bash files for user $testuser are:"
    ls -a /Users/$testuser/.b*
    echo
else
    echo "The user $testuser does not exist on this system."
    echo
fi

运行结果:

$ chmod +x if3.sh
$ ./if3.sh
/Users/starky
The bash files for user starky are:
/Users/starky/.bash_history /Users/starky/.bash_profile
/Users/starky/.bash_sessions:
...

但是将上述脚本中 testuser 变量的值改为系统中并不存在的用户时(如 NoSuchUser),则运行效果如下:

$ cat if3.sh
#!/bin/bash

testuser=NoSuchUser
if ls -d /Users/$testuser
then
    echo "The bash files for user $testuser are:"
    ls -a /Users/$testuser/.b*
    echo
else
    echo "The user $testuser does not exist on this system."
    echo
fi
$ ./if3.sh
ls: /Users/NoSuchUser: No such file or directory
The user NoSuchUser does not exist on this system.

其他结构形式的 if-else-then 语句还包括:

if command1
then
    commands
elif
    command2
then
    more commands
fi

当然理论上讲可以使用足够的 if-then-elif-then 语句,只不过类似情况下多使用更为恰当的 case 语句。

3. test 命令

上面提到的 if-then 语句,大多是这样的形式:

if command
then
    commands
...

if 后面跟的是一条普通的 Shell 命令,根据该命令是否成功执行,来确定 then 后面的命令是否执行。

可以借助 test 命令,使用如下形式的 if-then 语句:

if test condition
then
    commands
fi

如果 test 命令后面的 condition 计算后为 TRUE,则 test 命令退出并返回一个值为 0 的状态值。then 后面的命令被执行。
如果 test 命令后面的 condition 计算后为 FALSE,则 test 命令退出并返回一个非零的状态值。跳出当前的 if-then 并继续执行后面的内容。

如下面的两个例子:

$ cat test_condition.sh
#!/bin/bash

my_variable="Full"

if test $my_variable
then
    echo "The $my_variable expression returns a True"
else
    echo "The $my_variable expression returns a False"
fi
$ ./test_condition.sh
The Full expression returns a True
$ cat test_condition2.sh
#!/bin/bash

my_variable=""

if test $my_variable
then
    echo "The $my_variable expression returns a True"
else
    echo "The $my_variable expression returns a False"
fi
$ ./test_condition2.sh
The  expression returns a False

Bash Shell 还提供了另外一种不需要使用 test 命令来完成条件检查的 if-then 语句:

if [ condition ]
then
    commands
fi

方括号中的内容用来定义判断条件。注意方括号和 condition 之间的空格
支持三种形式的判断条件:

  • 数字比较
  • 字符串比较
  • 文件比较
数字比较
操作 描述
n1 -eq n2 检查 n1 是否等于 n2
n1 -ge n2 检查 n1 是否大于或等于 n2
n1 -gt n2 检查 n1 是否大于 n2
n1 -le n2 检查 n1 是否小于或等于 n2
n1 -lt n2 检查 n1 是否 小于 n2
n1 -ne n2 检查 n1 是否不等于 n2

示例如下:

$ cat numeric_test.sh
#!/bin/bash

value1=10
value2=11

if [ $value1 -gt 5 ]
then
    echo "The test value $value1 is greater than 5"
fi

if [ $value1 -eq $value2 ]
then
    echo "The values are equal"
else
    echo "The values are different"
fi
$ ./numeric_test.sh
The test value 10 is greater than 5
The values are different
字符串比较
操作 描述
str1 = str2 检查 str1 和 str2 是否相同
str1 != str2 检查 str1 和 str2 是否不相同
str1 < str2 检查 str1 是否小于 str2
str1 > str2 检查 str1 是否大于 str2
-n str1 检查 str1 是否长度大于 0(不为空)
-z str1 检查 str1 是否长度为 0(为空)

示例1:

$ cat welcome.sh
#!/bin/bash

testuser=starky

if [ $USER = $testuser ]
then
    echo "Welcome $testuser"
else
    echo "This is not $testuser"
fi
$ ./welcome.sh
Welcome starky

PS:在使用 str1 > str2str1 < str2 这种类型的条件时,需加上转义符号。否则大于号或小于号会被当作重定向处理。

示例2:

$ cat compare.sh
#!/bin/bash

val1=Testing
val2=testing

if [ $val1 \> $val2 ]
then
    echo "$val1 is greater than $val2"
else
    echo "$val1 is less than $val2"
fi
$ ./compare.sh
Testing is less than testing

PSif 条件中的比较依据的是基本 ASCII 顺序,通过每个字符(从首字母开始)的 ASCII 值的比较来判断大小顺序。


-n-z 常常用来确定指定字符串是否为空。
示例3:

$ cat empty.sh
#!/bin/bash

val1=testing
val2=''

if [ -n $val1 ]
then
    echo "The string '$val1' is not empty"
else
    echo "The string '$val1' is empty"
fi

if [ -z $val2 ]
then
    echo "The string '$val2' is empty"
else
    echo "The string '$val2' is not empty"
fi

if [ -z $val3 ]
then
    echo "The string '$val3' is empty"
else
    echo "The string '$val3' is not empty"
fi
$ ./empty.sh
The string 'testing' is not empty
The string '' is empty
The string '' is empty

上述脚本中 val3 变量自始至终没有被定义,被自动判断为空字符串。

文件比较
操作 描述
-d file 检查 file 是否存在且是一个目录
-e file 检查 file 是否存在
-f file 检查 file 是否存在且是一个文件
-r file 检查 file 是否存在且可读
-s file 检查 file 是否存在且不为空
-w file 检查 file 是否存在且可写
-x file 检查 file 是否存在且可执行
-O file 检查 file 是否存在且其属主为当前用户
-G file 检查 file 是否存在且其默认数组和当前用户相同
file1 -nt file2 检查 file1 是否比 file2 更新(newer than)
file1 -ot file2 检查 file1 是否比 file2 更老(older than)

示例1,目录检查:

$ cat check_dir.sh
#!/bin/bash

directory=/Users/starky
if [ -d $directory ]
then
    echo "The $directory directory exists"
    cd $directory
    ls
else
    echo "The $directory directory does not exist"
fi
$ ./check_dir.sh
The /Users/starky directory exists
Applications    Downloads   Music       extract.sh  program
Desktop     Library     Pictures    id_rsa.pub  software
Documents   Movies      Public      miniconda3  vim.tar.gz

示例2,删除空文件:

$ cat check_empty.sh
#!/bin/bash

file_name=empty

# 检查该文件是否存在
if [ -f $file_name ]
then
    # 该文件存在,则继续检查其内容是否非空
    if [ -s $file_name ]
    then
        # 内容不为空
        echo "The $file_name file exists and has data in it."
        echo "Will not remove this file"
    else
        # 内容为空
        echo "The $file_name file exists, but is empty."
        echo "Deleting empty file..."
        rm $file_name
    fi
else
    # 该文件不存在
    echo "File, $file_name, does not exist."
fi
$ ./check_empty.sh
File, empty, does not exist.
$ touch empty
$ ls empty
empty
$ ./check_empty.sh
The empty file exists, but is empty.
Deleting empty file...
$ ls empty
ls: empty: No such file or directory
4. 组合判断

if-then 语句允许使用布尔逻辑来完成多个条件的组合判断。

  • [ condition1 ] && [ condition2 ] 逻辑与
  • [ condition1 ] || [ condition2 ] 逻辑或

示例:

cat compound_test.sh
#!/bin/bash

if [ -d $HOME ] && [ -w $HOME/testing ]
then
    echo "The file exists and you can write to it"
else
    echo "I cannot write to the file"
fi
$ ./compound_test.sh
I cannot write to the file
$ touch ~/testing
$ ./compound_test.sh
The file exists and you can write to it
if-then 中的双括号

Bash Shell 在 if-then 语句中还提供了一些更高级的特性:

  • 双小括号(数学表达式)
  • 双中括号(字符串处理函数)
使用双小括号

双小括号允许在条件测试时使用(相对于 test 命令)更高级的数学表达式。
格式:(( expression ))
其中 expression 除了支持前面提到过的基本的数学操作符外,还支持以下操作符:

操作符 描述
val++ Post-increment
val-- Post-decrement
++val Pre-increment
--val Pre-decrement
! 逻辑非
~ 按位否定
**
<< 向左按位移位
>> 向右按位移位
& 按位逻辑与
| 按位逻辑或
&& 逻辑与
双管道符 逻辑或

示例:

$ cat parenthesis.sh
#!/bin/bash

val1=10

if (( $val1 ** 2 > 90 ))
then
    (( val2 = $val1 ** 2 ))
    echo "The square of $val1 is $val2"
fi
$ ./parenthesis.sh
The square of 10 is 100
使用双中括号

双中括号为 test 命令中的字符串比较提供了更高级的特性,尤其是支持 模式匹配
示例:

cat pattern.sh
#!/bin/bash

if [[ $USER == s* ]]
then
    echo "Hello $USER"
else
    echo "Sorry, I do not know you"
fi
$ ./pattern.sh
Hello starky
4. case 命令

很多时候,某个作为判断条件的变量有多个可能的取值,而你需要根据不同的取值指导程序去做对应的操作。
如果使用 if-then-else 语句,代码免不了会拉得有些长。。。

不同于 if 语句(通过 if、elif 依次检查同一个变量的所有取值),case 命令以类似列表的形式检查同一个变量的所有取值:

case variable in
    pattern1 | pattern2) commands1;;
    pattern3) commands2;;
    *) default commands;;
esac

case 命令将指定的变量与多个不同的模式进行比对,如果匹配,则执行该模式后面的命令。
可以在同一行中定义多个模式(使用 | 符号分隔)。
星号表示当之前的所有模式都不能被匹配时,默认执行的命令。

示例:

$ cat case_user.sh
#!/bin/bash

case $USER in
    rich | barbara)
        echo "Welcome, $USER"
        echo "Please enjoy your visit";;
    testing)
        echo "Special testing account";;
    jessica)
        echo "Do not forget to logout when you're done";;
    *)
        echo "Sorry, you are not allowed here";;
esac
$ ./case_user.sh
Sorry, you are not allowed here

上面的脚本等同于使用如下的 if 语句:

$ cat if_user.sh
#!/bin/bash

if [[ $USER = "rich" || $USER = "barbara" ]]
then
    echo "Welcome $USER"
    echo "Please enjoy your visit"
elif [ $USER = "testing" ]
then
    echo "Special testing account"
elif [ $USER = "jessica" ]
then
    echo "Do not forget to logout when you're done"
else
    echo "Sorry, you are not allowed here"
fi
$ ./if_user.sh
Sorry, you are not allowed here

二、更多结构化命令

1. for 命令

很多时候,需要重复地执行一系列命令直到某个特定的情况出现。
for 命令允许创建一个循环来遍历某些值,并在循环过程中使用遍历到的值执行一系列特定的指令。语法格式:

for var in list
do
    commands
done
遍历列表
$ cat for_list.sh
#!/bin/bash
for state in Alabama Alaska Arizona
do
    echo The next state is $state
done

echo "The last state we visited was $state"
$ ./for_list.sh
The next state is Alabama
The next state is Alaska
The next state is Arizona
The last state we visited was Arizona

每次 for 命令遍历由后面列表(Alabama Alaska Arizona)提供的值时,会把列表中当前项目的值赋值给 $state 变量(即第一次遍历把 Alabama 赋值给 $state,第二次是 Alaska),该 $state 变量可以在后面的 do ... done 结构中使用。

for 循环结束后 $state 变量依旧有效,即保留最后一次遍历时得到的值(列表的最后一项 Arizona)而不会被销毁。

遍历字符串中的列表

Bash Shell 中的for 循环可以直接接收单个字符串作为参数,并通过其中的空格将该字符串分割成多个遍历的项目。

$ cat for_variable.sh
#!/bin/bash

states="Alabama Alaska Arizona"
states=$states" Arkansas"

for state in $states
do
    echo "Have you ever visited $state?"
done
$ ./for_variable.sh
Have you ever visited Alabama?
Have you ever visited Alaska?
Have you ever visited Arizona?
Have you ever visited Arkansas?

脚本中 state=$state" Arkansas" 可以用来在 $state 变量中的字符串末尾添加新的字符串,也等同于在列表末尾添加了新的项目。

从命令中获取遍历的列表

可以通过命令替换将命令的输出作为 for 命令循环遍历的列表:

$ cat states.txt
Alabama Alaska Arizona
$ cat for_command.sh
#!/bin/bash

for state in $(cat states.txt)
do
    echo "Visit beautiful $state"
done
$ ./for_command.sh
Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
遍历文件目录

可以使用 for 命令直接循环目录中的文件(结合 * 符号):

$ cat for_dir.sh
#!/bin/bash

for file in /Users/starky/.vim* /Users/starky/badtest
do
    if [ -d "$file" ]
    then
        echo "$file is a directory"
    elif [ -f "$file" ]
    then
        echo "$file is a file"
    else
        echo "$file doesn't exit"
    fi
done
$ ./for_dir.sh
/Users/starky/.vim is a directory
/Users/starky/.viminfo is a file
/Users/starky/.vimrc is a file
/Users/starky/badtest doesn't exit

注意脚本中的 if [ -d "$file" ] 段代码, "$file" 被双引号包裹了起来。
原因是文件名中包含空格是合法的,但是在 Shell 脚本中,如果 $file 变量的值包含空格,则不能直接在 if [ -d ... ] 中使用,所以这里使用了双引号。

2. C 语法的 for 命令

Bash Shell 也支持语法类似于 C 语言形式的 for 循环:
for (( variable assignment ; condition ; iteration process ))

$ cat for_c.sh
#!/bin/bash

for (( i=1; i <= 10; i++ ))
do
    echo "The next number is $i"
done
$ ./for_c.sh
The next number is 1
The next number is 2
The next number is 3
The next number is 4
The next number is 5
The next number is 6
The next number is 7
The next number is 8
The next number is 9
The next number is 10
3. while 命令

while 命令允许用户定义一个判断指令,然后循环执行一系列命令,并在每次执行前检查判断指令的返回值(exit status)。直到判断指令的返回值不为 0,终止循环。

while 命令的格式为:

while test command
do
    other commands
done

示例程序如下:

$ cat while.sh
#!/bin/bash

var=5
while [ $var -gt 0 ]
do
    echo $var
    var=$[ $var -1 ]
done
$ ./while.sh
5
4
3
2
1
4. break 命令

break 命令用于跳出循环

$ cat break.sh
#!/bin/bash

for var in 1 2 3 4 5 6 7 8 9 10
do
    if [ $var -eq 5 ]
    then
        break
    fi
    echo "Iteration number: $var"
done
echo "The for loop is completed"
$ ./break.sh
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
The for loop is completed
5. 处理循环的输出

可以使用管道重定向处理循环的输出信息,直接将管道或重定向命令加在循环语句块的末尾即可。

$ cat output.sh
#!/bin/bash

for (( a = 1; a < 5; a++ ))
do
    echo "The number is $a"
done > numbers.txt

echo "The command is finished"
$ ./output.sh
The command is finished
$ cat numbers.txt
The number is 1
The number is 2
The number is 3
The number is 4

参考资料

Linux Command Line and Shell Scripting Bible 3rd Edition

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,392评论 0 5
  • 第 2 章 SHELL 基础知识2.1 shell脚本我们在上面简单介绍了一下什么是shell脚本,现在我们来进一...
    LiWei_9e4b阅读 1,574评论 0 0
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,746评论 0 10
  • 初读《World of Warcraft编年史》就被其宏大的构思和奇妙的想象所震撼。 利用简书作为笔记本,来简述这...
    南森兮阅读 311评论 1 2
  • 在茂密的树林中,常常可以看到从枝叶间透过的一道道光柱,类似于这种自然现象,就是丁达尔现象。摄影是光线捕捉的艺术,丁...
    伊水浅墨阅读 358评论 0 0