bash 语法作为程序员好像都了解一些,但又缺少体系化学习,需要使用到某些功能时又经常手忙脚乱地查。
本文主要参考阮一峰的bash教程,对bash的知识点进行了梳理。
本文目的是作为bash的语法备忘录、语法速查表。
模式扩展
模式扩展(globbing),类似C语言中的宏展开,我们通常使用的通配符*
就是其中之一。
Bash 一共提供八种扩展,前4种为文件扩展,只有文件路径确实存在才会扩展。
~
波浪线扩展?
问号扩展*
星号扩展[]
方括号扩展{}
大括号扩展$var
变量扩展$(date)
命令扩展$((1 + 1))
算术扩展
波浪线扩展
波浪线~
会自动扩展成当前用户的主目录。~user
表示扩展成用户user
的主目录。如果用户不存在,则波浪号扩展不起作用。
1 | bash-5.1$ echo ~/projects/ |
问号扩展
?
字符代表文件路径里面的任意单个字符,不包括空字符。
只有文件确实存在的前提下,才会发生扩展。
1 | bash-5.1$ touch {a,b}.txt ab.txt |
星号扩展
*
字符代表文件路径里面的任意数量的任意字符,包括零个字符。
1 | bash-5.1$ ls *.txt |
方括号扩展
方括号扩展的形式是[...]
,只有文件确实存在的前提下才会扩展。
[^...]
和[!...]
。它们表示匹配不在方括号里面的字符
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围
1 | bash-5.1$ ls [ab].txt |
大括号扩展
大括号扩展{...}
表示分别扩展成大括号里面的所有值
大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。
1 | bash-5.1$ echo {1,2,3} |
变量扩展
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值
1 | bash-5.1$ echo $HOME |
命令扩展
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。
1 | bash-5.1$ echo $(date) |
算术扩展
$((...))
可以扩展成整数运算的结果
1 | bash-5.1$ echo $((1+1)) |
引号使用
单引号
单引号用于保留字符的字面含义,在单引号里转义字符和模式扩展都会失效。
1 | bash-5.1$ ls '[ab].txt' |
双引号
双引号比单引号宽松,三个特殊字符除外:美元符号($
)、反引号(`
)和反斜杠(\
)。这三个字符,会被 Bash 自动扩展。
也就是说,相比单引号在双引号中变量扩展,命令扩展,算术扩展以及转义字符是有效的。
1 | bash-5.1$ echo "$((1+1))" |
引号嵌套
1 | # 双引号中使用单引号 |
here doc
Here 文档(here document)是一种输入多行字符串的方法,格式如下。
它的格式分成开始标记(<< token
)和结束标记(token
), 一般用字符串EOF
作为token
1 | << token |
例如
1 | bash-5.1$ cat << EOF |
here string
Here 文档还有一个变体,叫做 Here 字符串(Here string),使用三个小于号(<<<
)表示。
它的作用是将字符串通过标准输入,传递给命令。
1 | bash-5.1$ cat <<< foobar |
变量
bash 是基于标准输入在不同进程间交互数据的,大部分功能都是在操作字符串,所以变量的默认类型也是字符串。
声明变量和读取变量
声明时等号两边不能有空格。
Bash 变量名区分大小写,HOME
和home
是两个不同的变量。
1 | bash-5.1$ foo=1 |
变量查看和删除
1 | # 查看所有变量, 其中包含父进程export的变量 |
变量输出
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。
如果希望子进程能够读到这个变量,需要使用export命令。
1 | bash-5.1$ bash -c set | grep foo |
环境变量
平时所说的环境变量,就是init进程export输出的。子进程对变量的修改不会影响父进程,也就是说变量不是共享的。
1 | # 查看环境变量 |
下面是一些常见的环境变量。
BASHPID
:Bash 进程的进程 ID。BASHOPTS
:当前 Shell 的参数,可以用shopt
命令修改。DISPLAY
:图形环境的显示器名字,通常是:0
,表示 X Server 的第一个显示器。EDITOR
:默认的文本编辑器。HOME
:用户的主目录。HOST
:当前主机的名称。IFS
:词与词之间的分隔符,默认为空格。LANG
:字符集以及语言编码,比如zh_CN.UTF-8
。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。PS1
:Shell 提示符。PS2
: 输入多行命令时,次要的 Shell 提示符。PWD
:当前工作目录。RANDOM
:返回一个0到32767之间的随机数。SHELL
:Shell 的名字。SHELLOPTS
:启动当前 Shell 的set
命令的参数TERM
:终端类型名,即终端仿真器所用的协议。UID
:当前用户的 ID 编号。USER
:当前用户的用户名。
特殊变量
Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。
$?
: 上一个命令的退出码, 0为成功,其他为失败- $$$$: 当前进程的pid
$_
: 为上一个命令的最后一个参数$!
: 为最近一个后台执行的异步命令的进程 ID。$0
: bash脚本的参数列表,0是脚本文件路径,1到n是第1到第n个参数
变量默认值
${varname:-word}
: 如果变量varname存在且不为空,则返回它的值,否则返回word${varname:=word}
: 如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。${varname:+word}
: 如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在。${varname:?message}
: 如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。
declare 命令
declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-A
:声明关联数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-p
:查看变量信息。-r
:声明只读变量。-x
:该变量输出为环境变量。
数据类型
bash 有字符串,数字,数字,关联数组四种数据类型,默认是字符串,其他类型需要手动声明。
字符串
定义
语法 varname=value
1 | bash-5.1$ s1=abcdefg |
获取长度(length)
语法 ${#varname}
1 | bash-5.1$ echo ${#s1} |
子字符串(substr)
语法 ${varname:offset:length}
, offset为负数的时候,前面要加空格,防止与默认值语法冲突。
1 | bash-5.1$ s1=abcdefg |
替换 (replace)
字符串头部的模式匹配
${variable#pattern}
: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分${variable##pattern}
: 删除最长匹配(贪婪匹配)的部分,返回剩余部分
匹配模式pattern可以使用*
、?
、[]
等通配符。
1 | $ myPath=/home/cam/book/long.file.name |
字符串尾部的模式匹配
${variable%pattern}
: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分${variable%%pattern}
: 删除最长匹配(贪婪匹配)的部分,返回剩余部分
1 | $ path=/home/cam/book/long.file.name |
任意位置的模式匹配
如果匹配pattern
则用replace
替换匹配的内容
${variable/pattern/replace}
: 替换第一个匹配${variable//pattern/replace}
: 替换所有匹配
1 | $ path=/home/cam/foo/foo.name |
数字
使用 declare -i
声明整数变量。
1 | # 声明为整数,可以直接计算,不需要使用$符号 |
数值的进制
Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。
number
:没有任何特殊表示法的数字是十进制数(以10为底)。0number
:八进制数。0xnumber
:十六进制数。base#number
:base
进制的数。
1 | bash-5.1$ declare -i a=0x77 |
算术表达式
((...))
语法可以进行整数的算术运算。
支持的算术运算符如下。
+
:加法-
:减法*
:乘法/
:除法(整除)%
:余数**
:指数++
:自增运算(前缀或后缀)--
:自减运算(前缀或后缀)
如果要读取算术运算的结果,需要在((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值。
1 | bash-5.1$ echo $((1+1)) |
数组
创建数组
array=(item1 item2)
语法可初始化数组,括号内可以换行,多行初始化可以用#
注释。
1 | # 直接初始化数组 |
访问数组元素
array[index]
语法可访问数组元素,不带index访问则是访问数组首个元素。
1 | bash-5.1$ a=(1 2 3) |
数组长度
${#array[@]}
和 ${#array[*]}
可访问获得数组长度
1 | bash-5.1$ a=(1 2 3) |
获取非空元素下标
${!array[@]}
或 ${!array[*]}
, 可以获得非空元素的下标
1 | bash-5.1$ a=(1 2 3) |
数组元素切片
${array[@]:position:length}
的语法可以提取数组成员。
1 | bash-5.1$ a=({1..10}) |
数组追加元素
数组末尾追加元素,可以使用+=
赋值运算符。
1 | bash-5.1$ a=({1..10}) |
删除元素
删除一个数组成员,使用unset
命令。
1 | bash-5.1$ a=(1 2 3) |
关联数组
declare -A
可以声明关联数组,关联数组使用字符串而不是整数作为数组索引。
除了初始化外,使用方法和数组基本相同
1 | bash-5.1$ declare -A a |
控制流
注释
#
表示注释,每行从#
开始之后的内容代表注释,会被bash忽略.
1 | bash-5.1$ echo 1111 # 222 |
条件判断
bash 和常规编程语言一样使用if
作为分支条件的关键字, fi
作为结束的关键字,else
和elif
子句是可选的
其中if
和elif
的condition
所判断的内容是命令的状态码是否为0,为0则执行关联的语句。
1 | 因为bash中分号(;)和换行是等价的,所以有下面两种风格,其他多行语句也是类似的 |
这里的condition
可以是多个命令,如command1 && command2
,或者command1 || command2
,则if判断的是这两个命令的状态码的逻辑计算结果。
condition
也是可以是command1; command2
, 则则if判断的是最后一个命令的状态码。
这里最常用的condition
是test
命令, 也就是[[]]
和[]
. test
是bash的内置命令,会执行给定的表达式,结果为真满足则返回状态码0, 否则返回状态码1.
下文循环语言的condition
也是相同的,就不赘述了
1 | bash-5.1$ test 1 -eq 1 |
[[]]
和[]
的区别是[[]]
内部支持&&
,||
逻辑判断,所以以下三种写法是等价的。
由于[
和]
是命令, 所以两侧一定要有空格,也是就是[ 1 -eq 1 ]
,否则bash会认为命令找不到。
1 | test |
逻辑操作符
判断条件支持且(&&)或(||)非(!)
1 | not |
判断时引号使用(quote)
使用[
和test
时,变量引用注意加双引号,否则得不到正确的结果,[[
则不需要。
1 | bash-3.2$ echo "$SSH_CLIENT" |
字符串判断
bash默认数据类型为字符串,所以常见的 >
, <
是用于字符串判断。
注意:字符串判断不支持>=
和<=
, 得使用逻辑组合来替代
-z string
:字符串串长度为0-n string
: 字符串长度大于0string1 == string2
: string1 等于 string2string1 = string2
: string1 等于 string2string1 > string2
: 如果按照字典顺序string1排列在string2之后string1 < string2
: 如果按照字典顺序string1排列在string2之前
数字(整数)判断
下面的表达式用于判断整数。
[ 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
。
文件判断
以下表达式用来判断文件状态。仅列举常用判断,详细支持列表参考 https://tldp.org/LDP/abs/html/fto.html
[ -a file ]
:如果 file 存在,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
, 同-a
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -L file ]
:如果 file 存在并且是符号链接,则为true
, 同-h
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
。
switch case
bash也支持,switch case,语法如下。
1 | case EXPRESSION in |
例如1
2
3
4
5
6
7
8
9
10
11a=2
case $a in
1)
echo 11
;;
2)
echo 22
;;
*)
;;
esac
循环
while 循环
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。condition
与if语句的相同,就不赘述了。
1 | while condition; do |
unitl 循环
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
1 | until condition; do |
for-in 循环
for...in
循环用于遍历列表的每一项。
1 | for variable in list; do |
常见的几种用法1
2
3
4
5
6
7
8
9
10
11
12for i in 1 2 3; do
echo $i
done
for i in {1..3}; do
echo $i
done
list=(1 2 3)
for i in ${list[@]}; do
echo $i
done
for 循环
for
循环还支持 C 语言的循环语法。
1 | for (( expression1; expression2; expression3 )); do |
上面代码中,expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
例如1
2
3for ((i=1; i<=3; i++)); do
echo $i
done
跳出循环
Bash 提供了两个内部命令break
和continue
,用来在循环内部跳出循环。
break
命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。continue
命令立即终止本轮循环,开始执行下一轮循环。
函数
函数定义
Bash 函数定义的语法有两种,其中fn
为定义的函数名称。
1 | 第一种 |
函数参数
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
${N}
:函数的第一个到第N个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
函数调用
funcname arg1 arg ... argN
的语法进行函数调用。主要函数的返回值和输出值(标准输出)的区别,这和主流编程语言不同
1 | add() { |
函数返回值
return
命令用于从函数返回一个值。返回值和命令的状态码一样,可以用$?
拿到值。return
也可以不接具体的值,则返回值是return命令的上一条命令的状态码。
如果不加return
,则返回值是函数体最后一条命令的状态码。
1 | function func_return_value { |
关键概念
shebang
Shebang(也称为Hashbang)是一个由井号和叹号构成的字符序列#!
, 其出现在可执行文本文件的第一行的前两个字符。
在文件中存在Shebang的情况下,类Unix操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令.
例如,shell脚本1
2
3
echo Hello, world!
python 脚本1
2
3#!/usr/bin/env python -u
print("Hello, world!")
状态码
每个命令都会返回一个退出状态码(有时候也被称为返回状态)。
成功的命令返回 0,不成功的命令返回非零值,非零值通常都被解释成一个错误码。行为良好的 UNIX 命令、程序和工具都会返回 0 作为退出码来表示成功,虽然偶尔也会有例外。
状态码一般是程序的main函数的返回码,如c,c++。
如果是bash脚本,状态码的值则是 exit
命令的参数值。
当脚本以不带参数的 exit
命令来结束时,脚本的退出状态码就由脚本中最后执行的命令来决定,这与函数的 return
行为是一致的。
特殊变量$?
可以查看上个命令的退出状态码
文件描述符
文件描述符在形式上是一个非负整数。指向内核为每一个进程所维护的该进程打开文件的记录表。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
标准输入输出
每个Unix进程(除了可能的守护进程)应均有三个标准的POSIX文件描述符,对应于三个标准流:
0
:标准输入1
:标准输出2
:错误输出
打开新的文件描述符
手动指定描述符1
2
3exec 3<> /tmp/foo #open fd 3.
echo "test" >&3
exec 3>&- #close fd 3.
系统自动分配描述符,bash4.1开始支持(在macos报错,原因不明)1
2
3
4
5
6
7
8!/bin/bash
FILENAME=abc.txt
exec {FD}<>"$FILENAME"
echo 11 >&FD
echo 22 >&FD
FD>&-
描述符重定向
command > file
: 将输出重定向到 file。command < file
: 将输入重定向到 file。command >> file
: 将输出以追加的方式重定向到 file。n > file
: 将文件描述符为 n 的文件重定向到 file。n >> file
: 将文件描述符为 n 的文件以追加的方式重定向到 file。n >& m
: 将输出文件 m 和 n 合并。n <& m
: 将输入文件 m 和 n 合并。
所以命令中常见的ls -al > output.txt 2>&1
, 就是将标准输出和错误输出都重定向到一个文件。
等价于ls -al &>output.txt
,本人偏好这种写法,比较简洁。
IFS (Input Field Separators)
IFS决定了bash在处理字符串的时候是如何进行单词切分。
IFS的默认值是空格,TAB,换行符,即\t\n
1 | echo "$IFS" | cat -et |
例如,在for循环的时候,如何区分每个item1
2
3for i in `echo -e "foo bar\tfoobar\nfoofoo"`; do
echo "'$i' is the substring";
done
也可以自定义1
2
3
4
5
6
7OLD_IFS="$IFS"
IFS=":"
string="1:2:3"
for i in $string; do
echo "'$i' is the substring";
done
IFS=$OLD_IFS
任务管理
linux进程分前台(fg)和后台(bg)。
在命令的末尾添加&
可以将命令后台执行,一般配合输出重定向使用。
jobs
可以查看当前bash进程的子进程,并通过fg
和bg
进行前台和后台切换。
%1
代表后台的第一个进程,以此类推%N
代表第n个.
control + Z
可以将当前前台程序暂停,配合bg
可以将其转后台。
wait [pid]
可以等待子进程结束,如果不带pid参数则等待所有子进程结束。
1 | bash-5.1$ sleep 100 |
后台进程并发控制
可以利用jobs对后台进程并发数目进行控制1
2
3
4
5
6
7
8for i in {1..30}; do
sleep $((30+i)) &
if [[ $(jobs | wc -l ) -gt 10 ]]; then
jobs
wait
fi
done
wait