8. Shell 基础
Shell 是命令行解释器,有众多版本,比如 sh(Bourne Shell)、bash(Bourne Again Shell)、dash(Debian Almquist Shell)以及 zsh(Z Shell)等。
以前 bash 是 GNU/Linux 操作系统中的 /bin/sh 的符号连接,后来把 bash 从 NetBSD 移植到 Linux 并更名为 dash,且 /bin/sh 符号连接到 dash。
Ubuntu 6.10 开始默认使用 dash,dash 符合 POSIX 标准。
1$ which sh
2/usr/bin/sh
3$ ll /usr/bin/sh
4lrwxrwxrwx 1 root root 4 Jul 19  2019 /usr/bin/sh -> dash*
标记为 #!/bin/sh 的脚本不应使用任何 POSIX 没有规定的特性(如 let 等命令), 但 #!/bin/bash 可以,bash 支持的写法比 dash 多。
想要支持 sh xx.sh 运行的,必须遵照 POSIX 规范去写;
想要脚本写法多样化、不需要考虑效率的,可以在文件头注明 #!/bin/bash ,使用 bash xx.sh 或 source xx.sh (相当于 . xx.sh )来执行。
后文介绍的是 bash 的一些基本语法。
Tip
- 执行 - cat /etc/shells查看系统可使用的 Shell 种类。
- 执行 - echo $SHELL查看系统默认的 Shell 环境。
- 执行 - echo $0或- ps -p $$查看当前所处的 Shell 环境。
8.1. 通配符(Wildcard)
- *:匹配 0 个或多个任意字符。
- ?:匹配一个任意字符。
- [a-z0-9...]:匹配集合中的任意单个字符;使用- ^或- !取集合的补集。
- {str1,str2,...}:匹配集合中的任意单个字符串;逗号和字符串之间不能有空格;- touch play.{h,cpp}将创建 play.h 和 play.cpp 两个文件。
8.2. 变量
变量定义的时候不需要 $ 符号(Parameter Expansion),引用取值的时候需要。定义的时候等号两边不允许带空格:
1var="hello world"
2echo $var
3echo ${var}
引用的时候可以使用 {} 以明确变量名的边界。
使变量变为只读、不可变: readonly var
删除变量(不能删除只读变量): unset var
8.3. 字符串
字符串可以用单引号、双引号,也可以不用引号,如果字符串中有空格则需要带上引号。
- 获取字符串长度: - ${#string}
- 提取子字符串: - ${string:1:4}从字符串第 2 个字符开始截取 4 个字符(下标从 0 开始)
- 单引号
- 保留引号内每个字符的字面值,剥夺其特殊含义。 
- 双引号
- $- `- \- !- *- @等符号具有 特殊含义 。
1$ echo "$(date +"%Y-%m-%d %H:%M:%S" -d "-3 days")"
22022-11-06 23:53:52
3$ echo '$(date +"%Y-%m-%d %H:%M:%S" -d "-3 days")'
4$(date +"%Y-%m-%d %H:%M:%S" -d "-3 days")
字符串替换
给定一个字符串变量 FOO :
- ${FOO%suffix}去掉后缀(将 suffix 替换为空)
- ${FOO#prefix}去掉前缀(将 prefix 替换为空)
- ${FOO%%suffix}去掉长后缀(通配符贪婪匹配)
- ${FOO##prefix}去掉长前缀(通配符贪婪匹配)
- ${FOO/from/to}将 from 的第一个匹配替换为 to
- ${FOO//from/to}将 from 的所有匹配都替换为 to
- ${FOO/%from/to}将 from 匹配的后缀替换为 to
- ${FOO/#from/to}将 from 匹配的前缀替换为 to
8.4. 数组
定义:
array=(value0 value1 value2 value3)
或:
1array=(
2value0
3value1
4value2
5value3
6)
- 下标操作: - ${arr[0]}
- 获取全部元素: - ${arr[*]}或- ${arr[@]}
- 获取长度: - ${#arr[*]}或- ${#arr[@]}
- 遍历: - 1for i in {0..3} 2do 3 echo ${array[$i]} 4done 5 6for v in ${array[*]} 7do 8 echo $v 9done 
Attention
dash 不支持 {1..10} 这种列表的写法。
8.5. 注释
- 单行注释: - #
- 多行注释: - 1:<<EOF 2注释内容... 3注释内容... 4注释内容... 5EOF 6 7:<<' 8注释内容... 9注释内容... 10注释内容... 11' 12 13:<<! 14注释内容... 15注释内容... 16注释内容... 17! 
8.6. 传递参数
在执行 Shell 脚本时,可以向脚本传递参数,脚本内获取参数的格式为:$n 。 $1 为执行脚本的第一个参数,$2 为执行脚本的第二个参数,以此类推;超过 9 应该使用花括号如 ${10} ;$0 为执行的文件名(包含文件路径)。
- 获取参数个数: - $#
- 以单一字符串形式获取全部参数: - $*,得到类似于- "$1 $2 … $n"的值
- 以列表形式获取全部参数: - $@,得到类似于- "$1" "$2" … "$n"的值
8.7. 运算
Note
Shell 对于输入都是统一按字符串类型处理的,不管有没有加引号。有一些运算符是专门用于字面值是数值的字符串。
数值运算
expr 可以实现基础的数值运算和一些字符串操作:
- 出现在表达式中的运算符、数字、变量、圆括号的左右两边要有空格。 
- 变量需要加 - $前缀。
- 乘号 - *和圆括号- ()需要使用转义符号- \(为了和正则表达式的符号区分)。
1a=10
2b=20
3echo `expr $a + $b`
基础运算:
- 加: - expr $a + $b
- 减: - expr $a - $b
- 乘: - expr $a \* $b
- 除: - expr $a / $b
- 求余: - expr $a % $b
- 复合: - expr \( $a + $b \) \* $c
- 赋值: - a=$b
Note
还有几种方式可以执行运算:
使用
[],变量不需要$符号
$[a+b]
$[a-b]
$[a*b]
$[a/b]
使用双圆括号
(())
$((a+b))
$(((a+b)*c))
使用
let
let a++
let a+=10
let a=b*100
Note
`command` 等效于 $(command) ,都是获取 Shell 指令执行的结果,例如 echo `expr $a + $b` 等效于 echo $(expr $a + $b) 。
反引号是老式用法,推荐使用 $(command) 。
关系运算
下面的关系运算符只支持数字,不支持字面值非数值的字符串。
- 相等: - [ $a -eq $b ]
- 不等: - [ $a -ne $b ]
- 大于: - [ $a -gt $b ]
- 小于: - [ $a -lt $b ]
- 大于等于: - [ $a -ge $b ]
- 小于等于: - [ $a -le $b ]
布尔运算
- 非: - [ ! event ]取反。
- 与: - [ $a -lt 20 -a $b -gt 100 ]
- 或: - [ $a -lt 20 -o $b -gt 100 ]
逻辑运算
- 与: - [[ $a -lt 20 && $b -gt 100 ]]- 等价于 - [ $a -lt 20 ] && [ $b -gt 100 ]
- 等价于 - [ $a -lt 20 -a $b -gt 100 ]
 
- 或: - [[ $a -lt 20 || $b -gt 100 ]]
字符串运算
- 相等: - [ $a = $b ]- 也可使用 - ==,是 bash 独有的运算符。
 
- 不等: - [ $a != $b ]
- 字典序比较: - [ $a \> $b ]
- [ $a \< $b ]
 
- 长度为 0: - [ -z $a ]
- 长度不为 0: - [ -n $a ]
- 是否为空: - [ $a ],不为空返回 true 。
1$ [ ! 1 -gt 2 ] && echo '1 < 2'
21 < 2
3$ [[ 199 < 2 ]] && echo '199 < 2'
4199 < 2
Note
${parameter:-word} :当参数 parameter 已定义且为非空字符串,该表达式值为 ${parameter} ,否则为默认值 word 。
${parameter-word} :当参数 parameter 已定义,该表达式值为 ${parameter} ,否则为默认值 word 。
设置默认值的好处是,在已设置 set -u 的情况下,访问未定义的变量不会报错导致程序终止。
Hint
&& 只有在前面的命令返回 true 时,才会执行后面的命令。
Note
单中括号和双中括号:
- 括号左右都需要空格和其它字符隔开。 
- 两种括号都能用于条件判断。 
- [是 Shell 的内部命令,等效于- test。
- [[是 Shell 的关键字,支持正则匹配(- =~)。
- &&- ||- <- >能直接在- [[ ]]中使用;- [ ]内使用- <- >需要转义。
1$ type [ [[ test
2[ is a shell builtin
3[[ is a shell keyword
4test is a shell builtin
5$ [[ abcd = *bc* ]] && echo 'bc in abcd'
6bc in abcd
7$ [[ abcd =~ .*bc.* ]] && echo 'bc in abcd'
8bc in abcd
Note
[ $1 = 'target' ] 判断第一个参数值是否为 target,如果执行脚本的时候没有输入参数,会报错:
[: =: unary operator expected
因为原命令变成了 [ = 'target' ] 。
更规范的写法是在变量外部都加双引号,即: [ "$1" = 'target' ] ,如果执行脚本的时候没有输入参数,原命令变成 [ '' = 'target' ] 。
文件测试
- 目录: - [ -d $file ]
- 普通文件(非目录、非设备文件): - [ -f $file ]
- 可读: - [ -r $file ]
- 可写: - [ -w $file ]
- 可执行: - [ -x $file ]
- 为空(文件大小是否大于 0): - [ -s $file ]
- 存在: - [ -e $file ]
Note
test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试,返回 false 或 true。
- 数值: - test $num1 -eq $num2
- 字符串: - test $str1 = $str2
- 文件: - test -e $file
printf
printf 命令模仿 C 程序库里的 printf() 。
printf 由 POSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。
默认 printf 不会像 echo 自动添加换行符,需要手动添加 \n 。
例子:
1printf "Hello, Shell\n"
2printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
3printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
4printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
5printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876
输出:
1Hello, Shell
2姓名     性别   体重kg
3郭靖     男      66.12
4杨过     男      48.65
5郭芙     女      47.99
%s %c %d %f 都是格式替代符。
%-10s 指宽度为 10 个字符( - 表示左对齐,没有则表示右对齐)。
8.8. 流程控制
if else
1if condition1
2then
3    command1
4elif condition2
5then
6    command2
7else
8    commandN
9fi
for
1for var in item1 item2 ... itemN
2do
3    command1
4    command2
5    ...
6    commandN
7done
写成单行:
for var in item1 item2 ... itemN; do command1; command2; ...; done
for 循环的几种形式:
for i in {1..10}
for i in $(seq 1 10)
for ((i=1; i<=10; ++i))
Note
seq 的使用方法( man seq ):
seq [option] [first [increment]] last
first increment 缺省则默认为 1。
参数:
- -f
输出格式。需要符合
printf的浮点型格式,即%f。如果firstincrementlast中有浮点数,则默认按照三者中的最高精度输出;如果都是整型,则默认为%g格式;指定%g会强制把浮点型转换成整型;%03g指定宽度为 3,用 0 补足;prefix_%g_suffix添加了前后缀。- -s
分隔符,默认为
\n。- -w
等宽序列,将序列中最大值的宽度作为序列的宽度。
while
1while condition
2do
3    command
4done
until
1until condition
2do
3    command
4done
case
 1case $var in
 2value1)
 3    command1
 4    command2
 5    ...
 6    commandN
 7    ;;
 8value2)
 9    command1
10    command2
11    ...
12    commandN
13    ;;
14esac
每一个匹配值必须以右括号 ) 结束;一旦匹配到一个值,则执行完相应命令后不再继续其他匹配。
break 和 continue
- break跳出本层循环
- continue跳出本次循环
8.9. 函数
定义形式如下:
1[function] funname [()]
2{
3
4    action
5
6    [return int]
7
8}
上面的中括号表示该部分可以缺省。
如果不加 return ,将以最后一条命令运行结果作为返回值;返回值只能是 0 到 255 之间的整数,如果需要获取函数的计算结果,可以定义全局变量。
调用函数时可以向其传递参数,在函数体内部,通过 $n 的形式来获取参数的值,例如, $1 表示第一个参数, $2 表示第二个参数。
- 参数个数: - $#
- 以单一字符串形式获取全部参数: - $*
- 以列表形式获取全部参数: - $@
- 脚本运行的当前进程 ID: - $$
- 返回值: - $?表示返回值或显示最后命令的退出状态;0 表示没有错误,其他值表明有错误。
示例:
 1_global_var=10
 2function foo()
 3{
 4    echo "hello world"
 5    printf "param-1: %s\n" ${1}
 6    a=200
 7    b=125
 8    _global_var=$((a+b))
 9    return $((a+b))
10}
11
12foo "goodbye"
13echo $_global_var
14echo $?
输出:
1hello world
2param-1: goodbye
3325
40
8.10. 参考资料
- Shell 教程 
- Shell test 命令 
- Shell test 单中括号[] 双中括号[[]] 的区别 
- Bash cheatsheet