一. 概述

shell脚本的目的是为了按照要求(包括时间、流程、条件等)执行一些命令,这些命令分为内部命令外部程序。sh、bash、zsh等这些都是解释器,它们本身识别一些命令,内部命令在解释器的代码中实现的;外部命令就是独立的外部程序,被shell调用时作为一个子进程执行。type <filename>可以返回一个命令是内部命令还是外部程序。有些命令既有内部实现,也有外部程序实现,可以用-a选项输出所有的结果,比如:

[calvin ~]$ type -a echo
echo is a shell builtin
echo is /bin/echo

其实,直接把命令写在一个文本文件中组成的一个简单脚本,就可以完成相当多的自动化任务了。但是这样简陋的脚本(也可能很臃肿)不能充分发挥shell的能力,它还可以更强大、更通用、更优雅一些。脚本中的其它元素就是为了更好地描述命令而存在的,比如:变量,可以作为命令的参数,可以作为命令的集合;指令,像if thenfor 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内核中fileinode的关系会更好理解),这样程序中输出给文件描述符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>

如果expressiontrue,test命令的返回状态为0,否则为1(包括无法识别的表达式)。

expression的形式多种多样:

  • (exp): 就是exp的值
  • !exp: exp取反
  • exp1 -a exp2: exp1exp2相与的结果
  • exp1 -o exp2: exp1exp2相或的结果
  • 字符串比较
  • -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>来指定脚本的退出状态码。