Bash-note-3
Bash-note-3
参考文献:Bash 脚本教程
Bash脚本操作
Bash 脚本入门
脚本(script)就是包含一系列命令的一个文本文件。Shell 读取这个文件,依次执行里面的所有命令,就好像这些命令直接输入到命令行一样。所有能够在命令行完成的任务,都能够用脚本完成。
脚本的好处是可以重复使用,也可以指定在特定场合自动调用,比如系统启动或关闭时自动执行脚本。
Shebang 行
脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!
字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。
#!
后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh
或/bin/bash
。
1 |
|
#!
与脚本解释器之间有没有空格,都是可以的。
如果 Bash 解释器不放在目录/bin
,脚本就无法执行了。为了保险,可以写成下面这样。
1 |
上面命令使用env
命令(这个命令总是在/usr/bin
目录),返回 Bash 可执行文件的位置。env
命令的详细介绍,请看后文。
Shebang 行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。举例来说,脚本是script.sh
,有 Shebang 行的时候,可以直接调用执行。
1 | $ ./script.sh |
上面例子中,script.sh
是脚本文件名。脚本通常使用.sh
后缀名,不过这不是必需的。
如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
1 | $ /bin/sh ./script.sh |
执行权限和路径
前面说过,只要指定了 Shebang 行的脚本,可以直接执行。这有一个前提条件,就是脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。
1 | # 给所有用户执行权限 |
脚本的权限通常设为755
(拥有者有所有权限,其他人有读和执行权限)或者700
(只有拥有者可以执行)。
除了执行权限,脚本调用时,一般需要指定脚本的路径(比如path/script.sh
)。如果将脚本放在环境变量$PATH
指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。
建议在主目录新建一个~/bin
子目录,专门存放可执行脚本,然后把~/bin
加入$PATH
。
1 | export PATH=$PATH:~/bin |
上面命令改变环境变量$PATH
,将~/bin
添加到$PATH
的末尾。可以将这一行加到~/.bashrc
文件里面,然后重新加载一次.bashrc
,这个配置就可以生效了。
1 | $ source ~/.bashrc |
以后不管在什么目录,直接输入脚本文件名,脚本就会执行。
1 | $ script.sh |
上面命令没有指定脚本路径,因为script.sh
在$PATH
指定的目录中。
env 命令
env
命令总是指向/usr/bin/env
文件,或者说,这个二进制文件总是在目录/usr/bin
。
#!/usr/bin/env NAME
这个语法的意思是,让 Shell 查找$PATH
环境变量里面第一个匹配的NAME
。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。
/usr/bin/env bash
的意思就是,返回bash
可执行文件的位置,前提是bash
的路径是在$PATH
里面。其他脚本文件也可以使用这个命令。比如 Node.js 脚本的 Shebang 行,可以写成下面这样。
1 |
env
命令的参数如下。
-i
,--ignore-environment
:不带环境变量启动。-u
,--unset=NAME
:从环境变量中删除一个变量。--help
:显示帮助。--version
:输出版本信息。
下面是一个例子,新建一个不带任何环境变量的 Shell。
1 | $ env -i /bin/sh |
注释
Bash 脚本中,#
表示注释,可以放在行首,也可以放在行尾。
1 | # 本行是注释 |
建议在脚本开头,使用注释说明当前脚本的作用,这样有利于日后的维护。
脚本参数
调用脚本的时候,脚本文件名后面可以带有参数。
1 | $ script.sh word1 word2 word3 |
上面例子中,script.sh
是一个脚本文件,word1
、word2
和word3
是三个参数。
脚本文件内部,可以使用特殊变量,引用这些参数。
$0
:脚本文件名,即script.sh
。$1
~$9
:对应脚本的第一个参数到第九个参数。$#
:参数的总数。$@
:全部的参数,参数之间使用空格分隔。$*
:全部的参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果脚本的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
注意,如果命令是command -o foo bar
,那么-o
是$1
,foo
是$2
,bar
是$3
。
下面是一个脚本内部读取命令行参数的例子。
1 |
|
执行结果如下。
1 | $ ./script.sh a b c |
用户可以输入任意数量的参数,利用for
循环,可以读取每一个参数。
1 |
|
上面例子中,$@
返回一个全部参数的列表,然后使用for
循环遍历。
如果多个参数放在双引号里面,视为一个参数。
1 | $ ./script.sh "a b" |
上面例子中,Bash 会认为"a b"
是一个参数,$1
会返回a b
。注意,返回时不包括双引号。
shift 命令
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推。
while
循环结合shift
命令,也可以读取每一个参数。
1 |
|
上面例子中,shift
命令每次移除当前第一个参数,从而通过while
循环遍历所有参数。
shift
命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1
。
1 | shift 3 |
上面的命令移除前三个参数,原来的$4
变成$1
。
getopts 命令
getopts
命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与while
循环一起使用,取出脚本所有的带有前置连词线(-
)的参数。
1 | getopts optstring name |
它带有两个参数。第一个参数optstring
是字符串,给出脚本所有的连词线参数。比如,某个脚本可以有三个配置项参数-l
、-h
、-a
,其中只有-a
可以带有参数值,而-l
和-h
是开关参数,那么getopts
的第一个参数写成lha:
,顺序不重要。注意,a
后面有一个冒号,表示该参数带有参数值,getopts
规定带有参数值的配置项参数,后面必须带有一个冒号(:
)。getopts
的第二个参数name
是一个变量名,用来保存当前取到的配置项参数,即l
、h
或a
。
下面是一个例子。
1 | while getopts 'lha:' OPTION; do |
上面例子中,while
循环不断执行getopts 'lha:' OPTION
命令,每次执行就会读取一个连词线参数(以及对应的参数值),然后进入循环体。变量OPTION
保存的是,当前处理的那一个连词线参数(即l
、h
或a
)。如果用户输入了没有指定的参数(比如-x
),那么OPTION
等于?
。循环体内使用case
判断,处理这四种不同的情况。
如果某个连词线参数带有参数值,比如-a foo
,那么处理a
参数的时候,环境变量$OPTARG
保存的就是参数值。
注意,只要遇到不带连词线的参数,getopts
就会执行失败,从而退出while
循环。比如,getopts
可以解析command -l foo
,但不可以解析command foo -l
。另外,多个连词线参数写在一起的形式,比如command -lh
,getopts
也可以正确处理。
变量$OPTIND
在getopts
开始执行前是1
,然后每次执行就会加1
。等到退出while
循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1
就是已经处理的连词线参数个数,使用shift
命令将这些参数移除,保证后面的代码可以用$1
、$2
等处理命令的主参数。
配置项参数终止符 --
-
和--
开头的参数,会被 Bash 当作配置项解释。但是,有时它们不是配置项,而是实体参数的一部分,比如文件名叫做-f
或--file
。
1 | $ cat -f |
上面命令的原意是输出文件-f
和--file
的内容,但是会被 Bash 当作配置项解释。
这时就可以使用配置项参数终止符--
,它的作用是告诉 Bash,在它后面的参数开头的-
和--
不是配置项,只能当作实体参数解释。
1 | $ cat -- -f |
上面命令可以正确展示文件-f
和--file
的内容,因为它们放在--
的后面,开头的-
和--
就不再当作配置项解释了。
如果要确保某个变量不会被当作配置项解释,就要在它前面放上参数终止符--
。
1 | $ ls -- $myPath |
上面示例中,--
强制变量$myPath
只能当作实体参数(即路径名)解释。如果变量不是路径名,就会报错。
1 | $ myPath="-l" |
上面例子中,变量myPath
的值为-l
,不是路径。但是,--
强制$myPath
只能作为路径解释,导致报错“不存在该路径”。
下面是另一个实际的例子,如果想在文件里面搜索--hello
,这时也要使用参数终止符--
。
1 | $ grep -- "--hello" example.txt |
上面命令在example.txt
文件里面,搜索字符串--hello
。这个字符串是--
开头,如果不用参数终止符,grep
命令就会把--hello
当作配置项参数,从而报错。
exit 命令
exit
命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。
1 | $ exit |
上面命令中止当前脚本,将最后一条命令的退出状态,作为整个脚本的退出状态。
exit
命令后面可以跟参数,该参数就是退出状态。
1 | # 退出值为0(成功) |
退出时,脚本会返回一个退出值。脚本的退出值,0
表示正常,1
表示发生错误,2
表示用法不对,126
表示不是可执行脚本,127
表示命令没有发现。如果脚本被信号N
终止,则退出值为128 + N
。简单来说,只要退出值非0,就认为执行出错。
下面是一个例子。
1 | if [ $(id -u) != "0" ]; then |
上面的例子中,id -u
命令返回用户的 ID,一旦用户的 ID 不等于0
(根用户的 ID),脚本就会退出,并且退出码为1
,表示运行失败。
exit
与return
命令的差别是,return
命令是函数的退出,并返回一个值给调用者,脚本依然执行。exit
是整个脚本的退出,如果在函数之中调用exit
,则退出函数,并终止脚本执行。
命令执行结果
命令执行结束后,会有一个返回值。0
表示执行成功,非0
(通常是1
)表示执行失败。环境变量$?
可以读取前一个命令的返回值。
利用这一点,可以在脚本中对命令执行结果进行判断。
1 | cd /path/to/somewhere |
上面例子中,cd /path/to/somewhere
这个命令如果执行成功(返回值等于0
),就删除该目录里面的文件,否则退出脚本,整个脚本的返回值变为1
,表示执行失败。
由于if
可以直接判断命令的执行结果,执行相应的操作,上面的脚本可以改写成下面的样子。
1 | if cd /path/to/somewhere; then |
更简洁的写法是利用两个逻辑运算符&&
(且)和||
(或)。
1 | # 第一步执行成功,才会执行第二步 |
source 命令
source
命令用于执行一个脚本,通常用于重新加载一个配置文件。
1 | $ source .bashrc |
source
命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source
命令执行脚本时,不需要export
变量。
1 |
|
上面脚本输出$foo
变量的值。
1 | # 当前 Shell 新建一个变量 foo |
上面例子中,当前 Shell 的变量foo
并没有export
,所以直接执行无法读取,但是source
执行可以读取。
source
命令的另一个用途,是在脚本内部加载外部库。
1 |
|
上面脚本在内部使用source
命令加载了一个外部库,然后就可以在脚本里面,使用这个外部库定义的函数。
source
有一个简写形式,可以使用一个点(.
)来表示。
1 | $ . .bashrc |
别名,alias 命令
alias
命令用来为一个命令指定别名,这样更便于记忆。下面是alias
的格式。
1 | alias NAME=DEFINITION |
上面命令中,NAME
是别名的名称,DEFINITION
是别名对应的原始命令。注意,等号两侧不能有空格,否则会报错。
一个常见的例子是为grep
命令起一个search
的别名。
1 | alias search=grep |
alias
也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today
的命令。
1 | $ alias today='date +"%A, %B %-d, %Y"' |
有时为了防止误删除文件,可以指定rm
命令的别名。
1 | $ alias rm='rm -i' |
上面命令指定rm
命令是rm -i
,每次删除文件之前,都会让用户确认。
alias
定义的别名也可以接受参数,参数会直接传入原始命令。
1 | $ alias echo='echo It says: ' |
上面例子中,别名定义了echo
命令的前两个参数,等同于修改了echo
命令的默认行为。
指定别名以后,就可以像使用其他命令一样使用别名。一般来说,都会把常用的别名写在~/.bashrc
的末尾。另外,只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。
直接调用alias
命令,可以显示所有别名。
1 | $ alias |
unalias
命令可以解除别名。
1 | $ unalias lt |
read 命令
用法
有时,脚本需要在执行过程中,由用户提供一部分数据,这时可以使用read
命令。它将用户的输入存入一个变量,方便后面的代码使用。用户按下回车键,就表示输入结束。
read
命令的格式如下。
1 | read [-options] [variable...] |
上面语法中,options
是参数选项,variable
是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY
会包含用户输入的一整行数据。
下面是一个例子demo.sh
。
1 |
|
上面例子中,先显示一行提示文本,然后会等待用户输入文本。用户输入的文本,存入变量text
,在下一行显示出来。
1 | $ bash demo.sh |
read
可以接受用户输入的多个值。
1 |
|
上面例子中,read
根据用户的输入,同时为两个变量赋值。
如果用户的输入项少于read
命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中。
如果read
命令之后没有定义变量名,那么环境变量REPLY
会包含所有的输入。
1 |
|
上面脚本的运行结果如下。
1 | $ read-single |
read
命令除了读取键盘输入,可以用来读取文件。
1 |
|
上面的例子通过read
命令,读取一个文件的内容。done
命令后面的定向符<
,将文件内容导向read
命令,每次读取一行,存入变量myline
,直到文件读取完毕。
参数
read
命令的参数如下。
(1)-t 参数
read
命令的-t
参数,设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
1 |
|
上面例子中,输入命令会等待3秒,如果用户超过这个时间没有输入,这个命令就会执行失败。if
根据命令的返回值,转入else
代码块,继续往下执行。
环境变量TMOUT
也可以起到同样作用,指定read
命令等待用户输入的时间(单位为秒)。
1 | $ TMOUT=3 |
上面例子也是等待3秒,如果用户还没有输入,就会超时。
(2)-p 参数
-p
参数指定用户输入的提示信息。
1 | read -p "Enter one or more values > " |
上面例子中,先显示Enter one or more values >
,再接受用户的输入。
(3)-a 参数
-a
参数把用户的输入赋值给一个数组,从零号位置开始。
1 | $ read -a people |
上面例子中,用户输入被赋值给一个数组people
,这个数组的2号成员就是dodo
。
(4)-n 参数
-n
参数指定只读取若干个字符作为变量值,而不是整行读取。
1 | $ read -n 3 letter |
上面例子中,变量letter
只包含3个字母。
(5)-e 参数
-e
参数允许用户输入的时候,使用readline
库提供的快捷键,比如自动补全。具体的快捷键可以参阅《行操作》一章。
1 |
|
上面例子中,read
命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read
命令的输入默认不支持readline
库的功能。-e
参数就可以允许用户使用自动补全。
(6)其他参数
-d delimiter
:定义字符串delimiter
的第一个字符作为用户输入的结束,而不是一个换行符。-r
:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符。-s
:使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息。-u fd
:使用文件描述符fd
作为输入。
IFS 变量
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS
的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如果把IFS
定义成冒号(:
)或分号(;
),就可以分隔以这两个符号分隔的值,这对读取文件很有用。
1 |
|
上面例子中,IFS
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效,该命令执行后IFS
会自动恢复原来的值。如果不写在一行,就要采用下面的写法。
1 | OLD_IFS="$IFS" |
另外,上面例子中,<<<
是 Here 字符串,用于将变量值转为标准输入,因为read
命令只能解析标准输入。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
1 |
|
上面的命令可以逐行读取文件,每一行存入变量line
,打印出来以后再读取下一行。
条件判断
本章介绍 Bash 脚本的条件判断语法。
if 结构
if
是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下。
1 | if commands; then |
这个命令分成三个部分:if
、elif
和else
。其中,后两个部分是可选的。
if
关键字后面是主要的判断条件,elif
用来添加在主条件不成立时的其他判断条件,else
则是所有条件都不成立时要执行的部分。
1 | if test $USER = "foo"; then |
上面的例子中,判断条件是环境变量$USER
是否等于foo
,如果等于就输出Hello foo.
,否则输出其他内容。
if
和then
写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。
1 | if true |
上面的例子中,true
和false
是两个特殊命令,前者代表操作成功,后者代表操作失败。if true
意味着命令部分总是会执行,if false
意味着命令部分永远不会执行。
除了多行的写法,if
结构也可以写成单行。
1 | $ if true; then echo 'hello world'; fi |
注意,if
关键字后面也可以是一条命令,该条命令执行成功(返回值0
),就意味着判断条件成立。
1 | $ if echo 'hi'; then echo 'hello world'; fi |
上面命令中,if
后面是一条命令echo 'hi'
。该命令会执行,如果返回值是0
,则执行then
的部分。
if
后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0
,就会执行then
的部分。
1 | $ if false; true; then echo 'hello world'; fi |
上面例子中,if
后面有两条命令(false;true;
),第二条命令(true
)决定了then
的部分是否会执行。
elif
部分可以有多个。
1 |
|
上面例子中,如果用户输入3
,就会连续判断3次。
test 命令
if
结构的判断条件,一般使用test
命令,有三种形式。
1 | # 写法一 |
上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。
上面的expression
是一个表达式。这个表达式为真,test
命令执行成功(返回值为0
);表达式为伪,test
命令执行失败(返回值为1
)。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。
1 | $ test -f /etc/hosts |
上面的例子中,test
命令采用两种写法,判断/etc/hosts
文件是否存在,这两种写法是等价的。命令执行后,返回值为0
,表示该文件确实存在。
实际上,[
这个字符是test
命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。
下面把test
命令的三种形式,用在if
结构中,判断一个文件是否存在。
1 | # 写法一 |
判断表达式
if
关键字后面,跟的是一个命令。这个命令可以是test
命令,也可以是其他命令。命令的返回值为0
表示判断成立,否则表示不成立。因为这些命令主要是为了得到返回值,所以可以视为表达式。
常用的判断表达式有下面这些。
文件判断
以下表达式用来判断文件状态。
[ -a file ]
:如果 file 存在,则为true
。[ -b file ]
:如果 file 存在并且是一个块(设备)文件,则为true
。[ -c file ]
:如果 file 存在并且是一个字符(设备)文件,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -g file ]
:如果 file 存在并且设置了组 ID,则为true
。[ -G file ]
:如果 file 存在并且属于有效的组 ID,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -k file ]
:如果 file 存在并且设置了它的“sticky bit”,则为true
。[ -L file ]
:如果 file 存在并且是一个符号链接,则为true
。[ -N file ]
:如果 file 存在并且自上次读取后已被修改,则为true
。[ -O file ]
:如果 file 存在并且属于有效的用户 ID,则为true
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -S file ]
:如果 file 存在且是一个网络 socket,则为true
。[ -t fd ]
:如果 fd 是一个文件描述符,并且重定向到终端,则为true
。 这可以用来判断是否重定向了标准输入/输出/错误。[ -u file ]
:如果 file 存在并且设置了 setuid 位,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为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 |
|
上面代码中,$FILE
要放在双引号之中,这样可以防止变量$FILE
为空,从而出错。因为$FILE
如果为空,这时[ -e $FILE ]
就变成[ -e ]
,这会被判断为真。而$FILE
放在双引号之中,[ -e "$FILE" ]
就变成[ -e "" ]
,这会被判断为伪。
字符串判断
以下表达式用来判断字符串。
[ string ]
:如果string
不为空(长度大于0),则判断为真。[ -n string ]
:如果字符串string
的长度大于零,则判断为真。[ -z string ]
:如果字符串string
的长度为零,则判断为真。[ string1 = string2 ]
:如果string1
和string2
相同,则判断为真。[ string1 == string2 ]
等同于[ string1 = string2 ]
。[ string1 != string2 ]
:如果string1
和string2
不相同,则判断为真。[ string1 '>' string2 ]
:如果按照字典顺序string1
排列在string2
之后,则判断为真。[ string1 '<' string2 ]
:如果按照字典顺序string1
排列在string2
之前,则判断为真。
注意,test
命令内部的>
和<
,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符。
下面是一个示例。
1 |
|
上面代码中,首先确定$ANSWER
字符串是否为空。如果为空,就终止脚本,并把退出状态设为1
。注意,这里的echo
命令把错误信息There is no answer.
重定向到标准错误,这是处理错误信息的常用方法。如果$ANSWER
字符串不为空,就判断它的值是否等于yes
、no
或者maybe
。
注意,字符串判断时,变量要放在双引号之中,比如[ -n "$COUNT" ]
,否则变量替换成字符串以后,test
命令可能会报错,提示参数过多。另外,如果不放在双引号之中,变量为空时,命令会变成[ -n ]
,这时会判断为真。如果放在双引号之中,[ -n "" ]
就判断为伪。
整数判断
下面的表达式用于判断整数。
[ 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 |
|
上面例子中,先判断变量$INT
是否为空,然后判断是否为0
,接着判断正负,最后通过求余数判断奇偶。
正则判断
[[ expression ]]
这种判断形式,支持正则表达式。
1 | [[ string1 =~ regex ]] |
上面的语法中,regex
是一个正则表示式,=~
是正则比较运算符。
下面是一个例子。
1 |
|
上面代码中,先判断变量INT
的字符串形式,是否满足^-?[0-9]+$
的正则模式,如果满足就表明它是一个整数。
test 判断的逻辑运算
通过逻辑运算,可以把多个test
判断表达式结合起来,创造更复杂的判断。三种逻辑运算AND
,OR
,和NOT
,都有自己的专用符号。
AND
运算:符号&&
,也可使用参数-a
。OR
运算:符号||
,也可使用参数-o
。NOT
运算:符号!
。
下面是一个AND
的例子,判断整数是否在某个范围之内。
1 |
|
上面例子中,&&
用来连接两个判断条件:大于等于$MIN_VAL
,并且小于等于$MAX_VAL
。
使用否定操作符!
时,最好用圆括号确定转义的范围。
1 | if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then |
上面例子中,test
命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。
算术判断
Bash 还提供了((...))
作为算术条件,进行算术运算的判断。
1 | if ((3 > 2)); then |
上面代码执行后,会打印出true
。
注意,算术判断不需要使用test
命令,而是直接使用((...))
结构。这个结构的返回值,决定了判断的真伪。
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
1 | $ if ((1)); then echo "It is true."; fi |
上面例子中,((1))
表示判断成立,((0))
表示判断不成立。
算术条件((...))
也可以用于变量赋值。
1 | $ if (( foo = 5 ));then echo "foo is $foo"; fi |
上面例子中,(( foo = 5 ))
完成了两件事情。首先把5
赋值给变量foo
,然后根据返回值5
,判断条件为真。
注意,赋值语句返回等号右边的值,如果返回的是0
,则判断为假。
1 | $ if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi |
下面是用算术条件改写的数值判断脚本。
1 |
|
只要是算术表达式,都能用于((...))
语法,详见《Bash 的算术运算》一章。
普通命令的逻辑运算
如果if
结构使用的不是test
命令,而是普通命令,比如上一节的((...))
算术运算,或者test
命令与普通命令混用,那么可以使用 Bash 的命令控制操作符&&
(AND)和||
(OR),进行多个命令的逻辑运算。
1 | $ command1 && command2 |
对于&&
操作符,先执行command1
,只有command1
执行成功后, 才会执行command2
。对于||
操作符,先执行command1
,只有command1
执行失败后, 才会执行command2
。
1 | $ mkdir temp && cd temp |
上面的命令会创建一个名为temp
的目录,执行成功后,才会执行第二个命令,进入这个目录。
1 | $ [ -d temp ] || mkdir temp |
上面的命令会测试目录temp
是否存在,如果不存在,就会执行第二个命令,创建这个目录。这种写法非常有助于在脚本中处理错误。
1 | [ ! -d temp ] && exit 1 |
上面的命令中,如果temp
子目录不存在,脚本会终止,并且返回值为1
。
下面就是if
与&&
结合使用的写法。
1 | if [ condition ] && [ condition ]; then |
下面是一个示例。
1 |
|
上面的例子只有在指定文件里面,同时存在搜索词word1
和word2
,就会执行if
的命令部分。
下面的示例演示如何将一个&&
判断表达式,改写成对应的if
结构。
1 | [[ -d "$dir_name" ]] && cd "$dir_name" && rm * |
case 结构
case
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下。
1 | case expression in |
上面代码中,expression
是一个表达式,pattern
是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号(;
)结尾。
1 |
|
上面例子中,最后一条匹配语句的模式是*
,这个通配符可以匹配其他字符和没有输入字符的情况,类似if
的else
部分。
下面是另一个例子。
1 |
|
上面的例子判断当前是什么操作系统。
case
的匹配模式可以使用各种通配符,下面是一些例子。
a)
:匹配a
。a|b)
:匹配a
或b
。[[:alpha:]])
:匹配单个字母。???)
:匹配3个字符的单词。*.txt)
:匹配.txt
结尾。*)
:匹配任意输入,通过作为case
结构的最后一个模式。
1 |
|
上面例子中,使用通配符[[:lower:]] | [[:upper:]]
匹配字母,[0-9]
匹配数字。
Bash 4.0之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&
终止每个条件块。
1 |
|
执行上面的脚本,会得到下面的结果。
1 | $ test.sh |
可以看到条件语句结尾添加了;;&
以后,在匹配一个条件之后,并没有退出case
结构,而是继续判断下一个条件。
循环
Bash 提供三种循环语法for
、while
和until
。
while 循环
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
1 | while condition; do |
上面代码中,只要满足条件condition
,就会执行命令commands
。然后,再次判断是否满足条件condition
,只要满足,就会一直执行下去。只有不满足条件,才会退出循环。
循环条件condition
可以使用test
命令,跟if
结构的判断条件写法一致。
1 |
|
上面例子中,只要变量$number
小于10,就会不断加1,直到$number
等于10,然后退出循环。
关键字do
可以跟while
不在同一行,这时两者之间不需要使用分号分隔。
1 | while true |
上面的例子会无限循环,可以按下 Ctrl + c 停止。
while
循环写成一行,也是可以的。
1 | $ while true; do echo 'Hi, while looping ...'; done |
while
的条件部分也可以是执行一个命令。
1 | $ while echo 'ECHO'; do echo 'Hi, while looping ...'; done |
上面例子中,判断条件是echo 'ECHO'
。由于这个命令总是执行成功,所以上面命令会产生无限循环。
while
的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。
1 | $ while true; false; do echo 'Hi, looping ...'; done |
上面代码运行后,不会有任何输出,因为while
的最后一个命令是false
。
until 循环
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
1 | until condition; do |
关键字do
可以与until
不写在同一行,这时两者之间不需要分号分隔。
1 | until condition |
下面是一个例子。
1 | $ until false; do echo 'Hi, until looping ...'; done |
上面代码中,until
的部分一直为false
,导致命令无限运行,必须按下 Ctrl + c 终止。
1 |
|
上面例子中,只要变量number
小于10,就会不断加1,直到number
大于等于10,就退出循环。
until
的条件部分也可以是一个命令,表示在这个命令执行成功之前,不断重复尝试。
1 | until cp $1 $2; do |
上面例子表示,只要cp $1 $2
这个命令执行不成功,就5秒钟后再尝试一次,直到成功为止。
until
循环都可以转为while
循环,只要把条件设为否定即可。上面这个例子可以改写如下。
1 | while ! cp $1 $2; do |
一般来说,until
用得比较少,完全可以统一都使用while
。
for…in 循环
for...in
循环用于遍历列表的每一项。
1 | for variable in list |
上面语法中,for
循环会依次从list
列表中取出一项,作为变量variable
,然后在循环体中进行处理。
关键词do
可以跟for
写在同一行,两者使用分号分隔。
1 | for variable in list; do |
下面是一个例子。
1 |
|
上面例子中,word1 word2 word3
是一个包含三个单词的列表,变量i
依次等于word1
、word2
、word3
,命令echo $i
则会相应地执行三次。
列表可以由通配符产生。
1 | for i in *.png; do |
上面例子中,*.png
会替换成当前目录中所有 PNG 图片文件,变量i
会依次等于每一个文件。
列表也可以通过子命令产生。
1 |
|
上面例子中,cat ~/.bash_profile
命令会输出~/.bash_profile
文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
in list
的部分可以省略,这时list
默认等于脚本的所有参数$@
。但是,为了可读性,最好还是不要省略,参考下面的例子。
1 | for filename; do |
在函数体中也是一样的,for...in
循环省略in list
的部分,则list
默认等于函数的所有参数。
for 循环
for
循环还支持 C 语言的循环语法。
1 | for (( expression1; expression2; expression3 )); do |
上面代码中,expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
它等同于下面的while
循环。
1 | (( expression1 )) |
下面是一个例子。
1 | for (( i=0; i<5; i=i+1 )); do |
上面代码中,初始化变量i
的值为0,循环执行的条件是i
小于5。每次循环迭代结束时,i
的值加1。
for
条件部分的三个语句,都可以省略。
1 | for ((;;)) |
上面脚本会反复读取命令行输入,直到用户输入了一个点(.
)为止,才会跳出循环。
break,continue
Bash 提供了两个内部命令break
和continue
,用来在循环内部跳出循环。
break
命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。
1 |
|
上面例子只会打印3行结果。一旦变量$number
等于3,就会跳出循环,不再继续执行。
continue
命令立即终止本轮循环,开始执行下一轮循环。
1 |
|
上面例子中,只要用户输入的文件不存在,continue
命令就会生效,直接进入下一轮循环(让用户重新输入文件名),不再执行后面的打印语句。
select 结构
select
结构主要用来生成简单的菜单。它的语法与for...in
循环基本一致。
1 | select name |
Bash 会对select
依次进行下面的处理。
select
生成一个菜单,内容是列表list
的每一项,并且每一项前面还有一个数字编号。- Bash 提示用户选择一项,输入它的编号。
- 用户输入以后,Bash 会将该项的内容存在变量
name
,该项的编号存入环境变量REPLY
。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。 - 执行命令体
commands
。 - 执行结束后,回到第一步,重复这个过程。
下面是一个例子。
1 |
|
执行上面的脚本,Bash 会输出一个品牌的列表,让用户选择。
1 | $ ./select.sh |
如果用户没有输入编号,直接按回车键。Bash 就会重新输出一遍这个菜单,直到用户按下Ctrl + c
,退出执行。
select
可以与case
结合,针对不同项,执行不同的命令。
1 |
|
上面例子中,case
针对用户选择的不同项,执行不同的命令。
Bash 函数
本章介绍 Bash 函数的用法。
简介
函数(function)是可以重复使用的代码片段,有利于代码的复用。它与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
Bash 函数定义的语法有两种。
1 | # 第一种 |
上面代码中,fn
是自定义的函数名,函数代码就写在大括号之中。这两种写法是等价的。
下面是一个简单函数的例子。
1 | hello() { |
上面代码中,函数体里面的$1
表示函数调用时的第一个参数。
调用时,就直接写函数名,参数跟在函数名后面。
1 | $ hello world |
下面是一个多行函数的例子,显示当前日期时间。
1 | today() { |
删除一个函数,可以使用unset
命令。
1 | unset -f functionName |
查看当前 Shell 已经定义的所有函数,可以使用declare
命令。
1 | $ declare -f |
上面的declare
命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合more
或less
使用。
declare
命令还支持查看单个函数的定义。
1 | $ declare -f functionName |
declare -F
可以输出所有已经定义的函数名,不含函数体。
1 | $ declare -F |
参数变量
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
$1
~$9
:函数的第一个到第9个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果函数的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
下面是一个示例脚本test.sh
。
1 |
|
运行该脚本,结果如下。
1 | $ bash test.sh |
上面例子中,由于函数alice
只有第一个和第二个参数,所以第三个和第四个参数为空。
下面是一个日志函数的例子。
1 | function log_msg { |
使用方法如下。
1 | $ log_msg "This is sample log message" |
return 命令
return
命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
1 | function func_return_value { |
函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?
拿到返回值。
1 | $ func_return_value |
return
后面不跟参数,只用于返回也是可以的。
1 | function name { |
全局变量和局部变量,local 命令
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
1 | # 脚本 test.sh |
上面脚本的运行结果如下。
1 | $ bash test.sh |
上面例子中,变量$foo
是在函数fn
内部声明的,函数体外也可以读取。
函数体内不仅可以声明全局变量,还可以修改全局变量。
1 |
|
上面代码执行后,输出的变量$foo
值为2。
函数里面可以用local
命令声明局部变量。
1 |
|
上面脚本的运行结果如下。
1 | $ bash test.sh |
上面例子中,local
命令声明的$foo
变量,只在函数体内有效,函数体外没有定义。
- Title: Bash-note-3
- Author: Charles
- Created at : 2023-02-18 08:08:20
- Updated at : 2023-11-05 21:36:19
- Link: https://charles2530.github.io/2023/02/18/bash-note-3/
- License: This work is licensed under CC BY-NC-SA 4.0.