一. 概述
shell脚本的目的是为了按照要求(包括时间、流程、条件等)执行一些命令,这些命令分为内部命令和外部程序。sh、bash、zsh等这些都是解释器,它们本身识别一些命令,内部命令在解释器的代码中实现的;外部命令就是独立的外部程序,被shell调用时作为一个子进程执行。type <filename>
可以返回一个命令是内部命令还是外部程序。有些命令既有内部实现,也有外部程序实现,可以用-a选项输出所有的结果,比如:
[calvin ~]$ type -a echo
echo is a shell builtin
echo is /bin/echo
其实,直接把命令写在一个文本文件中组成的一个简单脚本,就可以完成相当多的自动化任务了。但是这样简陋的脚本(也可能很臃肿)不能充分发挥shell的能力,它还可以更强大、更通用、更优雅一些。脚本中的其它元素就是为了更好地描述命令而存在的,比如:变量,可以作为命令的参数,可以作为命令的集合;指令,像if then
,for do
等;函数,可以让一组命令构成一个小的模块,提高复用性。
所以,脚本中的主角是各种各样的命令,尤其是外部命令,往往决定脚本的实际功能,但外部程序太多而且有各自独特的使用方法,不是本文关注的内容。脚本语法更像是胶水,把各种命令粘在一起实现需求,下面介绍这些粘合剂的形式与功能。
二. 变量
变量的值都是字符串,一个变量可以处于已定义和未定义两种状态,已定义的变量可以有值也可以为空。
定义变量时不要有空格:variable=<value>
。当获取一个变量的值得时候,用${}
把变量包起来,叫做变量的扩展。扩展得到的值可以在脚本中生效,比如赋值给其他变量、作为命令参数、作为命令本身甚至再次扩展等。
以下是一些变量展开的方式和变形:
2.1 ${var}获取变量的值
一般来说,$var
和${var}
效果一样,都可以扩展一个变量(即获得它的值)。但如果要扩展变量值加一些字符时,就会出错,因为默认时他会把$
后的一整个单词作为扩展对象。这时应该用${var}
,大括号来指明变量名称的范围。
[calvin ~]$ aa='hello'
[calvin ~]$ echo $aa
hello
[calvin ~]$ echo $aa_world # 把aa_world当成要展开的变量名,它是没有定义的。
[calvin ~]$ echo ${aa}_world
hello_world
这里应该可以感受到,shell展开变量仅仅是字符串级别的替换,替换之后的字符串作为下次操作的对象。
2.2 $(cmd)提取命令的输出
- 展开的结果是cmd执行后的标准输出,相当于用反引号括起来的命令,这个功能非常好用。
[calvin ~]$ pwd
/home/calvin
[calvin ~]$ echo $(pwd)
/home/calvin
-
$()
中的命令的错误输出是不会被提取的,提取的只是标准输出:
[calvin ~]$ var=$(cat 1.txt)
cat: 1.txt: No such file or directory
[calvin ~]$ echo $var # $var显然是空的
-
$()
中的命令在子shell中执行,其中对变量的改变并不会反映到当前shell脚本中。
[calvin ~]$ export a=outside
[calvin ~]$ echo $a
outside
[calvin ~]$ b=$(a=inside; echo $a)
[calvin ~]$ echo $b
inside
[calvin ~]$ echo $a
outside
2.3 条件赋值
(1) ${var:-string} 和 ${var:=string}
- 若变量
var
为空或者未定义,则用string
作为${var:-string}
的值,否则用变量var
的展开值
[calvin ~]$ echo $a
[calvin ~]$ echo ${a:-bbc}
bbc
[calvin ~]$ echo $a
[calvin ~]$ a=hello
[calvin ~]$ echo ${a:-bcc}
hello
[calvin ~]$ unset a
[calvin ~]$ echo $a
[calvin ~]$ echo ${a:=bbc}
bbc
[calvin ~]$ echo $a
bbc
-
${var:=string}
和${var:-string}
相比,在发现$var
为空时,还把string
赋值给了var
。
(2) ${var:+string}
- 规则和上面的完全相反,当
var
不是空的时候值为string
,若var
为空时则为变量var
的值,即空值
[calvin ~]$ a=hello
[calvin ~]$ echo $a
hello
[calvin ~]$ echo ${a:+bbc}
bbc
[calvin ~]$ echo $a
hello
[calvin ~]$ unset a
[calvin ~]$ echo $a
[calvin ~]$ echo ${a:+bbc}
(3) ${var:?string}
- 若变量
var
不为空,则用变量var
的值作为展开值,否则把string
输出到标准错误中,并从脚本中退出
[calvin ~]$ echo $a
[calvin ~]$ echo ${a:?bbc}
-bash: a: bbc
[calvin ~]$ a=hello
[calvin ~]$ echo ${a:?bbc}
hello
2.4 $((exp)) POSIX标准的扩展计算
这种计算是符合C语言的运算符,也就是说只要符合C的运算符都可用在$((exp))
,包括三目运算符
注意:这种扩展计算是整数型的计算,不支持浮点型和字符串等
若是逻辑判断,表达式exp
为真则展开值为1
,否则为0
[calvin ~]$ echo $(3+2)
3+2: command not found
[calvin ~]$ echo $((3+2))
5
[calvin ~]$ echo $((3.5+2))
-bash: 3.5+2: syntax error: invalid arithmetic operator (error token is ".5+2")
[calvin ~]$ echo $((3>2))
1
[calvin ~]$ echo $((3>2?'a':'b'))
-bash: 3>2?'a':'b': syntax error: operand expected (error token is "'a':'b'")
[calvin ~]$ echo $((3>2?3:2))
3
[calvin ~]$ echo $((a=3+2))
5
[calvin ~]$ echo $((a++))
5
[calvin ~]$ echo $a
6
2.5 模式匹配替换结构
${var%pattern}
${var%%pattern}
${var#pattern}
${var##pattern}
${var%pattern},${var%%pattern}
从右边开始匹配${var#pattern},${var##pattern}
从左边开始匹配${var%pattern} ,${var#pattern}
表示最短匹配,匹配到就停止,非贪婪${var%%pattern},${var##pattern}
是最长匹配
只有在pattern
中使用了通配符才能有最长最短的匹配,否则没有最 长最短匹配之分
结构中的pattern
支持的通配符:
-
*
表示零个或多个任意字符 -
?
表示零个或一个任意字符 -
[...]
表示匹配中括号里面的字符 -
[!...]
表示不匹配中括号里面的字符
[root@bogon ~]# f=a.tar.gz
[root@bogon ~]# echo ${f##*.}
gz
[root@bogon ~]# echo ${f%%.*}
a
[root@bogon ~]# var=abcdccbbdaa
[root@bogon ~]# echo ${var%%d*}
abc
[root@bogon ~]# echo ${var%d*}
abcdccbb
[root@bogon ~]# echo ${var#*d}
ccbbdaa
[root@bogon ~]# echo ${var##*d}
aa
#发现输出的内容是var去掉pattern的那部分字符串的值
记忆的方法为:#
是 去掉左边(键盘上#
在 $
的左边)%
是去掉右边(键盘上%
在$
的右边)
单一符号是最小匹配;两个符号是最大匹配
2.6 其他展开
-
${file:0:5}
:提取${file}
最左边的 5 个字节 -
${file:5:5}
:提取第 5 个字节及之后的连续5个字节 -
${file:5}
:提取第5个字节及之后的子串
也可以对变量值里的字符串作替换:
${file/dir/path}
:将第一个dir 替换为path${file//dir/path}
:将全部dir 替换为 path${#var}
可计算出变量值${var}
的长度
三. 重定向
重定向其实是文件描述符的复制,实现通过一个描述符访问另一个文件。
3.1 输出重定向
把某个输出流的内容通过另一个输出流输出。 比如:
-
echo "hello world" > tempfile
,其实是以截断只写方式打开tempfile
文件(文件描述符fd
),然后把文件描述符1
(标准输出)作为fd
的复制,并关闭fd
(如果明白linux内核中file
和inode
的关系会更好理解),这样程序中输出给文件描述符1的数据都重定向给了tempfile
。 -
echo "hello world" >> tempfile
,与上面的区别是以添加的方式打开tempfile
。 -
cat /etc/shadow 2> error
,错误信息会放到error
文件中。
3.2 输入重定向
把某个文件当作标准输入,从中读取数据。 比如:
-
cat <error
这里cat
本应该从标准输入0
接收数据然后输出到标准输出,输入重定向相当于在文件描述符0
上打开文件error
内联输入重定向无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据。必须指定文本标记来划分数据的开始和结尾。
$ wc << flag
> data
> haha
> flag
2 2 8
$
其本质是把你临时输入的内容缓存下来作为命令的输入。
3.3 更复杂的重定向
-
a.out 3<>temp
: 在文件描述符3上打开temp文件进行读写。<>
左边是文件描述符,而右边是文件名,如果想要重定向到另一个文件描述符,可用&n
的形式。 -
a.out 1>&2
表示把原本输出给标准输出的内容交给标准错误。
四. 管道
管道其实是一个环形缓冲区,左边的程序把数据放入缓冲区,右边的程序把数据读走。表现出来就是右边的程序把左边程序的输出作为输入。ls -l | head
输出头10条文件目录项信息,|
是管道符号,左边命令的输出作为右边的输入。
4.1 xargs
管道是把左边命令的输出(文件描述符1),作为右边命令的输入(文件描述符0)。很多时候我们需要左边输出作为右边命令的参数,这时使用xargs比较合适(这个词意味着不用xargs也可以,任何时候你都可以用sed/awk结合输出构造一个命令再用管道传给sh,哈哈)。
xargs的-i参数可以指定站位符,表示把左边命令的输出放在后续命令的特定位置作为参数。
[root@centos6 xargsi]# echo This is file1.txt > file1.txt
[root@centos6 xargsi]# echo This is file2.txt > file2.txt
[root@centos6 xargsi]# echo This is file3.txt > file3.txt
[root@centos6 xargsi]# vim files.txt
[root@centos6 xargsi]# cat files.txt
file1.txt
file2.txt
file3.txt
[root@centos6 xargsi]# cat files.txt | xargs -I {} cat {} 等价于cat files.txt | ( while read arg ; do cat $arg; done )
This is file1.txt
This is file2.txt
This is file3.txt
五. 数值计算
shell不擅长数值计算,它的所有变量都是字符串类型的。
5.1 鸡肋
operation
只能是整数计算。
$[ operation ]
或$(( opeation ))
将表达式的结果作为展开值。expr <operation>
从标准输出表达式的计算结果
5.2 bc
复杂的数学计算可以借助bc
计算器(bash calculator
)。 bc
是一个交互式计算器,可以定义变量,使用注释,创建函数和编程语句等,功能丰富。bc
中的scale
变量定义了小数的位数,默认时为0,使用小数前记得先设置这个变量。
可以通过variable=$(echo "options; expression" | bc)
的形式在shell脚本中使用bc。在options
中可以定义变量,expression
中定义了需要执行的数学计算,输出结果赋值给variable
。比如:
$ cat test.sh
#! /bin/bash
var1=100
var2=40
var3=$(echo "scale=4; $var1 / $var2" | bc)
echo ans is $var3
$
如果需要大量的运算,难以在一行内列出多个表达式,可以使用内联重定向:
➜ shell_scripts git:(dev) ✗ cat bc1.sh
#! /bin/bash
var1=10.34
var2=21.33
var3=33.2
var4=87
var5=$(bc EOF
scale=4
a1=($var1 * $var2)
b1=($var3 + $var4)
a1 + a2
EOF
)
echo ans is $var5
➜ shell_scripts git:(dev) ✗ bash bc1.sh
ans is 220.5522
➜ shell_scripts git:(dev) ✗
六. 流程控制
6.1 if-then语句
if command1;then
commands
elif comand2;then
commands
else
commands
fi
if
语句会运行if
后面的命令,如果命令的退出状态码是0
(正常退出),位于then
部分的命令就会被执行;否则,else
部分的命令会被执行。
6.1.1 test命令
由于if判断的是命令的返回状态,有时我们的逻辑表达式不是个命令,这是需要用test包裹一下。
用法:test <expression>
如果expression
为true
,test命令的返回状态为0,否则为1(包括无法识别的表达式)。
expression
的形式多种多样:
-
(exp)
: 就是exp
的值 -
!exp
:exp
取反 -
exp1 -a exp2
:exp1
和exp2
相与的结果 -
exp1 -o exp2
:exp1
和exp2
相或的结果 - 字符串比较
-
-n string
:string
的长度非0 -
string
: 等价与-n string
-
-z string
:string
的长度为0 -
str1 = str2
:这两个字符串是否相等 -
str1 != str2
: -
str1 < str2
: -
str1 > str2
:
- 数值比较
-
int1 -eq int2
:把两边当成整数,比较是否相同 -
int1 -ge int2
:int1
大于或等于int2
? -
int1 -gt int2
:int1
大于int2
? -
int1 -le int2
: 小于或等于 -
int1 -lt int2
: 小于 -
int1 -ne int2
: 不等于
- 文件比较
-
-d file
: 文件是否存在且为目录 -
-e file
: 是否存在 -
-f file
: 是否存在且为文件 -
-r file
: 是否存在且可读 -
-s file
: 是否存在且非空 -
-w file
: 是否存在且可写 -
-x file
: 是否存在且可执行 -
-O file
: 是否存在且属当前用户所有 -
-G file
: 是否存在且默认组与当前用户相同 -
file1 -nt file2
: 左边比右边新 -
file1 -ot file2
: 左边比右边旧
test
命令增强了if
语句的判断能力,把逻辑表达式转换成命令执行状态。
bash shell提供了另一种条件测试方法,无需使用test
指令。
if [ condition ];then
commands
fi
这种形式也可以使用 [ conditions1 ] && [condition2 ]
和 [ condition1 ] || [condition2]
这样的复合逻辑式。
在字符串比较时,>
要进行转义,否则认为是重定向。
6.1.2 双括号命令
$((expression))
形式:这种形式的expression
可以是任意的数学赋值或者比较表达式,其中的变量不需要用$
展开。支持自增自减、移位、位运算等运算符。
不需要将双括号里的大于号转义expression
成立展开结果是字符1
,否则是0
.
6.1.3 双方括号
[[expression]]
双方括号提供了字符串比较的高级特性。它拥有test
命令中的标准字符串比较,还提供了模式匹配功能。比如:
➜ cat test_double_square_brackets.sh
#! /bin/bash
# using pattern matching
#
if [[ $USER == i* ]] ; then
echo "Hello $USER"
else
echo "Sorry, I don't know you"
fi
➜ bash test_double_square_brackets.sh
Hello invoker
➜
注意:普通的if语句使用一个等号判断是否想等,而双括号和双方括号都是采用两个等号。
6.2 case语句
case variable in
pattern1 | pattern2)
commands1;;
pattern3)
commands2;;
*)
default-commands;;
esac
case
命令从上到下寻找,如果变量与某个分支的模式匹配,则执行此分支的命令,然后退出case
。由于*
匹配所有字符串,可作为最后的默认分支。分支的命令块结束时要用两个分号。
6.3 循环
6.3.1 for语句
for
语句允许你创建一个便利一系列值的循环。每次使用其中的一个值来执行定义好的一组命令。
for var in list;do
commands
done
list
是由内部变量IFS(internal field separator)
分隔的字符串。默认是IFS
是空格、制表符或者换行符。也可以改变IFS
的值:IFS=:$'\n'
表示换行和冒号都是字段分隔符。(换行和制表符用$'\n'
和$'\t'
表示)
bash对for
的扩展
for ((variable assignment; condition; iteration process));do
commands
done
这种类似c语言风格的for
循环并没有遵循shell标准:变量赋值可以有空格;条件中的变量不用$
展开;迭代过程的算术表达式不需要expr
。
6.3.2 while命令
while test-command;do
commands
done
test-command
可以是命令本身(判断返回状态),也可以是用test
或者方括号封装的逻辑表达式。测试命令可以指定多个,只有最后一个巨额定判断结果。
6.3.3 until命令
until test-command;do
commands
done
until
命令与while
类似,只是当条件成立是退出循环。
6.3.4 break和continue
与C语言不同的是break n
可以直接跳出n层循环。
6.3.5 处理循环的输出
如果想要对某个循环中的输出进行重定向或者管道可以在done
命令之后添加相应的处理。
七. 其他
这里列出一些琐碎的知识点
- 默认一行处理一条命令,但也可以用
;
分割多条命令,按顺序执行。 - 脚本文件中第一行必须用
#! path-to-shell
指定所用的shell。 - 除了第一行之外的#都是注释的开始标记,注释此行中#之后的内容
- 脚本最好要有执行权限,最好以
sh
作为后缀 - shell中变量
$?
来保存上个已执行命令的退出状态码。 - 默认情况下,shell脚本会以脚本中最后一个命令的退出状态码退出。
- 脚本中可以使用
exit <status>
来指定脚本的退出状态码。