理解 Bash 的 if 语句

理解 Bash 的 if 语句 (转)

写 bash 脚本的日子也不短了,但是每次用到 if 语句时大脑还是会卡壳一下,要翻教程和看以前的代码,因为条件部分语法神出鬼没,捉摸不定,于是我还是花点时间狠狠研究了一下,写了这篇文章做总结。

诡异的语法

一般 bash 教程给出的语法示例基本就是:

if condition; then echo yes else echo no fi

看起来很简单,除了 condition 外(也就是条件部分),其余关键字都没什么难懂了,顶多注意一下写在同一行要加分号。

但条件部分代码说得不明不白,只是列出一堆实际例子,例如判断文件存在可以用 [ -f file ],判断目录用 [ -d dir ] 等等。这些例子也不算复杂,但是真写起来,想构造复杂点条件都能让你调试到欲仙欲死。

所以这篇文章说理解 if 语句,就是指理解 if 语句的条件部分。

大概有如下问题:

  • 为毛是用方括号,不是小括号?

  • 为毛见到有的代码用两个方括号,有的还是两个小括号?

  • 为毛有时比较字符串相等可以用 -eq,但又可以用等号?

  • 为毛有时还能直接用命令,命令到底要不要加反单引号呢?

  • 写了句复合条件,结果一堆看不懂的语法错误?

  • 测试好麻烦啊,每次都要 if/then/else 整句写全,有没有简单点测试方法?

五花八门的语法一直让我无比纠结,一般编程语言的 if 语句说明都不过半页,但是 bash 的 if 语句,我看了好几本书和教程,都没搞清楚,不能一口气就能写出来,而是要翻教程和看以前的代码,反复调试。

最后是靠 Advanced Bash-Scripting Guide 这本书搞清楚的。

条件部分的意义

很多教程都这么说:condition 的代码执行后,如果结果为 true,就继续执行 then 部分,否则继续执行 else部分。跟其它语言的一样的,没区别,例如 [ -f file ] 判断文件是否存在,文件存在就是 true 了,不存在就是 false 了。

说 true 和 false,也就是执行结果是个布尔值,这么说就是造成条件部分难写的原因,被这个说法误导了,所以要换一个角度看。

应该说 condition 的代码正常执行

什么叫「正常执行」呢?这里要搞懂一个叫「退出状态码」(exit status)概念,有时候也叫「返回状态码」(return code)。也就是子进程退出时向调用它的父进程返回的一个整数值,一般编程语言都有个 exit() 函数来直接退出,这个函数的参数就是返回给父进程的状态码,不传默认就是 0。

对于 bash 来说,调用过的子进程的返回状态码保存在 $? 环境变量中,每执行过一个命令后都会被更新,可以用 echo 来查看。

$ ls; echo $? 0

按 unix 的规范,返回状态码为 0 就是表示正常执行,其它值都表示不正常。

非要当成布尔值看待的话,可以这样想:为 0 就是正常,正常就是 true,不为 0 就是不正常,不正常就是 false。这跟其它语言 0 当 false 不同,所以特容易搞反。

但是返回什么值是程序自己决定的,一般常见 unix 程序都会仔细定义状态码。

对于 ls 来说,列得到文件表示正常:

$ ls exists.txt; echo $?; exists.txt 0 $ ls not_exists.txt; echo $?; ls: cannot access not_exists.txt: No such file or directory 2

对于 grep 来说,有匹配表示正常:

$ echo 'abc' | grep 'a'; echo $? abc 0 $ echo 'abc' | grep 'd'; echo $? 1

或者具体点说,condition 的代码执行后,这时候环境变量 $? 的值是否为 0

先定义一下用语,ls、grep 这些这里称为「程序」,而「命令」是指在提示符(即交互 shell)里打的整条字符串,「程序」名称加上参数或管道就是一条「命令」了。

下面来逐步解释一下。

test 程序

如果要检查文件是否存在,只要找到一个程序,存在文件返回状态码 0,不存在就返回非 0 就行了,上面的 ls 就这样了。

然后构造一个 ls 命令来判断,不需要反单引号包围,套到 if 语句里:

$ if ls exists.txt; then echo yes; else echo no; fi exists.txt yes $ if ls not_exists.txt; then echo yes; else echo no; fi ls: cannot access not_exists.txt: No such file or directory no

可以把 if 关键词的作用当成:判断后面跟着的命令的 $? 是否为 0。

这里 ls 也输出我们不需要的信息,因为我们只让它更新 $? 就够了,要屏蔽掉这些输出:

$ if ls exists.txt &> /dev/null; then echo yes; else echo no; fi yes $ if ls not_exists.txt &> /dev/null; then echo yes; else echo no; fi no

加上了 &> /dev/null 略显丑陋,那有没有其它程序仅仅更新 $? 但没有任何输出呢?

这就是 test 程序了,用 -f 参数表示判断是否存在文件,先检查下:

$ test -f exists.txt; echo $? 0 $ test -f not_exists.txt; echo $? 1

确实无误,套上 if 语句:

$ if test -f exists.txt; then echo yes; else echo no; fi yes $ if test -f not_exists.txt; then echo yes; else echo no; fi no

这里的「文件」指普通文件,如果我要判断其它类型的文件,例如目录、软链接或管道呢?test 也提供判断这些文件类型的参数,可以通过 man test 查看手册。

单方括号语法糖

如果你打开了 test 的 man 后,发现几个眼熟的东西:

SYNOPSIS test EXPRESSION test [ EXPRESSION ] [ ] [ OPTION

bash 给 test 程序特殊优待,可以用另一种语法来编写,也就是把 test 的参数包围在单个方括号里。

test args 也可以写成 [ args ],注意方括号和里面的参数要留个空格,不然提示语法错误,我曾经就被这样折腾了半天。

也可以在直接在提示符里执行,效果跟用普通方法没差别:

$ [ -f exists.txt ]; echo $? 0 $ [ -f not_exists.txt ]; echo $? 1

套上 if 语句:

$ if [ -f exists.txt ]; then echo yes; else echo no; fi yes $ if [ -f not_exists.txt ]; then echo yes; else echo no; fi no

这就是为什么 if 条件部分用的是单个方括号,bash 会把这个写法转换回一般写法,所以说是语法糖。

为什么要提供这个语法糖呢?估计 bash 觉得这样写更好看吧,也让你打少两个字符。但是这个语法糖,迷惑了我好多年,那么小括号有什么用?

单个小括号的作用

单个小括号在 bash 中不像其它语言那样表示分隔符和优先级调整,而是启动一个 subshell 来执行里面的代码,也就是再启动一个 bash 来运行,好处是 subshell 有独立的环境变量。

例如,你在 home 目录,cd 到 /tmp 目录,sleep 5 秒,最后 cd 回 home,但是你会在 sleep 的过程中按 <kbd>Ctrl + c</kbd> 中断。

如果你使用这个命令:

~$ cd /tmp/; sleep 5; cd ~ ^C /tmp$

你会留在 /tmp 目录中,因为最后的 cd ~ 根本没执行。所以如果你希望临时切换别的目录执行某些命令,但又希望中断后回到原来的目录,这个方法就不凑效了。

但是如果你加上小括号:

~$ ( cd /tmp/; sleep 5; ) ^C ~$

这里没有最后的 cd ~,因为多此一举,subshell 有自己的工作目录,相当于你另外开一个终端而已,这样避免一些环境变量被某些代码弄乱。

取反操作

你会想当然认为就是加 ! 符号:

$ [ ! -f exists.txt ]; echo $? 1

确实对了,但是这只是 test 命令里的内部取反,而不是 bash 的,换回一般写法就是:

$ test ! -f exists.txt; echo $? 1

对于 bash 的取反,也就是不正常运行 $? 应该为 0,也是在命令开头加 !

$ ! test -f exists.txt; echo $? 1 $ ! test -f not_exists.txt; echo $? 0

注意 ! 后要有一空格,不然在提示符中会被当成「调用历史命令」解析了,但以脚本执行时不会,反正都加上最好。

于是这样就是蛋疼的双重否定了:

$ ! test ! -f exists.txt; echo $? 0

数字和字符串比较

如果你想比较数字是否相等,想当然写成:

$ [ 3 == 1 ]; echo $? 1 $ [ 3 != 1 ]; echo $? 0

相等也可以用单个等号,用两个比较符合习惯。但是等号左右一定要有空格,否则结果不如你想,因为没空格就是变量赋值!

如果你想比较两个数字,于是这样写:

$ [ 3 > 1 ]; echo $? 0

看起来也如你想的一样,但是如果:

$ [ 3 > 6 ]; echo $? 0

这是搞毛啊?赶紧 ls 一下看看当前目录是不是多了两个名字为 1 和 6 的空文件。

那是因为 > 不是表示大于,而是标准输出重定向,因为标准输出为空,所以只建立了空文件,相当于 touch 命令了。

所以要对 > 符号转义,这样就 OK 了:

$ [ 3 \> 1 ]; echo $? 0 $ [ 3 \> 6 ]; echo $? 1

别高兴得太早,这里还有坑:

$ [ 3 \> 10 ]; echo $? 0

因为这不是按数字比较,而是按字符串,这里 310 在 bash 眼中就是字符串,传给 test 后,test 默认也是当成字符串。

如果显式加上单引号,就清楚了:

$ [ '3' \> '10' ]; echo $? 0

字符串比较就是按 ASCII 编码比较,因为先比较第一个字符,3 比 1 的 ASCII 编码大。

所以上面的几个比较其实全部都是字符串比较,只不过长度一样的话,看起来就是按数字比较。

如果想按数字大小怎么办?可以用 -gt 参数,这样 test 就会把两边当成一个数字看待:

$ [ 3 -gt 1 ]; echo $? 0 $ [ 3 -gt 6 ]; echo $? 1 $ [ 3 -gt 10 ]; echo $? 1

同样,-eq 也是按数字比较:

$ [ 1 == 01 ]; echo $?; 1 $ [ 1 -eq 01 ]; echo $?; 0

复合条件

假如你要再判断某个目录是否存在,又想当然写成:

$ [ -f exists.txt && -d exists_folder ]; echo $? bash: [: missing `]' 2

结果提示漏了右括号,那是因为 && 被 bash 预先解析了,而不是当成 test 的参数传递。

  • && 表示如果左边的命令正常执行了,那么继续执行右边的命令,相当于没有 else 部分的 if 语句简化版。

  • || 表示如果左边的命令不是正常执行了,那么继续执行右边的命令,相当于没有 then 部分的 if 语句(或者 if not)。

从效果看也可以分别当成逻辑与和逻辑或的。

所以上面那条命令以 && 分开看,左边的 [ -f exists.txt 明显是个不完整命令,漏了个 ],当然右边的也漏了 [

修正如下:

$ [ -f exists.txt ] && [ -d exists_folder ]; echo $? 0

换回一般写法也应该是:

$ test -f exists.txt && test -d exists_folder; echo $? 0

使用 || 则是:

$ [ -f not_exists.txt ] || [ -d exists_folder ]; echo $? 0

如果你想先把 &&|| 转义,但 test 不支持这个参数,表示逻辑与和逻辑或的参数分别是 -a-o,所以这样就 OK 了:

$ [ -f exists.txt -a -d exists_folder ]; echo $? 0 $ test -f exists.txt -a -d exists_folder; echo $? 0

这样好处就是只调用了一次 test 程序而不是两次。

双方括号关键词

上面我们用 [ -f exists.txt && -d exists_folder ] 来表示复合条件,结果发现这是一个坑,于是 bash 后来从 ksh 抄来一个特性来填这个坑,结果挖了更大的一个坑。

把单括号换成双括号就 OK 了:

$ [[ -f exists.txt && -d exists_folder ]]; echo $? 0

震惊之情溢于言表,&& 不是隔开两个命令么,怎么用两个方括号又合法了?

前面说说单方括号是语法糖,因为只是 test 命令的另一种写法,bash 最后会调用程序 test,一般就是 /usr/bin/test。

用 type 程序看下类型:

$ type [ [ is a shell builtin $ type test test is a shell builtin

又说这是叫 builtin,坑爹,不过常用命令如 cd、echo 都是这样的。

但是说双方括号是「关键词」,关键词就是 bash 自己内建的语法分析:

$ type [[ [[ is a shell keyword

就因为这是关键词,所以被双方括号包围的代码都有另外一种意义,&&||><</CODE> 这些符号的意义都被改变了,就和其它编程语言的用法一样了。

例如上面的比较大小,对 > 不再需要转义了:

$ [[ 3 > 1 ]]; echo $? 0

但依然是表示按字符串比较,不是按数字:

$ [[ 3 > 10 ]]; echo $? 0

可以看作增强版的 test,因为逻辑与和逻辑或已经可以直接用 &&||,所以 -a-o 就不能用了,其余的参数和 test 基本一样,-f-d 也可以用。

还可以用 =~ 来检查是否匹配正则,简单的就不用劳烦 grep 了:

$ [[ abc =~ a ]]; echo $? 0

因为对 && 那几个符号自动转义了,比较直观,不容易搞错,相对安全,所以推荐优先使用 [[ 而不是 [

双小括号的作用

双小括号的作用就是把里面的代码作为算术表达式来执行,像双方括号一样,里面的代码有另外的意义。

例如给变量赋值:

$ a=1+1; echo $a 1+1 $ (( b = 1 + 1 )); echo $b 2

a1+1 只是一个字符串,而 b 就是一个算术表达式结果。

正是因为是算术表达式,所以比较也是按数字本身而不是字符串:

$ (( 3 > 1 )); echo $? 0 $ (( 3 > 6 )); echo $? 1 $ (( 3 > 10 )); echo $? 1

所以也可以套上 if 语句来用:

$ if (( 3 > 1 )); then echo yes; else echo no; fi yes

真令人抓狂。

一些技巧

可以组合多个命令:

$ if echo abc; echo def; then echo yes; else echo no; fi abc def yes

也可以用管道:

$ if echo abc | grep -q a; then echo yes; else echo no; fi yes

太长或太复杂的话可以用函数封装:

$ function echo_abc() { echo abc | grep -q a; } $ if echo_abc; then echo yes; else echo no; fi yes

如果需要保留命令的标准输出到变量以便再使用,可以直接比较 $? 的值,单纯赋值不改变 $? 的:

$ text=`echo abc | grep a`; $ if [[ $? == 0 ]]; then echo 'text:' $text; else echo no; fi text: abc $ text=`echo abc | grep d`; $ if [[ $? == 0 ]]; then echo 'text:' $text; else echo no; fi no

总结

亲自动手测试了这么多个例子,总算搞把各种堆在一起的概念一一分解开来理解,至少写起来都知道该看参考手册的那一部分了。

感觉依然是:到处都是坑啊!

原文链接 <wbr> http://qixinglu.com/post/understand_bash_if_statement.html

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

推荐阅读更多精彩内容

  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,364评论 0 5
  • .bat脚本基本命令语法 目录 批处理的常见命令(未列举的命令还比较多,请查阅帮助信息) 1、REM 和 :: 2...
    庆庆庆庆庆阅读 8,036评论 1 19
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,719评论 0 10
  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,114评论 0 13
  • Decemeber 28th 2016 Introduction to the course Screenshot...
    简为2016阅读 164评论 0 0