shell脚本语法
温馨提示:看这篇文章之前,希望你已掌握基本的shell命令,这里重点关注怎么进行shell编程。
一. 概述
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的长度非0string: 等价与-n string-z string:string的长度为0str1 = 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>来指定脚本的退出状态码。

453

被折叠的 条评论
为什么被折叠?



