shell编程基础
shell脚本的关键在于输入多个命令并处理每个命令的结果,甚至需要将一个命令的结果传给
另一个命令。shell可以让你将多个命令串起来,一次执行完成。
如果仅是多个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开;
1 | date ; who |
使用这种办法就能将任意多个命令串连在一起使用了,只要不超过最大命令行字符数255就行。除此之外,我们可以创建shell脚本文件,注意必须在文件的第一行指定要使用的shell解释器。其格式为:
1 |
在通常的shell脚本中,井号(#)用作注释行。shell并不会处理shell脚本中的注释行。然而,shell脚本文件的第一行是个例外,#后面的惊叹号会告诉shell用哪个shell来运行脚本。
在指定了shell之后,就可以在文件的每一行中输入命令,然后加一个回车符。shell会按根据命令在文件中出现的顺序进行处理。
注意,不要忘了给写好的shell脚本赋执行权限。
变量
变量允许你临时性地将信息存储在shell脚本中,以便和脚本中的其他命令一起使用。
环境变量
shell维护着一组环境变量,用来记录特定的系统信息。比如系统的名称、登录到系统上的用户名、用户的系统ID(也称为UID)、用户的默认主目录以及shell查找程序的搜索路径。可以用set
命令来显示一份完整的当前环境变量列表。
在脚本中,可以在环境变量名称之前加上美元符$
来直接使用这些环境变量。
用户变量
除了环境变量,shell脚本还允许在脚本中定义和使用自己的变量。
- 用户变量可以是任何由字母、数字或下划线组成的文本字符串,长度不超过20个。
- 用户变量区分大小写,所以变量Var1和变量var1是不同的。
- 使用等号将值赋给用户变量。在变量、等号和值之间不能出现空格。
- 在脚本的整个生命周期里,shell脚本中定义的变量会一直保持着它们的值,但在shell脚本结束时会被删除掉。
要记住,引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用美元符。如果在引用时没有使用美元符,shell会将变量名解释成普通的文本字符串。
重定义变量
已定义的变量,可以被重新定义,如:
1 | your_name="qinjx" |
只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
1 | readonly var |
删除变量
使用 unset 命令可以删除变量。变量被删除后不能再次使用。unset 命令不能删除只读变量。语法:
1 | unset variable_name |
字符串
单引号
1 | str='this is a string' |
单引号字符串的限制:
- 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
- 单引号字串中不能出现单引号(对单引号使用转义符后也不行)。
双引号
1 | your_name='qinjx' |
双引号的优点:
- 双引号里可以有变量
- 双引号里可以出现转义字符
Shell 数组
bash支持一维数组(不支持多维数组),并且没有限定数组的大小。
定义数组
在 Shell 中,用括号来表示数组,数组元素用”空格”符号分割开。定义数组的一般形式为:数组名=(值1 值2 ... 值n)
还可以单独定义数组的各个分量:
1 | array_name[0]=value0 |
可以不使用连续的下标,而且下标的范围没有限制。
读取数组
读取数组元素值的一般格式是:
1 | # ${数组名[下标]} |
获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同:
1 | # 取得数组元素的个数 |
算术运算符
假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
+ | 加法 | expr $a + $b 结果为 30。 |
- | 减法 | expr $a - $b 结果为 -10。 |
* | 乘法 | expr $a \* $b 结果为 200。 |
/ | 除法 | expr $b / $a 结果为 2。 |
% | 取余 | expr $b % $a 结果为 0。 |
= | 赋值 | a=$b 将把变量 b 的值赋给 a。 |
== | 相等。用于比较两个数字,相同则返回 true。 | [ $a == $b ] 返回 false。 |
!= 不相等。用于比较两个数字,不相同则返回 true。 | [ $a != $b ] 返回 true。 |
消息输出
在echo命令后面加上了一个字符串,该命令就能显示出这个文本字符串。
默认情况下,不需要使用引号将要显示的文本字符串划定出来。但有时在字符串中出现引号的话就比较麻烦了。
1 | # 一个例子 |
echo命令可用单引号或双引号来划定文本字符串。如果在字符串中用到了它们,你需要在文本中使用其中一种引号,而用另外一种来将字符串划定起来。
1 | $ echo "you're joking" |
echo -n
可以把文本字符串和命令输出显示在同一行中:
1 | echo -n "The time and date are: " |
1 | echo "The cost of the item is $15" |
只要脚本在引号中出现美元符,它就会以为你在引用一个变量(
$15
未定义,为空)。要显示美元符,你必须在它前面放置一个反斜线,进行转义。
命令替换
shell脚本中最有用的特性之一就是可以从命令输出中提取信息,并将其赋给变量。
有两种方法可以将命令输出赋给变量:
- 反引号字符 (`)
- $()格式
命令替换允许你将shell命令的输出赋给变量。
1 | testing='date' |
重定向输入和输出
bash shell提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件)。重定向可以用于输入,也可以用于输出,可以将文件重定向到命令输入。
输出重定向
最基本的重定向将命令的输出发送到一个文件中。bash shell用大于号(>)来完成这项功能:
1 | $ date > test6 |
重定向操作符创建了一个文件test6(通过默认的umask设置),并将date命令的输出重定向到该文件中。如果输出文件已经存在了,重定向操作符会用新的文件数据覆盖已有文件。
不想覆盖文件原有内容,而是想要将命令的输出追加到已有文件中,可以用双大于号(>>)来追加数据。
输入重定向
输入重定向符号是小于号<
。
1 | $ wc < test6 |
wc命令可以对对数据中的文本进行计数。默认情况下,它会输出3个值:
- 文本的行数
- 文本的词数
- 文本的字节数
还有另外一种输入重定向的方法,称为内联输入重定向。内联输入重定向符号是远小于号(<<)。除了这个符号,你必须指定一个文本标记来划分输入数据的开始和结尾。任何字符串都可作为文本标记,但在数据的开始和结尾文本标记必须一致。
1 | command << marker |
管道
将命令输出直接重定向到另一个命令。这个过程叫作管道连接(piping)。
1 | command1 | command2 |
可以在一条命令中使用任意多条管道。可以持续地将命令的输出通过管道传给其他命令来细化操作。
expr 命令
expr命令允许在命令行上处理数学表达式:
操作符 | 描述 | |
---|---|---|
ARG1 \ | ARG2 | 如果ARG1既不是null也不是零值,返回ARG1;否则返回ARG2 |
ARG1 & ARG2 | 如果没有参数是null或零值,返回ARG1;否则返回0 | |
ARG1 < ARG2 | 如果ARG1小于ARG2,返回1;否则返回0 | |
ARG1 <= ARG2 | 如果ARG1小于或等于ARG2,返回1;否则返回0 | |
ARG1 = ARG2 | 如果ARG1等于ARG2,返回1;否则返回0 | |
ARG1 != ARG2 | 如果ARG1不等于ARG2,返回1;否则返回0 | |
ARG1 >= ARG2 | 如果ARG1大于或等于ARG2,返回1;否则返回0 | |
ARG1 > ARG2 | 如果ARG1大于ARG2,返回1;否则返回0 | |
ARG1 + ARG2 | 返回ARG1和ARG2的算术运算和 | |
ARG1 - ARG2 | 返回ARG1和ARG2的算术运算差 | |
ARG1 * ARG2 | 返回ARG1和ARG2的算术乘积 | |
ARG1 / ARG2 | 返回ARG1被ARG2除的算术商 | |
ARG1 % ARG2 | 返回ARG1被ARG2除的算术余数 | |
STRING : REGEXP | 如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配 | |
match STRING REGEXP | 如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配 | |
substr STRING POS LENGTH | 返回起始位置为POS(从1开始计数)、长度为LENGTH个字符的子字符串 | |
index STRING CHARS | 返回在STRING中找到CHARS字符串的位置;否则,返回0 | |
length STRING | 返回字符串STRING的数值长度 | |
+ TOKEN | 将TOKEN解释成字符串,即使是个关键字 | |
(EXPRESSION) | 返回EXPRESSION的值 |
- 许多expr命令操作符在shell中另有含义(比如星号)。要解决这个问题,对于那些容易被shell错误解释的字符,在它们传入expr命令之前,需要使用shell的转义字符(反斜线)将其标出来。
- 要将一个数学算式的结果赋给一个变量,需要使用命令替换来获取expr命令的输出:
var3=$(expr $var2 / $var1)
方括号
在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号($[ operation ])
将数学表达式围起来。
1 | $ var1=$[ 1 + 5 ] |
boolean 运算符
运算符 | 说明 | 举例 |
---|---|---|
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。 |
-o | 或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true。 |
-a | 与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |
逻辑运算符
运算符 | 说明 | 举例 | ||||
---|---|---|---|---|---|---|
&& | 逻辑的AND | [[ $a -lt 100 && $b -gt 100 ]] 返回 false |
||||
\ | \ | 逻辑的 OR | `[[ $a -lt 100 | $b -gt 100 ]]`返回 true |
数值比较
比较 | 描述 |
---|---|
n1 -eq n2 | 检查n1是否与n2相等 |
n1 -ge n2 | 检查n1是否大于或等于n2 |
n1 -gt n2 | 检查n1是否大于n2 |
n1 -le n2 | 检查n1是否小于或等于n2 |
n1 -lt n2 | 检查n1是否小于n2 |
n1 -ne n2 | 检查n1是否不等于n2 |
但是涉及浮点值时,数值条件测试会有一个限制。,bash shell只能处理整数。
字符串比较
比较 | 描述 |
---|---|
str1 = str2 | 检查str1是否和str2相同 |
str1 != str2 | 检查str1是否和str2不同 |
str1 < str2 | 检查str1是否比str2小 |
str1 > str2 | 检查str1是否比str2大 |
-n str1 | 检查str1的长度是否非0 |
-z str1 | 检查str1的长度是否为0 |
- 在比较字符串的相等性时,比较测试会将所有的标点和大小写情况都考虑在内。
- 要开始使用测试条件的大于或小于功能时,大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名;
- 大于和小于顺序和sort命令所采用的不同。
文件比较
比较 | 描述 |
---|---|
-d file | 检查file是否存在并是一个目录 |
-e file | 检查file是否存在 |
-f file | 检查file是否存在并是一个文件 |
-r file | 检查file是否存在并可读 |
-s file | 检查file是否存在并非空 |
-w file | 检查file是否存在并可写 |
-x file | 检查file是否存在并可执行 |
-O file | 检查file是否存在并属当前用户所有 |
-G file | 检查file是否存在并且默认组与当前用户相同 |
file1 -nt file2 | 检查file1是否比file2新 |
file1 -ot file2 | 检查file1是否比file2旧 |
浮点数
bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释并计算该
表达式,最后返回结果。bash计算器能够识别:
- 数字(整数和浮点数)
- 变量(简单变量和数组)
- 注释(以#或C语言中的/ /开始的行)
- 表达式
- 编程语句(例如if-then语句)
- 函数
可以在shell提示符下通过bc命令访问bash计算器,要退出bash计算器,你必须输入quit。
浮点运算是由内建变量scale控制的。必须将这个值设置为你希望在计算结果中保留的小数位数,否则无法得到期望的结果。
1 | $ bc -q |
scale变量的默认值是0。在scale值被设置前,bash计算器的计算结果不包含小数位。在将其值设置成4后,bash计算器显示的结果包含四位小数。-q命令行选项可以不显示bash计算器冗长的欢迎信息。
脚本中使用bc: variable=$(echo "options; expression" | bc)
;第一部分options允许你设置变量。如果你需要不止一个变量,可以用分号将其分开。expression参数定义了通过bc执行的数学表达式。
如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。bc命令能识别输入重定向,最好的办法是使用内联输入重定向,它允许你直接在命令行中重定向数据。在shell脚本中,你可以将输出赋给一个变量。
1 | variable=$(bc << EOF |
退出脚本
shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。
Linux提供了一个专门的变量$?
来保存上个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用$?
变量。它的值会变成由shell所执行的最后一条命令的退出状态码。
状态码 | 描述 |
---|---|
0 | 命令成功结束 |
1 | 一般性未知错误 |
2 | 不适合的shell命令 |
126 | 命令不可执行 |
127 | 没找到命令 |
128 | 无效的退出参数 |
128+x | 与Linux信号x相关的严重错误 |
130 | 通过Ctrl+C终止的命令 |
255 | 正常范围之外的退出状态码 |
exit 命令
默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。你可以改变这种默认行为,返回自己的退出状态码。exit命令允许你在脚本结束时指定一个退出状态码。
1 | exit $code |
如果退出状态码大于255,那么会被被缩减到了0~255的区间。shell通过模运算得到这个结果。一个值的模就是被除后的余数。最终的结果是指定的数值除以256后得到的余数。
if-then
1 | if command |
bash shell的if语句会运行if后面的那个命令。如果该命令的退出状态码(参见第11章)是0(该命令成功运行),位于then部分的命令就会被执行。如果该命令的退出状态码是其他值, then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。fi语句用来表示if-then语句到此结束。
if-then-else
1 | if command; then |
elif
1 | if command1;then |
,在elif语句中,紧跟其后的else语句属于elif代码块。它们并不属于之前的if-then代码块。其实质是嵌套的if-else
test
test命令提供了在if-then语句中测试不同条件的途径。如果test命令中列出的条件成立,test命令就会退出并返回退出状态码0。这样if-then语句就与其他编程语言中的if-then语句以类似的方式工作了。如果条件不成立,test命令就会退出并返回非零的退出状态码,这使得if-then语句不会再被执行。
test命令的格式非常简单: test condition
。
1 | if test condition |
如果不写test命令的condition部分,它会以非零的退出状态码退出,并执行else语句块。
bash shell提供了另一种条件测试方法,无需在if-then语句中声明test命令。
1 | if [ condition ] |
test命令可以判断三类条件:
- 数值比较
- 字符串比较
- 文件比较
数值比较
比较 | 描述 |
---|---|
n1 -eq n2 | 检查n1是否与n2相等 |
n1 -ge n2 | 检查n1是否大于或等于n2 |
n1 -gt n2 | 检查n1是否大于n2 |
n1 -le n2 | 检查n1是否小于或等于n2 |
n1 -lt n2 | 检查n1是否小于n2 |
n1 -ne n2 | 检查n1是否不等于n2 |
但是涉及浮点值时,数值条件测试会有一个限制。,bash shell只能处理整数。
字符串比较
比较 | 描述 |
---|---|
str1 = str2 | 检查str1是否和str2相同 |
str1 != str2 | 检查str1是否和str2不同 |
str1 < str2 | 检查str1是否比str2小 |
str1 > str2 | 检查str1是否比str2大 |
-n str1 | 检查str1的长度是否非0 |
-z str1 | 检查str1的长度是否为0 |
- 在比较字符串的相等性时,比较测试会将所有的标点和大小写情况都考虑在内。
- 要开始使用测试条件的大于或小于功能时,大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名;
- 大于和小于顺序和sort命令所采用的不同。
文件比较
比较 | 描述 |
---|---|
-d file | 检查file是否存在并是一个目录 |
-e file | 检查file是否存在 |
-f file | 检查file是否存在并是一个文件 |
-r file | 检查file是否存在并可读 |
-s file | 检查file是否存在并非空 |
-w file | 检查file是否存在并可写 |
-x file | 检查file是否存在并可执行 |
-O file | 检查file是否存在并属当前用户所有 |
-G file | 检查file是否存在并且默认组与当前用户相同 |
file1 -nt file2 | 检查file1是否比file2新 |
file1 -ot file2 | 检查file1是否比file2旧 |
复合条件测试
if-then语句允许你使用布尔逻辑来组合测试。有两种布尔运算符可用:
- 与: [ condition1 ] && [ condition2 ]
- 或: [ condition1 ] || [ condition2 ]
if-then 的高级特性
使用双括号
双括号命令允许你在比较过程中使用高级数学表达式。test命令只能在比较中使用简单的算术操作。双括号命令提供了更多的数学符号。
1 | (( expression )) |
符号 | 描述 | ||
---|---|---|---|
val++ | 后增 | ||
val– | 后减 | ||
++val | 先增 | ||
–val | 先减 | ||
! | 逻辑求反 | ||
~ | 位求反 | ||
** | 幂运算 | ||
<< | 左位移 | ||
>> | 右位移 | ||
& | 位布尔和 | ||
\ | 位布尔或 | ||
&& | 逻辑和 | ||
\ | \ | 逻辑或 |
使用双方括号
双方括号命令提供了针对字符串比较的高级特性。双方括号命令的格式如下:
1 | [[ expression ]] |
双方括号里的expression使用了test命令中采用的标准字符串比较。但它提供了test命令未提供的另一个特性——模式匹配(pattern matching)。
在模式匹配中,可以定义一个正则表达式来匹配字符串值:
1 | if [[ $USER == r* ]] |
case 命令
有了case命令,就不需要再写出所有的elif语句来不停地检查同一个变量的值了。case命令会采用列表格式来检查单个变量的多个值。
1 | case variable in |
case命令会将指定的变量与不同模式进行比较。如果变量和模式是匹配的,那么shell会执行为该模式指定的命令。可以通过竖线操作符在一行中分隔出多个模式模式。星号会捕获所有与已知模式不匹配的值。
1 | case $USER in |
for each
1 | for var in list |
在list参数中,你需要提供迭代中要用到的一系列值。可以通过几种不同的方法指定列表
中的值。
读取列表中的值
for命令最基本的用法就是遍历for命令自身所定义的一系列值。
1 | for test in Alabama Alaska Arizona Arkansas California Colorado |
每次for命令遍历值列表,它都会将列表中的下个值赋给$test
变量。$test
变量可以像for
命令语句中的其他脚本变量一样使用。在最后一次迭代后,$test
变量的值会在shell脚本的剩余
部分一直保持有效。
注意,当列表中存在特殊字符,如引号或是空格时需要注意。
从变量读取列表
还有一种情况是,你将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。
1 | list="Alabama Alaska Arizona Arkansas Colorado" |
从命令读取值
生成列表中所需值的另外一个途径就是使用命令的输出。可以用命令替换来执行任何能产生
输出的命令。
1 | file="states" |
更改字段分隔符
内部字段分隔符(internal field separator)环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:
- 空格
- 制表符
- 换行符
如果bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦。
要解决这个问题,可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。例如,如果你想修改IFS的值,使其只能识别换行符,那就必须这么做IFS=$'\n'
;
在处理代码量较大的脚本时,可能在一个地方需要修改IFS的值,然后忽略这次修改,在脚本的其他地方继续沿用IFS的默认值。一个可参考的安全实践是在改变IFS之前保存原来的IFS值,之后再恢复它。这种技术可以这样实现:
1 | IFS.OLD=$IFS |
用通配符读取目录
可以用for命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。
1 | # 在for命令中列出多个目录通配符,将目录查找和列表合并进同一个for语句 |
类c风格的for循环
1 | # for (( variable assignment ; condition ; iteration process )) |
注意,有些部分并没有遵循bash shell标准的for命令:
- 变量赋值可以有空格;
- 条件中的变量不以美元符开头;
- 迭代过程的算式未用expr命令格式。
C语言风格的for命令也允许为迭代使用多个变量。循环会单独处理每个变量,你可以为每个变量定义不同的迭代过程。尽管可以使用多个变量,但你只能在for循环中定义一种条件。
1 | for (( a=1, b=10; a <= 10; a++, b-- )) |
while 命令
1 | while test command |
while命令允许你在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。在含有多个命令的while语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一次迭代。要留心这种用法。
1 | while echo $var1 |
until 命令
until命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结束了。
1 | until test commands |
和while命令类似,你可以在until命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了bash shell是否执行已定义的other commands。
控制循环
break 命令
break命令是退出循环的一个简单方法。可以用break命令来退出任意类型的循环,包括while和until循环。
在处理多个循环时,break命令会自动终止你所在的最内层的循环。在内部循环,但需要停止外部循环。break命令接受单个命令行参数值:
1 | break n |
其中n指定了要跳出的循环层级。默认情况下,n为1,表明跳出的是当前的循环。如果你将n设为2,break命令就会停止下一级的外部循环。
continue 命令
continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置shell不执行命令的条件。和break命令一样,continue命令也允许通过命令行参数指定要继续执行哪一级循环:
1 | continue n |
其中n定义了要继续的循环层级。
处理循环的输出
在shell脚本中,你可以对循环的输出使用管道或进行重定向。这可以通过在done命令之后添加一个处理命令来实现。
1 | # 重定向例子 |