参考链接:
https://wangdoc.com/bash/index.html
控制多条命令的继发
命令的组合符&&
和||
,多个命令之间的继发关系可以通过三种符号去控制。
Command1 && Command2
上面命令的意思是,如果Command1
命令运行成功,则继续运行Command2
命令。
Command1 || Command2
上面命令的意思是,如果Command1
命令运行失败,则继续运行Command2
命令。
Command1 ; Command2
上面命令的意思是,不管Command1
有没有成功 ,继续运行Command2
命令。
type命令
利用type可以判断一个命令是内置命令还是外部程序
root@2010104-0289:~# type ls
ls is aliased to `ls --color=auto'
root@2010104-0289:~# type echo
echo is a shell builtin
root@2010104-0289:~# type go
go is /root/go/bin/go
type
命令的-t
参数,可以返回一个命令的类型:别名(alias),关键词(keyword),函数(function),内置命令(builtin)和文件(file)。
快捷键
-
Ctrl + L
:清除屏幕并将当前行移到页面顶部。 -
Ctrl + C
:中止当前正在执行的命令。 -
Shift + PageUp
:向上滚动。 -
Shift + PageDown
:向下滚动。 -
Ctrl + U
:从光标位置删除到行首。 -
Ctrl + K
:从光标位置删除到行尾。 -
Ctrl + D
:关闭 Shell 会话。 -
↑
,↓
:浏览已执行命令的历史记录。
模式扩展
Bash 一共提供八种扩展。
- 波浪线扩展
-
?
字符扩展 -
*
字符扩展 - 方括号扩展
- 大括号扩展
- 变量扩展
- 子命令扩展
- 算术扩展
Bash 允许用户关闭扩展。
$ set -o noglob
# 或者
$ set -f
下面的命令可以重新打开扩展。
$ set +o noglob
# 或者
$ set +f
波浪线~
扩展
波浪线~
会自动扩展成当前用户的主目录。
$ echo ~
/home/me
~user
表示扩展成用户user
的主目录。
$ echo ~foo
/home/foo
$ echo ~root
/root
如果用户不存在的话。
$ echo ~nonExistedUser
~nonExistedUser
~+
会扩展成当前所在的目录,等同于pwd
命令。
$ cd ~/foo
$ echo ~+
/home/me/foo
?扩展
?
字符代表文件路径里面的任意单个字符,不包括空字符。比如,Data???
匹配所有Data
后面跟着三个字符的文件名。
# 存在文件 a.txt 和 b.txt
$ ls ?.txt
a.txt b.txt
?
字符扩展属于文件名扩展,只有文件确实存在的前提下,才会发生扩展。如果文件不存在,扩展就不会发生。
# 当前目录有 a.txt 文件
$ echo ?.txt
a.txt
# 当前目录为空目录
$ echo ?.txt
?.txt
* 字符扩展
*
字符代表文件路径里面的任意数量的任意字符,包括零个字符。
# 存在文件 a.txt、b.txt 和 ab.txt
$ ls *.txt
a.txt b.txt ab.txt
注意,*
不会匹配隐藏文件(以.
开头的文件),即ls *
不会输出隐藏文件。
如果要匹配隐藏文件,需要写成.*
。
# 显示所有隐藏文件
$ echo .*
如果要匹配隐藏文件,同时要排除.
和..
这两个特殊的隐藏文件,可以与方括号扩展结合使用,写成.[!.]*
。
$ echo .[!.]*
注意,*
字符扩展属于文件名扩展,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。
# 当前目录不存在 c 开头的文件
$ echo c*.txt
c*.txt
*
只匹配当前目录,不会匹配子目录。
# 子目录有一个 a.txt
# 无效的写法
$ ls *.txt
# 有效的写法
$ ls */*.tx
Bash 4.0 引入了一个参数globstar
,当该参数打开时,允许**
匹配零个或多个子目录。
[]方括号扩展
方括号扩展的形式是[...]
,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。括号之中的任意一个字符。比如,[aeiou]
可以匹配五个元音字母中的任意一个。
# 存在文件 a.txt 和 b.txt
$ ls [ab].txt
a.txt b.txt
# 只存在文件 a.txt
$ ls [ab].txt
a.txt
方括号扩展还有两种变体:[^...]
和[!...]
。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc]
或[!abc]
表示匹配除了a
、b
、c
以外的字符。
# 存在 aaa、bbb、aba 三个文件
$ ls ?[!a]?
aba bbb
上面命令中,[!a]
表示文件名第二个字符不是a
的文件名,所以返回了aba
和bbb
两个文件。
注意,如果需要匹配[
字符,可以放在方括号内,比如[[aeiou]
。如果需要匹配连字号-
,只能放在方括号内部的开头或结尾,比如[-aeiou]
或[aeiou-]
。
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围。比如,[a-c]
等同于[abc]
,[0-9]
匹配[0123456789]
。
# 存在文件 a.txt、b.txt 和 c.txt
$ ls [a-c].txt
a.txt
b.txt
c.txt
# 存在文件 report1.txt、report2.txt 和 report3.txt
$ ls report[0-9].txt
report1.txt
report2.txt
report3.txt
...
这种简写形式有一个否定形式[!start-end]
,表示匹配不属于这个范围的字符。比如,[!a-zA-Z]
表示匹配非英文字母的字符。
$ echo report[!1–3].txt
report4.txt report5.txt
大括号扩展
大括号扩展{...}
表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}
扩展成1 2 3
。
$ echo {1,2,3}
1 2 3
$ echo d{a,e,i,u,o}g
dag deg dig dug dog
$ echo Front-{A,B,C}-Back
Front-A-Back Front-B-Back Front-C-Back
注意,大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。
另一个需要注意的地方是,大括号内部的逗号前后不能有空格。否则,大括号扩展会失效。
$ echo {1 , 2}
{1 , 2}
大括号可以嵌套。
$ echo {j{p,pe}g,png}
jpg jpeg png
$ echo a{A{1,2},B{3,4}}b
aA1b aA2b aB3b aB4b
大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。
$ echo {cat,d*}
cat dawg dg dig dog doug dug
上面例子中,会先进行大括号扩展,然后进行*
扩展。
大括号扩展有一个简写形式{start..end}
,表示扩展成一个连续序列。比如,{a..z}
可以扩展成26个小写英文字母。
大括号扩展有一个简写形式{start..end}
,表示扩展成一个连续序列。比如,{a..z}
可以扩展成26个小写英文字母。
$ echo {a..c}
a b c
$ echo d{a..d}g
dag dbg dcg ddg
$ echo {1..4}
1 2 3 4
$ echo Number_{1..5}
Number_1 Number_2 Number_3 Number_4 Number_5
这种简写形式支持逆序。
$ echo {c..a}
c b a
$ echo {5..1}
5 4 3 2 1
这种简写形式可以嵌套使用,形成复杂的扩展。
$ echo .{mp{3..4},m4{a,b,p,v}}
.mp3 .mp4 .m4a .m4b .m4p .m4v
大括号扩展的常见用途为新建一系列目录。
$ mkdir {2007..2009}-{01..12}
上面命令会新建36个子目录,每个子目录的名字都是”年份-月份“。
这个写法的另一个常见用途,是直接用于for
循环。
for i in {1..4}
do
echo $i
done
上面例子会循环4次。
如果整数前面有前导0
,扩展输出的每一项都有前导0
。
$ echo {01..5}
01 02 03 04 05
$ echo {001..5}
001 002 003 004 005
这种简写形式还可以使用第二个双点号(start..end..step
),用来指定扩展的步长。
$ echo {0..8..2}
0 2 4 6 8
上面代码将0
扩展到8
,每次递增的长度为2
,所以一共输出5个数字。
多个简写形式连用,会有循环处理的效果。
$ echo {a..c}{1..3}
a1 a2 a3 b1 b2 b3 c1 c2 c3
变量扩展
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值,
$ echo $SHELL
/bin/bash
变量名除了放在美元符号后面,也可以放在${}
里面。
$ echo ${SHELL}
/bin/bash
${!string*}
或${!string@}
返回所有匹配给定字符串string
的变量名。
$ echo ${!S*}
SECONDS SHELL SHELLOPTS SHLVL SSH_AGENT_PID SSH_AUTH_SOCK
子命令扩展
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。
$ echo $(date)
Tue Jan 28 00:01:13 CST 2020
上面例子中,$(date)
返回date
命令的运行结果。
算术扩展
$((...))
可以扩展成整数运算的结果,详见《Bash 的算术运算》一章。
$ echo $((2 + 2))
4
字符类
[[:class:]]
表示一个字符类,扩展成某一类特定字符之中的一个。常用的字符类如下。
-
[[:alnum:]]
:匹配任意英文字母与数字 -
[[:alpha:]]
:匹配任意英文字母 -
[[:blank:]]
:空格和 Tab 键。 -
[[:cntrl:]]
:ASCII 码 0-31 的不可打印字符。 -
[[:digit:]]
:匹配任意数字 0-9。 -
[[:graph:]]
:A-Z、a-z、0-9 和标点符号。 -
[[:lower:]]
:匹配任意小写字母 a-z。 -
[[:print:]]
:ASCII 码 32-127 的可打印字符。 -
[[:punct:]]
:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。 -
[[:space:]]
:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。 -
[[:upper:]]
:匹配任意大写字母 A-Z。 -
[[:xdigit:]]
:16进制字符(A-F、a-f、0-9)。
字符类的第一个方括号后面,可以加上感叹号!
,表示否定。比如,[![:digit:]]
匹配所有非数字。
$ echo [![:digit:]]*
字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出。
注意:文件名可以使用通配符。
Bash 允许文件名使用通配符,即文件名包括特殊字符。这时引用文件名,需要把文件名放在单引号里面。
$ touch 'fo*'
$ ls
fo*
上面代码创建了一个fo*
文件,这时*
就是文件名的一部分。
转义
某些字符在bash中含有特殊含义:针对这些需要转义。
在echo中要打印不可打印的字符时,要增加-e参数。
$ echo a\tb
atb
$ echo -e "a\tb"
a b
利用\,可以将一行命令写成多行。
单引号
Bash 允许字符串放在单引号或双引号之中,加以引用。
单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*
)、美元符号($
)、反斜杠(\
)等。
$ echo '*'
*
$ echo '$USER'
$USER
$ echo '$((2+2))'
$((2+2))
$ echo '$(echo foo)'
$(echo foo)
由于反斜杠在单引号里面变成了普通字符,所以如果单引号之中,还要使用单引号,不能使用转义,需要在外层的单引号前面加上一个美元符号($
),然后再对里层的单引号转义。
# 不正确
$ echo it's
# 不正确
$ echo 'it\'s'
# 正确
$ echo $'it\'s'
不过,更合理的方法是改在双引号之中使用单引号。
$ echo "it's"
it's
双引号
双引号比单引号宽松,可以保留大部分特殊字符的本来含义,但是三个字符除外:美元符号($
)、反引号(`)和反斜杠(\)。也就是说,这三个字符在双引号之中,会被 Bash 自动扩展。
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
Mon Jan 27 13:33:18 CST 2020
$ echo "I'd say: \"hello!\""
I'd say: "hello!"
$ echo "\\"
\
双引号的另一个常见的使用场合是,文件名包含空格。这时就必须使用双引号,将文件名放在里面。
$ ls "two words.txt"
上面命令中,two words.txt
是一个包含空格的文件名,否则就会被 Bash 当作两个文件。
双引号还有一个作用,就是保存原始命令的输出格式。
# 单行输出
$ echo $(cal)
一月 2020 日 一 二 三 四 五 六 1 2 3 ... 31
# 原始格式输出
$ echo "$(cal)"
一月 2020
日 一 二 三 四 五 六
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
上面例子中,如果$(cal)
不放在双引号之中,echo
就会将所有结果以单行输出,丢弃了所有原始的格式。
变量
Bash 变量分成环境变量和自定义变量两类。
环境变量
环境变脸是Bash自带的变量,进入shell的时候就已经定义好了的,可以直接使用。通常是系统定义好的,也可以由用户从父shell传入子shell。
env
命令或printenv
命令,可以显示所有环境变量。
注意,Bash 变量名区分大小写,HOME
和home
是两个不同的变量。
set
命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。
创建变量
命名规则
- 字母、数字和下划线字符组成。
- 第一个字符必须是一个字母或一个下划线,不能是数字。
- 不允许出现空格和标点符号。
变量声明的语法如下。
variable=value
Bash 没有数据类型的概念,所有的变量值都是字符串。
读取变量
读取变量的时候,直接在变量名前加上$
就可以了。
$ foo=bar
$ echo $foo
bar
读取变量的时候,变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况。
$ a=foo
$ echo $a_file
$ echo ${a}_file
foo_file
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
删除变量
unset
命令用来删除一个变量
这个命令不是很好用,因为不存在的变量相当于空字符串。
所以删除变量可以把这个变量设置为空字符串。
输出变量
利用export
可以用来向子Shell输出变量。
NAME=foo
export NAME
上面命令输出了变量NAME
。变量的赋值和输出也可以在一个步骤中完成。
export NAME=value
上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME
。
子 Shell 如果修改继承的变量,不会影响父 Shell。
# 输出变量 $foo
$ export foo=bar
# 新建子 Shell
$ bash
# 读取 $foo
$ echo $foo
bar
# 修改继承的变量
$ foo=baz
# 退出子 Shell
$ exit
# 读取 $foo
$ echo $foo
bar
特殊变量
$?
$?
为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0
,表示上一个命令执行成功;如果是非零,上一个命令执行失败。
$ ls doesnotexist
ls: doesnotexist: No such file or directory
$ echo $?
1
上面例子中,ls
命令查看一个不存在的文件,导致报错。$?
为1,表示上一个命令执行失败。
$$
$$为当前Shell的进程ID。
$ echo $$
10662
这个特殊的变量可以用来命名临时文件。
LOGFILE=/tmp/output_log.$$
$_
$_
为上一个命令的最后一个参数.
$ grep dictionary /usr/share/dict/words
dictionary
$ echo $_
/usr/share/dict/words
$!
$!
为最近一个后台执行的异步命令的进程 ID。
$ firefox &
[1] 11064
$ echo $!
11064
上面例子中,firefox
是后台运行的命令,$!
返回该命令的进程 ID。
$0
表示当前shell的名称
$-
表示当前shell的启动参数
#
表示脚本的参数数量
变量的默认值
bash提供四个特殊的语法,跟变量的默认值有关,目的是保证变量不为空.
${varname:-word}
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则返回word
。它的目的是返回一个默认值,比如${count:-0}
表示变量count
不存在时返回0
。
${varname:=word}
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则将它设为word
,并且返回word
。它的目的是设置变量的默认值,比如${count:=0}
表示变量count
不存在时返回0
,且将count
设为0
。
${varname:+word}
上面语法的含义是,如果变量名存在且不为空,则返回word
,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}
表示变量count
存在时返回1
(表示true
),否则返回空值。
${varname:?message}
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则打印出varname: message
,并中断脚本的执行。如果省略了message
,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}
表示变量count
未定义时就中断执行,抛出错误,返回给定的报错信息undefined!
。
上面四种语法如果用在脚本中,变量名的部分可以用到数字1
到9
,表示脚本的参数。
filename=${1:?"filename missing."}
上面代码出现在脚本中,1
表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
declare
declare
命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。
它的语法形式如下。
declare OPTION VARIABLE=value
declare
命令的主要参数(OPTION)如下。
-
-a
:声明数组变量。 -
-f
:输出所有函数定义。 -
-F
:输出所有函数名。 -
-i
:声明整数变量。 -
-l
:声明变量为小写字母。 -
-p
:查看变量信息。 -
-r
:声明只读变量。 -
-u
:声明变量为大写字母。 -
-x
:该变量输出为环境变量。
-i
-i
参数声明整数变量以后,可以直接进行数学运算。
# declare -i var1=2 var2=3
# echo $var1*$var2
2*3
# echo $(($var1*$var2))
6
# result=var1*var2
# echo $result
var1*var2
# declare -i result
# echo $result
var1*var2
# result=var1*var2
# echo $result
6
-x
-x
参数等同于export
命令,可以输出一个变量为子 Shell 的环境变量。
$ declare -x foo
# 等同于
$ export foo
-r
-r
参数可以声明只读变量,无法改变变量值,也不能unset
变量。
$ declare -r bar=1
$ bar=2
bash: bar:只读变量
$ echo $?
1
$ unset bar
bash: bar:只读变量
$ echo $?
1
上面例子中,后两个赋值语句都会报错,命令执行失败。
-u
-u
参数声明变量为大写字母,可以自动把变量值转成大写字母。
$ declare -u foo
$ foo=upper
$ echo $foo
UPPER
-l
-l
参数声明变量为小写字母,可以自动把变量值转成小写字母。
$ declare -l bar
$ bar=LOWER
$ echo $bar
lower
-p
-p
参数输出变量信息。
$ foo=hello
$ declare -p foo
declare -- foo="hello"
$ declare -p bar
bar:未找到
上面例子中,declare -p
可以输出已定义变量的值,对于未定义的变量,会提示找不到。
如果不提供变量名,declare -p
输出所有变量的信息。
-f
-f
参数输出当前环境的所有函数,包括它的定义。
-F
-F
参数输出当前环境的所有函数名,不包含函数定义。
readonly 命令
readonly
命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset
变量。
$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1
let命令
let
命令声明变量时,可以直接执行算术表达式。
$ let foo=1+2
$ echo $foo
3
上面例子中,let
命令可以直接计算1 + 2
。
let
命令的参数表达式如果包含空格,就需要使用引号。
$ let "foo = 1 + 2"
let
可以同时对多个变量赋值,赋值表达式之间使用空格分隔。
$ let "v1 = 1" "v2 = v1++"
$ echo $v1,$v2
2,1
上面例子中,let
声明了两个变量v1
和v2
,其中v2
等于v1++
,表示先返回v1
的值,然后v1
自增。
字符串操作
获取字符串长度
${#varname}
例子
$ myPath=/home/cam/book/long.file.name
$ echo ${#myPath}
29
大括号{}
是必需的,否则 Bash 会将$#
理解成脚本的参数个数,将变量名理解成文本。
$ echo $#myvar
0myvar
上面例子中,Bash 将$#
和myvar
分开解释了。
子字符串
${varname:offset:length}
上面语法的含义是返回变量$varname
的子字符串,从位置offset
开始(从0
开始计算),长度为length
。
$ count=frogfootman
$ echo ${count:4:4}
foot
这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。变量前面的美元符号可以省略。
# 报错
$ echo ${"hello":2:3}
如果省略length
,则从位置offset
开始,一直返回到字符串的结尾。
$ count=frogfootman
$ echo ${count:4}
footman
上面例子是返回变量count
从4号位置一直到结尾的子字符串。
如果offset
为负值,表示从字符串的末尾开始算起。注意,负数前面必须有一个空格, 以防止与${variable:-word}
的变量的设置默认值语法混淆。这时,如果还指定length
,则length
不能小于零。
$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
上面例子中,offset
为-5
,表示从倒数第5个字符开始截取,所以返回long.
。如果指定长度为2
,则返回lo
。
搜索和替换
Bash 提供字符串搜索和替换的多种方法。
字符串头部的模式匹配
以下两种语法可以检查字符串开头,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}
# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}
上面两种语法会删除变量字符串开头的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。
匹配模式pattern
可以使用*
、?
、[]
等通配符。
$ myPath=/home/cam/book/long.file.name
$ echo ${myPath#/*/}
cam/book/long.file.name
$ echo ${myPath##/*/}
long.file.name
下面写法可以删除文件路径的目录部分,只留下文件名。
$ path=/home/cam/book/long.file.name
$ echo ${path##*/}
long.file.name
如果要将头部匹配的部分,替换成其他内容,采用下面的写法。
# 模式必须出现在字符串的开头
${variable/#pattern/string}
# 示例
$ foo=JPG.JPG
$ echo ${foo/#JPG/jpg}
jpg.JPG
字符串尾部的模式匹配
以下两种语法可以检查字符串结尾,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable%pattern}
# 如果 pattern 匹配变量 variable 的结尾,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable%%pattern}
上面两种语法会删除变量字符串结尾的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。
$ path=/home/cam/book/long.file.name
$ echo ${path%.*}
/home/cam/book/long.file
$ echo ${path%%.*}
/home/cam/book/long
基本和#一致,只是将#换成了$
任意位置的模式匹配
以下两种语法可以检查字符串内部,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,换成其他的字符串返回。原始变量不会发生变化。
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}
下面的例子将分隔符从:
换成换行符。
$ echo -e ${PATH//:/'\n'}
/usr/local/bin
/usr/bin
/bin
...
上面例子中,echo
命令的-e
参数,表示将替换后的字符串的\n
字符,解释为换行符。
前面提到过,这个语法还有两种扩展形式。
# 模式必须出现在字符串的开头
${variable/#pattern/string}
# 模式必须出现在字符串的结尾
${variable/%pattern/string}
改变大小写
下面的语法可以改变变量的大小写。
# 转为大写
${varname^^}
# 转为小写
${varname,,}
下面是一个例子。
$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello
算数运算
算数表达式
((...))
语法可以进行整数的算术运算。
$ ((foo = 5 + 5))
$ echo $foo
10
((...))
会自动忽略内部的空格,所以下面的写法都正确,得到同样的结果。
$ ((2+2))
$ (( 2+2 ))
$ (( 2 + 2 ))
这个语法不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0
,命令就算执行成功。
$ (( 3 + 2 ))
$ echo $?
0
如果算术结果为0
,命令就算执行失败。
$ (( 3 - 3 ))
$ echo $?
1
这个语法只能计算整数,否则会报错。
~# echo $((1.2+1))
-bash: 1.2+1: syntax error: invalid arithmetic operator (error token is ".2+1")
如果在$((...))
里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错。
$ foo=hello
$ hello=3
$ echo $(( foo + 2 ))
5
上面例子中,变量foo
的值是hello
,而hello
也会被看作变量名。这使得有可能写出动态替换的代码。
进制
-
number
:没有任何特殊表示法的数字是十进制数(以10为底)。 -
0number
:八进制数。 -
0xnumber
:十六进制数。 -
base#number
:base
进制的数。
$ echo $((0xff))
255
$ echo $((2#11111111))
255
位运算
$ echo $((16>>2))
4
$ echo $((16<<2))
64
$ echo $((17&3))
1
$ echo $((17|3))
19
$ echo $((17^3))
18
逻辑运算
$ echo $((3 > 2))
1
$ echo $(( (3 > 2) || (4 <= 1) ))
1
$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0
赋值运算
算术表达式$((...))
可以执行赋值运算。
$ foo=5
$ echo $((foo*=2))
10
求值运算
逗号,
在$((...))
内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。
$ echo $((foo = 1 + 2, 3 * 4))
12
$ echo $foo
3
expr 命令
expr
命令支持算术运算,可以不使用((...))
语法。
$ expr 3 + 2
5
Bash行操作
光标移动
-
Ctrl + a
:移到行首。 -
Ctrl + b
:向行首移动一个字符,与左箭头作用相同。 -
Ctrl + e
:移到行尾。 -
Ctrl + f
:向行尾移动一个字符,与右箭头作用相同。
清除屏幕
Ctrl + l
快捷键可以清除屏幕,即将当前行移到屏幕的第一行,与clear
命令作用相同。
编辑操作
-
Ctrl + d
:删除光标位置的字符(delete)。 -
Ctrl + w
:删除光标前面的单词。 -
Ctrl + t
:光标位置的字符与它前面一位的字符交换位置(transpose)。
使用Ctrl + d
的时候,如果当前行没有任何字符,会导致退出当前 Shell,所以要小心。
-
Ctrl + k
:剪切光标位置到行尾的文本。 -
Ctrl + u
:剪切光标位置到行首的文本。 -
Ctrl + y
:在光标位置粘贴文本。
操作历史
Bash 会保留用户的操作历史,即用户输入的每一条命令都会记录。退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入~/.bash_history
文件,该文件默认储存500个操作。
环境变量HISTFILE
总是指向这个文件。
$ echo $HISTFILE
/home/me/.bash_history
$ echo Hello World
Hello World
$ echo Goodbye
Goodbye
$ !e
echo Goodbye
Goodbye
上面例子中,!e
表示找出操作历史之中,最近的那一条以e
开头的命令并执行。Bash 会先输出那一条命令echo Goodbye
,然后直接执行。
同理,!echo
也会执行最近一条以echo
开头的命令。
$ !echo
echo Goodbye
Goodbye
$ !echo H
echo Goodbye H
Goodbye H
$ !echo H G
echo Goodbye H G
Goodbye H G
注意,!string
语法只会匹配命令,不会匹配参数。所以!echo H
不会执行echo Hello World
,而是会执行echo Goodbye
,并把参数H
附加在这条命令之后。同理,!echo H G
也是等同于echo Goodbye
命令之后附加H G
。
感叹号!
的快捷键如下。
-
!!
:执行上一个命令。 -
!n
:执行历史文件里面行号为n
的命令。 -
!-n
:执行当前命令之前n
条的命令。 -
!string
:执行最近一个以指定字符串string
开头的命令。 -
!?string
:执行最近一条包含字符串string
的命令。 -
^string1^string2
:执行最近一条包含string1
的命令,将其替换成string2
最后,按下Ctrl + r
会显示操作历史,可以用方向键上下移动,选择其中要执行的命令。也可以键入命令的首字母,Shell 就会自动在历史文件中,查询并显示匹配的结果。
通过定制环境变量HISTTIMEFORMAT
,可以显示每个操作的时间。
$ export HISTTIMEFORMAT='%F %T '
$ history
1 2013-06-09 10:40:12 cat /etc/issue
2 2013-06-09 10:40:12 clear
上面代码中,%F
相当于%Y - %m - %d
,%T
相当于%H : %M : %S
。
只要设置HISTTIMEFORMAT
这个环境变量,就会在.bash_history
文件保存命令的执行时间戳。如果不设置,就不会保存时间戳。
如果不希望保存本次操作的历史,可以设置环境变量HISTSIZE
等于0。
目录堆栈
cd -
Bash 可以记忆用户进入过的目录。默认情况下,只记忆前一次所在的目录,cd -
命令可以返回前一次的目录。
# 当前目录是 /path/to/foo
$ cd bar
# 重新回到 /path/to/foo
$ cd -
上面例子中,用户原来所在的目录是/path/to/foo
,进入子目录bar
以后,使用cd -
可以回到原来的目录。
Bash 脚本入门
Shebang 行
脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!
字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。
#!
后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh
或/bin/bash
。
#!/bin/sh
# 或者
#!/bin/bash
如果 Bash 解释器不放在目录/bin
,脚本就无法执行了。为了保险,可以写成下面这样。
#!/usr/bin/env bash
Shebang 行不是必需的,但是建议加上这行。如果缺少该行,就需要手动将脚本传给解释器。举例来说,脚本是script.sh
,有 Shebang 行的时候,可以直接调用执行。
$ ./script.sh
如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh
env命令
env
命令总是指向/usr/bin/env
文件,或者说,这个二进制文件总是在目录/usr/bin
。
#!/usr/bin/env NAME
这个语法的意思是,让 Shell 查找$PATH
环境变量里面第一个匹配的NAME
。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。
/usr/bin/env bash
的意思就是,返回bash
可执行文件的位置,前提是bash
的路径是在$PATH
里面。其他脚本文件也可以使用这个命令。比如 Node.js 脚本的 Shebang 行,可以写成下面这样。
#!/usr/bin/env node
nv
命令的参数如下。
-
-i
,--ignore-environment
:不带环境变量启动。 -
-u
,--unset=NAME
:从环境变量中删除一个变量。 -
--help
:显示帮助。 -
--version
:输出版本信息。
下面是一个例子,新建一个不带任何环境变量的 Shell。
$ env -i /bin/sh
注释
Bash 脚本中,#
表示注释,可以放在行首,也可以放在行尾。
# 本行是注释
echo 'Hello World!'
echo 'Hello World!' # 井号后面的部分也是注释
脚本参数
调用脚本的时候,脚本文件名后面可以带有参数。
$ script.sh word1 word2 word3
脚本文件内部,可以使用特殊变量,引用这些参数。
-
$0
:脚本文件名,即script.sh
。 -
$1
~$9
:对应脚本的第一个参数到第九个参数。 -
$#
:参数的总数。 -
$@
:全部的参数,参数之间使用空格分隔。 -
$*
:全部的参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
#!/bin/bash
# script.sh
echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
执行结果如下。
$ ./script.sh a b c
全部参数:a b c
命令行参数数量:3
$0 = script.sh
$1 = a
$2 = b
$3 = c
用户可以输入任意数量的参数,利用for
循环,可以读取每一个参数。
#!/bin/bash
for i in "$@"; do
echo $i
done
shift 命令
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推。
while
循环结合shift
命令,也可以读取每一个参数。
#!/bin/bash
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
上面例子中,shift
命令每次移除当前第一个参数,从而通过while
循环遍历所有参数。
shift
命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1
。
shift 3
上面的命令移除前三个参数,原来的$4
变成$1
。
getopts 命令
getopts
命令用在脚本内部,可以解析复杂的脚本命令行参数,通常与while
循环一起使用,取出脚本所有的带有前置连词线(-
)的参数。
getopts optstring name
它带有两个参数。第一个参数optstring
是字符串,给出脚本所有的连词线参数。getopts
规定带有参数值的配置项参数,后面必须带有一个冒号(:
)。
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"
# ./test_param.sh -lh -a aa
linuxconfig
h stands for h
The value provided is aa
配置项参数终止符 --
变量当作命令的参数时,有时希望指定变量只能作为实体参数,不能当作配置项参数,这时可以使用配置项参数终止符--
。
$ myPath="~/docs"
$ ls -- $myPath
上面例子中,--
强制变量$myPath
只能当作实体参数(即路径名)解释。
如果变量不是路径名,就会报错。
$ myPath="-l"
$ ls -- $myPath
ls: 无法访问'-l': 没有那个文件或目录
上面例子中,变量myPath
的值为-l
,不是路径。但是,--
强制$myPath
只能作为路径解释,导致报错“不存在该路径”。
exit 命令
exit
命令后面可以跟参数,该参数就是退出状态。
# 退出值为0(成功)
$ exit 0
# 退出值为1(失败)
$ exit 1
source 命令
source
命令用于执行一个脚本,通常用于重新加载一个配置文件。
$ source .bashrc
source
命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source
命令执行脚本时,不需要export
变量。
#!/bin/bash
# test.sh
echo $foo
上面脚本输出$foo
变量的值。
# 当前 Shell 新建一个变量 foo
$ foo=1
# 打印输出 1
$ source test.sh
1
# 打印输出空字符串
$ bash test.sh
上面例子中,当前 Shell 的变量foo
并没有export
,所以直接执行无法读取,但是source
执行可以读取。
source
命令的另一个用途,是在脚本内部加载外部库。
#!/bin/bash
source ./lib.sh
function_from_lib
source
有一个简写形式,可以使用一个点(.
)来表示。
$ . .bashrc
别名,alias 命令
alias
命令用来为一个命令指定别名,这样更便于记忆。下面是alias
的格式。
alias NAME=DEFINITION
上面命令中,NAME
是别名的名称,DEFINITION
是别名对应的原始命令。注意,等号两侧不能有空格,否则会报错。
一个常见的例子是为grep
命令起一个search
的别名。
alias search=grep
alias
也可以用来为长命令指定一个更短的别名。下面是通过别名定义一个today
的命令。
$ alias today='date +"%A, %B %-d, %Y"'
$ today
星期一, 一月 6, 2020
有时为了防止误删除文件,可以指定rm
命令的别名。
$ alias rm='rm -i'
上面命令指定rm
命令是rm -i
,每次删除文件之前,都会让用户确认。
alias
定义的别名也可以接受参数,参数会直接传入原始命令。
$ alias echo='echo It says: '
$ echo hello world
It says: hello world
上面例子中,别名定义了echo
命令的前两个参数,等同于修改了echo
命令的默认行为。
指定别名以后,就可以像使用其他命令一样使用别名。一般来说,都会把常用的别名写在~/.bashrc
的末尾。另外,只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。
直接调用alias
命令,可以显示所有别名。
$ alias
unalias
命令可以解除别名。
$ unalias lt
read命令
read
命令的格式如下。
read [-options] [variable...]
上面语法中,options
是参数选项,variable
是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量REPLY
会包含用户输入的一整行数据。
下面是一个例子demo.sh
。
#!/bin/bash
echo -n "输入一些文本 > "
read text
echo "你的输入:$text"
# ./read_sh.sh
输入一些文本 > huangzle
你的输入:huangzle
read
可以接受用户输入的多个值。
#!/bin/bash
echo Please, enter your firstname and lastname
read FN LN
echo "Hi! $LN, $FN !"
上面例子中,read
根据用户的输入,同时为两个变量赋值。
如果用户的输入项少于read
命令给出的变量数目,那么额外的变量值为空。如果用户的输入项多于定义的变量,那么多余的输入项会包含到最后一个变量中。
如果read
命令之后没有定义变量名,那么环境变量REPLY
会包含所有的输入。
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
上面脚本的运行结果如下。
$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
read
命令除了读取键盘输入,可以用来读取文件。
while read myline
do
echo "$myline"
done < $filename
上面的例子通过read
命令,读取一个文件的内容。done
命令后面的定向符<
,将文件导向read
命令,每次读取一行,存入变量myline
,直到文件读取完毕。
参数
-t
read
命令的-t
参数,设置了超时的秒数。如果超过了指定时间,用户仍然没有输入,脚本将放弃等待,继续向下执行。
#!/bin/bash
echo -n "输入一些文本 > "
if read -t 3 response; then
echo "用户已经输入了"
else
echo "用户没有输入"
fi
上面例子中,输入命令会等待3秒,如果用户超过这个时间没有输入,这个命令就会执行失败。if
根据命令的返回值,转入else
代码块,继续往下执行。
环境变量TMOUT
也可以起到同样作用,指定read
命令等待用户输入的时间(单位为秒)。
$ TMOUT=3
$ read response
上面例子也是等待3秒,如果用户还没有输入,就会超时。
-p 参数
-p
参数指定用户输入的提示信息。
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
上面例子中,先显示Enter one or more values >
,再接受用户的输入。
-a参数
-a
参数把用户的输入赋值给一个数组,从零号位置开始。
$ read -a people
alice duchess dodo
$ echo ${people[2]}
dodo
上面例子中,用户输入被赋值给一个数组people
,这个数组的2号成员就是dodo
。
-n 参数
-n
参数指定只读取若干个字符作为变量值,而不是整行读取。
$ read -n 3 letter
abcdefghij
$ echo $letter
abc
上面例子中,变量letter
只包含3个字母。
-e 参数
-e
参数允许用户输入的时候,使用readline
库提供的快捷键,比如自动补全。
echo Please input the path to the file:
read -e fileName
echo $fileName
上面例子中,read
命令接受用户输入的文件名。这时,用户可能想使用 Tab 键的文件名“自动补全”功能,但是read
命令的输入默认不支持readline
库的功能。-e
参数就可以允许用户使用自动补全。
其它参数
-
-d delimiter
:定义字符串delimiter
的第一个字符作为用户输入的结束,而不是一个换行符。 -
-r
:raw 模式,表示不把用户输入的反斜杠字符解释为转义字符。 -
-s
:使得用户的输入不显示在屏幕上,这常常用于输入密码或保密信息。 -
-u fd
:使用文件描述符fd
作为输入。
IFS变量
read
命令读取的值,默认是以空格分隔。可以通过自定义环境变量IFS
(内部字段分隔符,Internal Field Separator 的缩写),修改分隔标志。
IFS
的默认值是空格、Tab 符号、换行符号,通常取第一个(即空格)。
如果把IFS
定义成冒号(:
)或分号(;
),就可以分隔以这两个符号分隔的值,这对读取文件很有用。
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
上面例子中,IFS
设为冒号,然后用来分解/etc/passwd
文件的一行。IFS
的赋值命令和read
命令写在一行,这样的话,IFS
的改变仅对后面的命令生效,该命令执行后IFS
会自动恢复原来的值。
如果IFS
设为空字符串,就等同于将整行读入一个变量。
条件判断
if
if
是最常用的条件判断结构,只有符合给定条件时,才会执行指定的命令。它的语法如下。
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
if
关键字后面是主要的判断条件,elif
用来添加在主条件不成立时的其他判断条件,else
则是所有条件都不成立时要执行的部分。
if test $USER = "foo"; then
echo "Hello foo."
else
echo "You are not foo."
fi
if
和then
写在同一行时,需要分号分隔。分号是 Bash 的命令分隔符。它们也可以写成两行,这时不需要分号。
if true
then
echo 'hello world'
fi
if false
then
echo 'it is false' # 本行不会执行
fi
除了多行的写法,if
结构也可以写成单行。
$ if true; then echo 'hello world'; fi
hello world
注意,if
关键字后面也可以是一条命令,该条命令执行成功(返回值0
),就意味着判断条件成立。
$ if echo 'hi'; then echo 'hello world'; fi
hi
hello world
if
后面可以跟任意数量的命令。这时,所有命令都会执行,但是判断真伪只看最后一个命令,即使前面所有命令都失败,只要最后一个命令返回0
,就会执行then
的部分。
$ if false; true; then echo 'hello world'; fi
hello world
test
if
结构的判断条件,一般使用test
命令,有三种形式。
# 写法一
test expression
# 写法二
[ expression ]
# 写法三
[[ expression ]]
上面三种形式是等价的,但是第三种形式还支持正则判断,前两种不支持。
上面的expression
是一个表达式。这个表达式为真,test
命令执行成功(返回值为0
);表达式为伪,test
命令执行失败(返回值为1
)。注意,第二种和第三种写法,[
和]
与内部的表达式之间必须有空格。
$ test -f /etc/hosts
$ echo $?
0
$ [ -f /etc/hosts ]
$ echo $?
0
上面的例子中,test
命令采用两种写法,判断/etc/hosts
文件是否存在,这两种写法是等价的。命令执行后,返回值为0
,表示该文件确实存在。
实际上,[
这个字符是test
命令的一种简写形式,可以看作是一个独立的命令,这解释了为什么它后面必须有空格。
下面把test
命令的三种形式,用在if
结构中,判断一个文件是否存在。
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi
# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi
# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi
文件判断
以下表达式用来判断文件状态。
-
[ -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
。
$FILE
要放在双引号之中。这样可以防止$FILE
为空,因为这时[ -e ]
会判断为真。而放在双引号之中,返回的就总是一个空字符串,[ -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 解释为重定向操作符。
注意,字符串判断时,变量要放在双引号之中,比如[ -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
。
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
正则表达式
[[ expression ]]
这种判断形式,支持正则表达式。
[[ string1 =~ regex ]]
上面的语法中,regex
是一个正则表示式,=~
是正则比较运算符。
下面是一个例子。
#!/bin/bash
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "INT is an integer."
exit 0
else
echo "INT is not an integer." >&2
exit 1
fi
上面代码中,先判断变量INT
的字符串形式,是否满足^-?[0-9]+$
的正则模式,如果满足就表明它是一个整数。
逻辑判断
通过逻辑运算,可以把多个test
判断表达式结合起来,创造更复杂的判断。
-
AND
运算:符号&&
,也可使用参数-a
。 -
OR
运算:符号||
,也可使用参数-o
。 -
NOT
运算:符号!
。
使用否定操作符!
时,最好用圆括号确定转义的范围。
if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
echo "$INT is in range."
fi
上面例子中,test
命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。
算数判断
Bash 还提供了((...))
作为算术条件,进行算术运算的判断。
if ((3 > 2)); then
echo "true"
fi
注意,算术判断不需要使用test
命令,而是直接使用((...))
结构。
如果算术计算的结果是非零值,则表示判断成立。这一点跟命令的返回值正好相反,需要小心。
case结构
case
结构用于多值判断,可以为每个值指定对应的命令,跟包含多个elif
的if
结构等价,但是语义更好。它的语法如下。
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
esac
OS=$(uname -s)
case "$OS" in
FreeBSD) echo "This is FreeBSD" ;;
Darwin) echo "This is Mac OSX" ;;
AIX) echo "This is AIX" ;;
Minix) echo "This is Minix" ;;
Linux) echo "This is Linux" ;;
*) echo "Failed to identify this OS" ;;
esac
case
的匹配模式可以使用各种通配符,下面是一些例子。
-
a)
:匹配a
。 -
a|b)
:匹配a
或b
。 -
[[:alpha:]])
:匹配单个字母。 -
???)
:匹配3个字符的单词。 -
*.txt)
:匹配.txt
结尾。 -
*)
:匹配任意输入,通过作为case
结构的最后一个模式。
#!/bin/bash
echo -n "输入一个字母或数字 > "
read character
case $character in
[[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
;;
[0-9] ) echo "输入了数字 $character"
;;
* ) echo "输入不符合要求"
esac
ash 4.0之前,case
结构只能匹配一个条件,然后就会退出case
结构。Bash 4.0之后,允许匹配多个条件,这时可以用;;&
终止每个条件块。
read -n 1 -p "Type a character > "
echo
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
[[:digit:]]) echo "'$REPLY' is a digit." ;;&
[[:graph:]]) echo "'$REPLY' is a visible character." ;;&
[[:punct:]]) echo "'$REPLY' is a punctuation symbol." ;;&
[[:space:]]) echo "'$REPLY' is a whitespace character." ;;&
[[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac
循环
while循环
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。
while condition; do
commands
done
上面代码中,只要满足条件condition
,就会执行命令commands
。然后,再次判断是否满足条件condition
,只要满足,就会一直执行下去。只有不满足条件,才会退出循环。
number=0
while [ "$number" -lt 10 ]; do
echo "Number = $number"
number=$((number + 1))
done
while
的条件部分可以执行任意数量的命令,但是执行结果的真伪只看最后一个命令的执行结果。
$ while true; false; do echo 'Hi, looping ...'; done
上面代码运行后,不会有任何输出,因为while
的最后一个命令是false
。
until 循环
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
until condition; do
commands
done
for...in 循环
for...in
循环用于遍历列表的每一项。
for variable in list
do
commands
done
for i in word1 word2 word3; do
echo $i
done
列表可以由通配符产生。
for i in *.png; do
ls -l $i
done
上面例子中,*.png
会替换成当前目录中所有 PNG 图片文件,变量i
会依次等于每一个文件。
列表也可以通过子命令产生。
#!/bin/bash
count=0
for i in $(cat ~/.bash_profile); do
count=$((count + 1))
echo "Word $count ($i) contains $(echo -n $i | wc -c) characters"
done
上面例子中,cat ~/.bash_profile
命令会输出~/.bash_profile
文件的内容,然后通过遍历每一个词,计算该文件一共包含多少个词,以及每个词有多少个字符。
in list
的部分可以省略,这时list
默认等于脚本的所有参数$@
。但是,为了可读性,最好还是不要省略,参考下面的例子。
for filename; do
echo "$filename"
done
# 等同于
for filename in "$@" ; do
echo "$filename"
done
for 循环
for (( expression1; expression2; expression3 )); do
commands
done
for (( i=0; i<5; i=i+1 )); do
echo $i
done
break,continue
for number in 1 2 3 4 5 6
do
echo "number is $number"
if [ "$number" = "3" ]; then
break
fi
done
select 结构
select
结构主要用来生成简单的菜单。它的语法与for...in
循环基本一致。
select name
[in list]
do
commands
done
Bash 会对select
依次进行下面的处理。
-
select
生成一个菜单,内容是列表list
的每一项,并且每一项前面还有一个数字编号。 - Bash 提示用户选择一项,输入它的编号。
- 用户输入以后,Bash 会将该项的内容存在变量
name
,该项的编号存入环境变量REPLY
。如果用户没有输入,就按回车键,Bash 会重新输出菜单,让用户选择。 - 执行命令体
commands
。 - 执行结束后,回到第一步,重复这个过程。
select brand in Samsung Sony iphone symphony Walton
do
echo "You have chosen $brand"
done
1) Samsung
2) Sony
3) iphone
4) symphony
5) Walton
#? 1
You have chosen Samsung
#? 1
You have chosen Samsung
Bash函数
函数和别名的区别:别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
上面代码中,fn
是自定义的函数名,函数代码就写在大括号之中。这两种写法是等价的。
# hello() {
> echo "Hello $1"
> }
# hello wolld
Hello wolld
删除一个函数可以用unset命令
unset -f functionName
查看当前 Shell 已经定义的所有函数,可以使用declare
命令。
$ declare -f #查看所有的函数
$ declare -F #查看所有函数名
$ declare -f functionName #查看特定函数名的定义
参数变量
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
-
$1
~$9
:函数的第一个到第9个的参数。 -
$0
:函数所在的脚本名。 -
$#
:函数的参数总数。 -
$@
:函数的全部参数,参数之间使用空格分隔。 -
$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
下面是一个示例脚本test.sh
。
#!/bin/bash
# test.sh
function alice {
echo "alice: $@"
echo "$0: $1 $2 $3 $4"
echo "$# arguments"
}
alice in wonderland
运行该脚本,结果如下。
$ bash test.sh
alice: in wonderland
test.sh: in wonderland
2 arguments
return 命令
return
命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
function func_return_value {
return 10
}
函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?
拿到返回值。
$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10
只能返回数字
全局变量和局部变量,local命令
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
fn () {
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
$ bash test.sh
fn: foo = 1
global: foo = 1
函数体内不仅可以声明全局变量,还可以修改全局变量。
foo=1
fn () {
foo=2
}
echo $foo
上面代码执行后,输出的变量$foo
值为2。
函数里面可以用local
命令声明局部变量。
# 脚本 test.sh
fn () {
local foo
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo =
上面例子中,local
命令声明的$foo
变量,只在函数体内有效,函数体外没有定义。
数组
创建数组
数组可以采用逐个赋值的方法创建。
ARRAY[INDEX]=value
上面语法中,ARRAY
是数组的名字,可以是任意合法的变量名。INDEX
是一个大于或等于零的整数,也可以是算术表达式。注意数组第一个元素的下标是0, 而不是1。
$ array[0]=val
$ array[1]=val
$ array[2]=val
数组也可以采用一次性赋值的方式创建。
ARRAY=(value1 value2 ... valueN)
# 等同于
ARRAY=(
value1
value2
value3
)
采用上面方式创建数组时,可以按照默认顺序赋值,也可以在每个值前面指定位置。
$ array=(a b c)
$ array=([2]=c [0]=a [1]=b)
$ days=(Sun Mon Tue Wed Thu Fri Sat)
$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu [5]=Fri [6]=Sat)
只为某些值指定位置,也是可以的。
names=(hatter [5]=duchess alice)
上面例子中,hatter
是数组的0号位置,duchess
是5号位置,alice
是6号位置。
没有赋值的数组元素的默认值是空字符串。
定义数组的时候,可以使用通配符。
$ mp3s=( *.mp3 )
上面例子中,将当前目录的所有 MP3 文件,放进一个数组。
read -a
命令则是将用户的命令行输入,读入一个数组。
$ read -a dice
上面命令将用户的命令行输入,读入数组dice
。
读取数组
读取数组指定位置的成员,要使用下面的语法。
$ echo ${array[i]} # i 是索引
读取所有的成员
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
这两个特殊索引配合for
循环,就可以用来遍历数组。
for i in "${names[@]}"; do
echo $i
done
@
和*
放不放在双引号之中,是有差别的。
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done
Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing
上面的例子中,数组activities
实际包含5个元素,但是for...in
循环直接遍历${activities[@]}
,会导致返回7个结果。为了避免这种情况,一般把${activities[@]}
放在双引号之中。
${activities[*]}
放在双引号之中,所有元素就会变成单个字符串返回。
$ for act in "${activities[*]}"; \
do \
echo "Activity: $act"; \
done
所以,拷贝一个数组的最方便方法,就是写成下面这样。
$ hobbies=( "${activities[@]}" )
这种写法也可以用来为新数组添加成员。
$ hobbies=( "${activities[@]" diving )
默认位置
如果读取数组成员时,没有读取指定哪一个位置的成员,默认使用0
号位置。
$ declare -a foo
$ foo=A
$ echo ${foo[0]}
A
上面例子中,foo
是一个数组,赋值的时候不指定位置,实际上是给foo[0]
赋值。
引用一个不带下标的数组变量,则引用的是0
号位置的数组元素。
$ foo=(a b c d e f)
$ echo ${foo}
a
$ echo $foo
a
上面例子中,引用数组元素的时候,没有指定位置,结果返回的是0
号位置。
数组的长度
要想知道数组的长度(即一共包含多少成员),可以使用下面两种语法。
${#array[*]}
${#array[@]}
提取数组的序号
${!array[@]}
或${!array[*]}
,可以返回数组的成员序号,即哪些位置是有值的。
$ arr=([5]=a [9]=b [23]=c)
$ echo ${!arr[@]}
5 9 23
$ echo ${!arr[*]}
5 9 23
${array[@]:position:length}
的语法可以提取数组成员。
$ food=( apples bananas cucumbers dates eggs fajitas grapes )
$ echo ${food[@]:1:1}
bananas
$ echo ${food[@]:1:3}
bananas cucumbers dates
上面例子中,${food[@]:1:1}
返回从数组1号位置开始的1个成员,${food[@]:1:3}
返回从1号位置开始的3个成员。
如果省略长度参数length
,则返回从指定位置开始的所有成员。
$ echo ${food[@]:4}
eggs fajitas grapes
上面例子返回从4号位置开始到结束的所有成员。
追加数组成员
数组末尾追加成员,可以使用+=
赋值运算符。它能够自动地把值追加到数组末尾。否则,就需要知道数组的最大序号,比较麻烦。
$ foo=(a b c)
$ echo ${foo[@]}
a b c
$ foo+=(d e f)
$ echo ${foo[@]}
a b c d e f
删除数组
删除一个数组成员,使用unset
命令。
$ foo=(a b c d e f)
$ echo ${foo[@]}
a b c d e f
$ unset foo[2]
$ echo ${foo[@]}
a b d e f
上面例子中,删除了数组中的第三个元素,下标为2。
删除成员也可以将这个成员设为空值。
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f
上面例子中,将数组的第二个成员设为空字符串,就删除了这个成员。
直接将数组变量赋值为空字符串,相当于删除数组的第一个成员。
unset ArrayName
可以清空整个数组。
关联数组
关联数组使用字符串而不是整数作为数组索引。
declare -A
可以声明关联数组。
declare -A colors
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
整数索引的数组,可以直接使用变量名创建数组,关联数组则必须用带有-A
选项的declare
命令声明创建。
访问关联数组成员的方式,几乎与整数索引数组相同。
echo ${colors["blue"]}
set命令
set
命令用来修改子 Shell 环境的运行参数,即定制环境。一共有十几个参数可以定制,官方手册有完整清单,本章介绍其中最常用的几个。
顺便提一下,如果命令行下不带任何参数,直接运行set
,会显示所有的环境变量和 Shell 函数。
$ set
set -u
执行脚本的时候,如果遇到不存在的变量,Bash默认忽略它。
大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响的往下执行。
set -u
就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。
#!/usr/bin/env bash
set -u
echo $a
echo bar
set -x
默认情况下,脚本执行后,只输出运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。
set -x
用来在运行结果之前,先输出执行的那一行命令。
#!/usr/bin/env bash
set -x
echo bar
执行上面的脚本,结果如下。
$ bash script.sh
+ echo bar
bar
脚本当中如果要关闭命令输出,可以使用set +x
。
#!/bin/bash
number=1
set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x
上面的例子中,只对特定的代码段打开命令输出。
bash错误处理
如果脚本里面有运行失败的命令(返回值非0
),Bash 默认会继续执行后面的命令。
#!/usr/bin/env bash
foo
echo bar
上面脚本中,foo
是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。
$ bash script.sh
script.sh:行3: foo: 未找到命令
bar
可以看到,Bash 只是显示有错误,并没有终止执行。
这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。
command || exit 1
上面的写法表示只要command
有非零返回值,脚本就会停止执行。
如果停止执行之前需要完成多个操作,就要采用下面三种写法。
# 写法一
command || { echo "command failed"; exit 1; }
# 写法二
if ! command; then echo "command failed"; exit 1; fi
# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。
command1 && command2
set -e
上面这些写法多少有些麻烦,容易疏忽。set -e
从根本上解决了这个问题,它使得脚本只要发生错误,就终止执行。
#!/usr/bin/env bash
set -e
foo
echo bar
执行结果如下。
$ bash script.sh
script.sh:行4: foo: 未找到命令
可以看到,第4行执行失败以后,脚本就终止执行了。
set -e
根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e
,该命令执行结束后,再重新打开set -e
。
set +e
command1
command2
set -e
上面代码中,set +e
表示关闭-e
选项,set -e
表示重新打开-e
选项。
还有一种方法是使用command || true
,使得该命令即使执行失败,脚本也不会终止执行。
#!/bin/bash
set -e
foo || true
echo bar
上面代码中,true
使得这一行语句总是会执行成功,后面的echo bar
会执行。
set -o pipefail
set -e
有一个例外情况,就是不适用于管道命令。
set -o pipefail
用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
其它参数
et
命令还有一些其他参数。
-
set -n
:等同于set -o noexec
,不运行命令,只检查语法是否正确。 -
set -f
:等同于set -o noglob
,表示不对通配符进行文件名扩展。 -
set -v
:等同于set -o verbose
,表示打印 Shell 接收到的每一行输入。
上面的-f
和-v
参数,可以分别使用set +f
、set +v
关闭。
常见错误
编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。
#! /bin/bash
dir_name=/path/not/exist
cd $dir_name
rm *
改成这样
[[ -d $dir_name ]] && cd $dir_name && rm *
bash -x 参数
加上-x
参数,执行每条命令之前,都会显示该命令。
$ bash -x script.sh
+ echo hello world
hello world
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
上面的脚本执行之后,会输出每一行命令。
$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
LINENO
变量LINENO
返回它在脚本里面的行号。
#!/bin/bash
echo "This is line $LINENO"
执行上面的脚本test.sh
,$LINENO
会返回3
。
$ ./test.sh
This is line 3
FUNCNAME
变量FUNCNAME
返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数,以此类推。
BASH_SOURCE
变量BASH_SOURCE
返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本,以此类推,跟变量FUNCNAME
是一一对应关系。
BASH_LINENO
变量BASH_LINENO
返回一个数组,内容是每一轮调用对应的行号。${BASH_LINENO[$i]}
跟${FUNCNAME[$i]}
是一一对应关系,表示${FUNCNAME[$i]}
在调用它的脚本文件${BASH_SOURCE[$i+1]}
里面的行号。
mktemp 命令,trap 命令
运行mktemp
命令,就能生成一个临时文件。
Bash 脚本使用mktemp
命令的用法如下。
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
参数
-d
参数可以创建一个临时目录。
-p
参数可以指定临时文件所在的目录。
-t
参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X
字符,表示随机字符,建议至少使用六个X
。默认的文件名模板是tmp.
后接十个随机字符。
$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV
trap
命令用来在 Bash 脚本中响应系统信号。
trap
的命令格式如下。
$ trap [动作] [信号1] [信号2] ...
trap
命令响应EXIT
信号的写法如下。
$ trap 'rm -f "$TMPFILE"' EXIT
上面命令中,脚本遇到EXIT
信号时,就会执行rm -f "$TMPFILE"
。
网友评论