shell脚本编程基础
一、构建基本脚本
创建shell基本
文件的第一行指定要使用的shell
1 |
写好一个文件之后需要执行,但是必须要让shell找到脚本
- 将shell脚本文件所处的目录添加到PATH环境变量中;
- 在提示符中用绝对或相对文件路径来引用shell脚本文件。
显示消息
果在echo
命令后面加上了一个字符串,该命令就能显示出这个文本字符串。
默认情况下,不需要使用引号将要显示的文本字符串划定出来。echo命令可用单引号或双引号来划定文本字符串。需要在文本中使用其中一种引号,而用另外一种来将字符串划定起来。
1 | echo "This is a test to see if you're paying attention" |
如果想把文本字符串和命令输出显示在同一行中,用echo语句的-n
参数。
1 | echo -n "The time and date are: " |
使用变量
环境变量
以在环境变量名称之前加上美元符($)来使用这些环境变量。
1 | echo "User info for userid: $USER" |
${variable}形式引用的变量。变量名两侧额外的花括号通常用来帮助识别美元符后的变量名。
用户变量
使用等号将值赋给用户变量。在变量、等号和值之间不能出现空格。
在脚本的整个生命周期里,shell脚本中定义的变量会一直保持着它们的值
1 | days=10 |
变量每次被引用时,都会输出当前赋给它的值。引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用$。
命令替换
shell脚本可以从命令输出中提取信息,并将其赋给变量。
有两种方法可以将命令输出赋给变量:
- 反引号字符(`)
- $()格式
命令替换允许你将shell命令的输出赋给变量。
1 | testing=`date` |
shell会运行命令替换符号中的命令,并将其输出赋给变量testing。
1 | #在脚本中通过命令替换获得当前日期并用它来生成唯一文件名。 |
命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。
使用路径./运行命令的话,也会创建出子shell;要是运行命令的时候不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。
重定向输入和输出
你想要保存某个命令的输出而不仅仅只是让它显示在显示器上。
输出重定向
1 | #重定向将命令的输出发送到一个文件中。> |
输入重定向
输入重定向将文件的内容重定向到命令
1 | command < inputfile |
管道
有时需要将一个命令的输出作为另一个命令的输入。
我们不用将命令输出重定向到文件中,可以将其直接重定向到另一个命令。这个过程叫作管道连接(piping)。
1 | command1 | command2 |
在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。
可以在一条命令中使用任意多条管道。可以持续地将命令的输出通过管道传给其他命令来细化操作。
1 | $ rpm -qa | sort | more |
执行数学运算
expr
expr命令允许在命令行上处理数学表达式
expr命令能够识别少数的数学和字符串操作符
1 | #许多expr命令操作符在shell中另有含义(比如星号)。当它们出现在在expr命令中时,会得到一些诡异的结果。 |
使用方括号
在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号($[ operation ]
)将数学表达式围起来。
1 | $ var1=$[1 + 5] |
在bash shell脚本中进行算术运算会有一个主要的限制。
1 |
|
bash shell数学运算符只支持整数运算。
浮点解决方案
用内建的bash计算器,叫作bc。
bc基本用法
允许在命令行中输入浮点表达式,然后解释并计算该表达式可以识别
1
2
3
4
5
6数字(整数和浮点数)
变量(简单变量和数组)
注释(以#或C语言中的/* */开始的行)
表达式
编程语句(例如if-then语句)
函数浮点运算是由内建变量scale控制的。必须将这个值设置为你希望在计算结果中保留的小数位
scale变量的默认值是0。在scale值被设置前,bash计算器的计算结果不包含小数位。在将其值设置成4后,bash计算器显示的结果包含四位小数。-q命令行选项可以不显示bash计算器冗长的欢迎信息.
1
2
3
4
5
6
7$ bc -q
3.44 / 5
0
scale=4
3.44 / 5
.6880
quit在脚本中使用bc
可以用命令替换运行bc命令,并将输出赋给一个变量。
1
2
3
4
5#第一部分options允许你设置变量。如果你需要不止一个变量,可以用分号将其分开。expression参数定义了通过bc执行的数学表达式。
variable=$(echo "options; expression" | bc)
var1=$(echo "scale=4; 3.44 / 5" | bc)
echo The answer is $var1如果需要进行大量运算,在一个命令行中列出多个表达式就会有点麻烦。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#用内联输入重定向,它允许你直接在命令行中重定向数据。,仍然需要命令替换符号将bc命令的输出赋给变量。
variable=$(bc << EOF
options
statements
expressions
EOF
)
var1=10.46
var2=43.67
var3=33.2
var4=71
var5=$(bc << EOF
scale = 4
a1 = ( $var1 * $var2)
b1 = ($var3 * $var4)
a1 + b1
EOF
)
echo The final answer for this mess is $var5在bash计算器中创建的变量只在bash计算器中有效,不能在shell脚本中使用。
退出脚本
shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。
查看退出状态码
$?
来保存上个已执行命令的退出状态码。对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用$?变量。它的值会变成由shell所执行的最后一条命令的退出状态码。
一个成功结束的命令的退出状态码是0。如果一个命令结束时有错误,退出状态码就是一个正数值。
状 态 码 | 描 述 |
---|---|
0 | 命令成功结束 |
1 | 一般性未知错误 |
2 | 不适合的shell命令 |
126 | 命令不可执行 |
127 | 没找到命令 |
128 | 无效的退出参数 |
128+x | 与Linux信号x相关的严重错误 |
130 | 通过Ctrl+C终止的命令 |
255 | 正常范围之外的退出状态码 |
exit 命令
shell脚本会以脚本中的最后一个命令的退出状态码退出。
exit
命令允许你在脚本结束时指定一个退出状态码。
因为退出状态码最大只能是255。超出这个区间,退出状态码被缩减到了0~255的区间。shell通过模运算得到这个结果。一个值的模就是被除后的余数。最终的结果是指定的数值除以256后得到的余数。
二、结构化命令
使用if-then 语句
1 | if command |
bash shell的if语句会运行if后面的那个命令。如果该命令的退出状态码是0(该命令成功运行),位于then部分的命令就会被执行。。如果该命令的退出状态码是其他值,then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。fi语句用来表示if-then语句到此结束。
if command; then
commands
fi通过把分号放在待求值的命令尾部,就可以将then语句放在同一行上了,这样看起来更像其他编程语言中的if-then语句。
在then部分,你可以使用不止一条命令。可以像在脚本中的其他地方一样在这里列出多条命令。bash shell会将这些命令当成一个块
if-then-else 语句
1 | if command |
当if语句中的命令返回退出状态码0时,then部分中的命令会被执行,这跟普通的if-then语句一样。当if语句中的命令返回非零退出状态码时,bash shell会执行else部分中的命令。
嵌套 if
elif。这样就不用再书写多个if-then语句了。elif使用另一个if-then语句延续else部分。
1 | if command1 |
elif语句行提供了另一个要测试的命令,这类似于原始的if语句行。如果elif后命令的退出状态码是0,则bash会执行第二个then语句部分的命令。使用这种嵌套方法,代码更清晰,逻辑更易懂。
test 命令
if-then语句是否能测试命令退出状态码之外的条件。答案是不能。
test
命令提供了在if-then语句中测试不同条件的途径。如果test命令中列出的条件成立,test命令就会退出并返回退出状态码0。
1 | test condition |
bash shell提供了另一种条件测试方法,无需在if-then语句中声明test命令。
1 | if [ condition ] |
第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。
test命令可以判断三类条件:
- 数值比较
- 字符串比较
- 文件比较
1. 数值比较
比 较 | 描 述 | |
---|---|---|
n1 -eq n2 | 检查n1是否与n2相等 | equal |
n1 -ge n2 | 检查n1是否大于或等于n2 | great than or equal |
n1 -gt n2 | 检查n1是否大于n2 | greater than |
n1 -le n2 | 检查n1是否小于或等于n2 | less than or equal |
n1 -lt n2 | 检查n1是否小于n2 | less than |
n1 -ne n2 | 检查n1是否不等于n2 | not equal |
2.字符串比较
条件测试还允许比较字符串值。比较字符串比较烦琐,你马上就会看到。
比 较 | 描 述 |
---|---|
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会把它们当作重定向符号,把字符串值当作文件名;
1 |
|
在比较测试中,大写字母被认为是小于小写字母的。但sort命令恰好相反。当你将同样的字符串放进文件中并用sort命令排序时,小写字母会先出现。这是由各个命令使用的排序技术不同造成的。
比较测试中使用的是标准的ASCII顺序,根据每个字符的ASCII数值来决定排序结果。sort命令使用的是系统的本地化语言设置中定义的排序顺序。对于英语,本地化设置指定了在排序顺序中小写字母出现在大写字母前。
3.文件比较
允许你测试Linux文件系统上文件和目录的状态。test命令的比较功能。
比 较 | 描 述 |
---|---|
-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语句允许你使用布尔逻辑来组合测试。有两种布尔运算符可用:
1 | [ condition1 ] && [ condition2 ] |
要让then部分的命令执行,两个条件都必须满足。
1 |
|
if-then 的高级特性
用于数学表达式的双括号
test命令只能在比较中使用简单的算术操作。双括号命令提供了更多的数学符号
1 | (( expression )) |
双括号命令符号
符 号 | 描 述 |
---|---|
val++ | 后增 |
val– | 后减 |
++val | 先增 |
–val | 先减 |
! | 逻辑求反 |
~ | 位求反 |
** | 幂运算 |
<< | 左位移 |
>> | 右位移 |
& | 位布尔和 |
| | 位布尔或 |
&& | 逻辑和 |
|| | 逻辑或 |
1 |
|
用于高级字符串处理功能的双方括号
1 | [[ expression ]] |
它提供了test命令未提供的另一个特性——模式匹配(pattern matching)。
1 |
|
case 命令
在一组可能的值中寻找特定值。
1 | case variable in |
case命令会将指定的变量与不同模式进行比较。可以通过竖线操作符在一行中分隔出多个模式模式。星号会捕获所有与已知模式不匹配的值。
1 |
|
三、循环结构化指令
for 命令
1 | for var in list |
在list参数中,你需要提供迭代中要用到的一系列值。
1.读取列表中的值
1 |
|
每次for命令遍历值列表,它都会将列表中的下个值赋给$test变量。$test变量可以像for命令语句中的其他脚本变量一样使用。在最后一次迭代后,$test变量的值会在shell脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值。
2.读取列表中的复杂值
有时会遇到难处理的数据。
1 |
|
有2个解决方法
- 使用转义字符(反斜线)来将单引号转义;
- 使用双引号来定义用到单引号的值。
1 |
|
3.从变量读取列表
将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。也可以通过for命令完成这个任务。
1 |
|
4. 从命令读取值
可以用命令替换来执行任何能产生输出的命令,然后在for命令中使用该命令的输出。
1 |
|
并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的单词,for命令仍然会将每个单词当作单独的值。
5.更改字段分隔符
特殊的环境变量IFS,叫作内部字段分隔符(internal field separator)。
IFS环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:
- 空格
- 制表符
- 换行符
bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。
可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。
1 | IFS=$'\n' |
1 |
|
在处理代码量较大的脚本时,可能在一个地方需要修改IFS的值,然后忽略这次修改,在脚本的其他地方继续沿用IFS的默认值。一个可参考的安全实践是在改变IFS之前保存原来的IFS值,之后再恢复它。
1
2
3
4 IFS.OLD=$IFS
IFS=$'\n'
<在代码中使用新的IFS值>
IFS=$IFS.OLD保证了在脚本的后续操作中使用的是IFS的默认值。
要遍历一个文件中用冒号分隔的值
1 | IFS=: |
果要指定多个IFS字符,只要将它们在赋值行串起来就行。
1 | IFS=$'\n':;" |
6.用通配符读取目录
可以用for命令来自动遍历目录中的文件。必须在文件名或路径名中使用通配符。它会强制shell使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程
1 |
|
在Linux中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将$file变量用双引号圈起来。
也可以在for命令中列出多个目录通配符,将目录查找和列表合并进同一个for语句。
1 |
|
可以在数据列表中放入任何东西。即使文件或目录不存在,for语句也会尝试处理列表中的内容。在处理文件或目录时,这可能会是个问题。你无法知道你正在尝试遍历的目录是否存在:在处理之前测试一下文件或目录总是好的
C语言风格的for
1 | for (( variable assignment ; condition ; iteration process )) |
1 |
|
r循环通过定义好的变量(本例中是变量i)来迭代执行这些命令。
如何使用多个变量
1 |
|
while 命令
是if-then语句和for循环的混杂体
1 | while test command |
test command的退出状态码必须随着循环中运行的命令而改变。如果退出状态码不发生变化, while循环就将一直不停地进行下去
1 |
|
多个测试命令
while命令允许你在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。
1 |
|
echo测试命令被执行并显示了var变量的值(现在小于0了)。直到shell执行test测试命令,whle循环才会停止。
until
until命令要求你指定一个通常返回非零退出状态码的测试命令。
1 | until test commands |
可以在until命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了bash shell是否执行已定义的other commands。
1 |
|
嵌套循环
循环语句可以在循环内使用任意类型的命令
1 |
|
循环处理文件数据
使用嵌套循环
修改IFS环境变量
通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即便数据中有空格也是如此。一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据。
1 |
|
使用了两个不同的IFS值来解析数据。第一个IFS值解析出/etc/passwd文件中的单独的行。内部for循环接着将IFS的值修改为冒号,允许你从/etc/passwd的行中解析出单独的值。
控制循环
break 命令
break命令来退出任意类型的循环
跳出单个循环
1
2
3
4
5
6
7
8
9
10
11
# breaking out of a for loop
for var1 in 1 2 3 4 5 6 7 8 9 10
do
if [ $var1 -eq 5 ]
then
break
fi
echo "Iteration number: $var1"
done
echo "The for loop is completed"跳出内部循环
处理多个循环时,break命令会自动终止你所在的最内层的循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# breaking out of an inner loop
for (( a = 1; a < 4; a++ ))
do
echo "Outer loop: $a"
for (( b = 1; b < 100; b++ ))
do
if [ $b -eq 5 ]
then
break
fi
echo " Inner loop: $b"
done
done跳出外部循环
break命令接受单个命令行参数值
break n
其中n指定了要跳出的循环层级。默认情况下,n为1,表明跳出的是当前的循环。如果你将n设为2,break命令就会停止下一级的外部循环。1
2
3
4
5
6
7
8
9
10
11
12
13
14
# breaking out of an outer loop
for (( a = 1; a < 4; a++ ))
do
echo "Outer loop: $a"
for (( b = 1; b < 100; b++ ))
do
if [ $b -gt 4 ]
then
break 2
fi
echo " Inner loop: $b"
done
done
continue 命令
提前中止某次循环中的命令,但并不会完全终止整个循环。
1 |
|
continue命令也允许通过命令行参数指定要继续执行哪一级循环:continue n
处理循环的输出输入
可以对循环的输出使用管道或进行重定向。这可以通过在done命令之后添加一个处理命令来实现。
1 | for file in /home/rich/* |
shell会将for命令的结果重定向到文件output.txt中,而不是显示在屏幕上。
读取文件的另外一种方式
1 | while read -r line |
四、处理用户的输入
命令行参数
向shell脚本传递数据的最基本方法是使用命令行参数。命令行参数允许在运行脚本时向命令行添加数据。
1 | $ ./addem 10 30 |
1.读取参数
bash shell会将一些称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括shell所执行的脚本名称。位置参数变量是标准的数字:$0是程序名,$1是第一个参数,$2是第二个参数,依次类推,直到第九个参数$9。
每个参数都是用空格分隔的,所以shell会将空格当成两个值的分隔符。要在参数值中包含空格,必须要用引号
1 | ./test3.sh 'Rich Blum' |
将文本字符串作为参数传递时,引号并非数据的一部分。它们只是表明数据的起止位置。
如果脚本需要的命令行参数不止9个,你仍然可以处理,但是需要稍微修改一下变量名。在第9个变量之后,你必须在变量数字周围加上花括号,比如${10}。
2.读取脚本名
用$0
参数获取shell在命令行启动的脚本名。
如果使用另一个命令来运行shell脚本,命令会和脚本名混在一起,出现在$0参数中。
1 | ./test5.sh |
得把脚本的运行路径给剥离掉。另外,还要删除与脚本名混杂在一起的命令。basename命令会返回不包含路径的脚本名。
1 |
|
3.测试参数
当脚本认为参数变量中会有数据而实际上并没有时,脚本很有可能会产生错误消息。这种写脚本的方法并不可取。在使用参数前一定要检查其中是否存在数据。
1 |
|
特殊参数变量
1.参数统计
特殊变量$#含有脚本运行时携带的命令行参数的个数。
1 | if [ $# -ne 2 ] |
if-then语句用-ne
测试命令行参数数量。如果参数数量不对,会显示一条错误消息告知脚本的正确用法。
1 |
|
2.抓取所有的数据
有时候需要抓取命令行上提供的所有参数。这时候不需要先用$#变量来判断命令行上有多少参数,然后再进行遍历,使用一组其他的特殊变量来解决这个问题。
$*
和$@
变量可以用来轻松访问所有的参数。
$*
变量会将命令行上提供的所有参数当作一个单词保存。基本上$*变量会将这些参数视为一个整体,而不是多个个体。$@
变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。就能够遍历所有的参数值,得到每个参数。
1 |
|
移动变量
bash shell的shift命令能够用来操作命令行参数。shift命令会根据它们的相对位置来移动命令行参数。
在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除
是在你不知道到底有多少参数时。你可以只操作第一个参数,移动参数,然后继续操作第一个参数。
1 | $ cat test13.sh |
也可以一次性移动多个位置,只需要给shift命令提供一个参数,指明要移动的位置数就行了。shift 2
处理选项
同时提供了参数和选项的bash命令。选项是跟在单破折线后面的单个字母,它能改变命令的行为。
1.查找选项
紧跟在脚本名之后,就跟命令行参数一样。
处理简单选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15用case语句来判断某个参数是否为选项。
#!/bin/bash
# extracting command line options as parameters
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) echo "Found the -b option" ;;
-c) echo "Found the -c option" ;;
*) echo "$1 is not an option" ;;
esac
shift
donecase语句在命令行参数中找到一个选项,就处理一个选项。如果命令行上还提供了其他参数,你可以在case语句的通用情况处理部分中处理。
分离参数和选项
同时使用选项和参数的情况。用特殊字符来将二者分开,这个特殊字符是双破折线(
--
)。shell会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# extracting options and parameters
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) echo "Found the -b option";;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
#
count=1
for param in $@
do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done在遇到双破折线时,脚本用break命令来跳出while循环。由于过早地跳出了循环,我们需要再加一条shift命令来将双破折线移出参数变量。
1
2
3
4
5
6
7./test16.sh -c -a -b -- test1 test2 test3
Found the -c option
Found the -a option
Found the -b option
Parameter #1: test1
Parameter #2: test2
Parameter #3: test3当脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作命令行参数。
处理带值的选项
有些选项会带上一个额外的参数值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# extracting command line options and values
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option";;
-b) param="$2"
echo "Found the -b option, with parameter value $param" shift ;;
-c) echo "Found the -c option";;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
#
count=1
for param in "$@"
do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done
./test17.sh -a -b test1 -d
Found the -a option
Found the -b option, with parameter value test1
-d is not an optioncase语句定义了三个它要处理的选项。-b选项还需要一个额外的参数值。由于要处理的参数是$1,额外的参数值就应该位于$2(因为所有的参数在处理完之后都会被移出)。只要将参数值从$2变量中提取出来就可以了。当然,因为这个选项占用了两个参数位,所以你还需要使用shift命令多移动一个位置。
如果你想将多个选项放进一个参数中时,它就不能工作了。
2.使用 getopt 命令
它能够识别命令行参数,从而在脚本中解析它们时更方便。
命令的格式
接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。
1
getopt optstring parameters
optstring
它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。在optstring中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数。1
2getopt ab:cd -a -b test1 -cd test2 test3
-a -b test1 -c -d -- test2 test3optstring定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,因为b选项需要一个参数值。
如果指定了一个不在optstring中的选项,默认情况下,getopt命令会产生一条错误消息。
1
2
3getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- e
-a -b test1 -c -d -- test2 test3如果想忽略这条错误消息,可以在命令后加-q选项。
在脚本中使用getopt
用getopt命令生成的格式化后的版本来替换已有的命令行选项和参数。用set命令.
set命令的选项之一是双破折线(–),它会将命令行参数替换成set命令的命令行值。该方法会将原始脚本的命令行参数传给getopt命令,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数。
1
set -- $(getopt -q ab:cd "$@")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
# 处理参数
count=1
for param in "$@"
do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done在getopt命令中仍然隐藏着一个小问题。
1
2
3
4
5
6
7./test18.sh -a -b test1 -cd "test2 test3" test4
Found the -a option
Found the -b option, with parameter value 'test1'
Found the -c option
Parameter #1: 'test2
Parameter #2: test3'
Parameter #3: 'test4'getopt命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。
使用更高级的
getopts
内建于bash shell。与
getopt
不同,前者将命令行上选项和参数处理后只生成一个输出,而getopts
命令能够和已有的shell参数变量配合默契。每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。
1
getopts optstring variable
类似于getopt的处理,要去掉错误消息的话,可以在optstring之前加一个冒号。
getopts
命令会用到两个环境变量。如果选项需要跟一个参数值,OPTARG环境变量就会保存这个值。OPTIND环境变量保存了参数列表中getopts正在处理的参数位置。这样就能在处理完选项之后继续处理其他命令行参数了。1
2
3
4
5
6
7
8
9
10
11
12
13
# simple demonstration of the getopts command
#
echo
while getopts :ab:c opt
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG";;
c) echo "Found the -c option" ;;
*) echo "Unknown option: $opt";;
esac
done每次迭代中存储它们的变量名(opt)。getopts命令解析命令行选项时会移除开头的单破折线,所以在case定义中不用单破折线。
可以在参数值中包含空格。
1
./test19.sh -b "test1 test2" -a
将选项字母和参数值放在一起使用,而不用加空格。
1
./test19.sh -abtest1
能够将命令行上找到的所有未定义的选项统一输出成问号。
1
2./test19.sh -d
Unknown option: ?optstring中未定义的选项字母会以问号形式发送给代码。
getopts命令知道何时停止处理选项,并将参数留给你处理。在getopts处理每个选项时,它会将OPTIND环境变量值增一。在getopts完成处理时,你可以使用shift命令和OPTIND值来移动参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Processing options & parameters with getopts
#
echo
while getopts :ab:cd opt
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG" ;;
c) echo "Found the -c option" ;;
d) echo "Found the -d option" ;;
*) echo "Unknown option: $opt" ;;
esac
done
#
shift $[ $OPTIND - 1 ]
#
echo
count=1
for param in "$@"
do
echo "Parameter $count: $param"
count=$[ $count + 1 ]
done
$ ./test20.sh -a -b test1 -d test2 test3 test4
Found the -a option
Found the -b option, with value test1
Found the -d option
Parameter 1: test2
Parameter 2: test3
Parameter 3: test4
将选项标准化
有些字母选项在Linux世界里已经拥有了某种程度的标准含义。如果能在shell脚本中支持这些选项,脚本看起来能更友好一些。常用的Linux命令选项:
选 项 | 描 述 |
---|---|
-a | 显示所有对象 |
-c | 生成一个计数 |
-d | 指定一个目录 |
-e | 扩展一个对象 |
-f | 指定读入数据的文件 |
-h | 显示命令的帮助信息 |
-i | 忽略文本大小写 |
-l | 产生输出的长格式版本 |
-n | 使用非交互模式(批处理) |
-o | 将所有输出重定向到的指定的输出文件 |
-q | 以安静模式运行 |
-r | 递归地处理目录和文件 |
-s | 以安静模式运行 |
-v | 生成详细输出 |
-x | 排除某个对象 |
-y | 对所有问题回答yes |
获得用户输入
1. 基本的读取
read命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量。
1 |
|
read命令包含了-p
选项,允许你直接在read命令行指定提示符。
read命令会将提示符后输入的所有数据分配给单个变量,要么你就指定多个变量。输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量。
在read命令行中不指定变量,read命令会将它收到的任何数据都放进特殊环境变量REPLY中。
1 |
|
2.超时
用-t
选项来指定一个计时器。-t选项指定了read命令等待输入的秒数。当计时器过期后,read命令会返回一个非零退出状态码。
1 |
|
让read命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量。
1 |
|
将-n
选项和值1一起使用,告诉read命令在接受单个字符后退出。
3.隐藏方式读取
-s选项可以避免在read命令中输入的数据出现在显示器上上(实际上,数据会被显示,只是read命令会将文本颜色设成跟背景色一样)。
1 |
|
4.从文件中读取
read命令来读取Linux系统上文件里保存的数据。每次调用read命令,它都会从文件中读取一行文本。
1 |
|
五、呈现数据
理解输入和输出
脚本输出的方法:
- 在显示器屏幕上显示输出
- 将输出重定向到文件中
1.标准文件描述符
Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(filedescriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。每个进程一次最多可以有九个文件描述符。bash shell保留了前三个文件描述符(0、1和2)
文件描述符 | 缩 写 | 描 述 |
---|---|---|
0 | STDIN | 标准输入 |
1 | STDOUT | 标准输出 |
2 | STDERR | 标准错误 |
STDIN: 代表shell的标准输入。对终端界面来说,标准输入是键盘。shell从STDIN文件描述符对应的键盘获得输入,在用户输入时处理每个字符。在使用输入重定向符号(<)时,Linux会用重定向指定的文件来替换标准输入文件描述符。它会读取文件并提取数据,就如同它是键盘上键入的。
1
2
3
4
5
6
7
8
9
10$ cat
this is a test
this is a test
this is a second test.
this is a second test.
$ cat < testfile
This is the first line.
This is the second line.
This is the third line.STDOUT :符代表shell的标准输出。在终端界面上,标准输出就是终端显示器。shell的所有输出(包括shell中运行的程序和脚本)会被定向到标准输出中,也就是显示器。
>
覆盖,>>
追加。如果你对脚本使用了标准输出重定向:
1
2$ ls -al badfile > test3
ls: cannot access badfile: No such file or directoryshell创建了输出重定向文件,但错误消息却显示在了显示器屏幕上。注意,在显示test3文件的内容时并没有任何错误。test3文件创建成功了,只是里面是空的。
shell对于错误消息的处理是跟普通输出分开的。如果你创建了在后台模式下运行的shell脚本,通常你必须依赖发送到日志文件的输出消息。用这种方法的话,如果出现了错误信息,这些信息是不会出现在日志文件中的。你需要换种方法来处理。
STDERR 代表shell的标准错误输出。shell或shell中运行的程序和脚本出错时生成的错误消息都会发送到这个位置。默认情况下,STDERR文件描述符会和STDOUT文件描述符指向同样的地方。也就是说,默认情况下,错误消息也会输出到显示器输出中。
但是,STDERR并不会随着STDOUT的重定向而发生改变。使用脚本时,也希望将错误消息保存到日志文件中的时候。
2.重定向错误
只重定向错误:STDERR文件描述符被设成2。可以选择只重定向错误消息,将该文件描述符值放在重定向符号前。
1
ls -al badfile 2> test4
重定向错误和数据:用两个重定向符号。
1
ls -al test test2 test3 badtest 2> test6 1> test7
也可以将STDERR和STDOUT的输出重定向到同一个输出文件。bash shell提供了特殊的重定向符号
&>
。1
ls -al test test2 test3 badtest &> test7
相较于标准输出,bash shell自动赋予了错误消息更高的优先级。这样能够集中浏览错误信息了。
在脚本中重定向输出
脚本中用STDOUT和STDERR文件描述符以在多个位置生成输出,只要简单地重定向相应的文件描述符就行了。
1.临时重定向
可以将单独的一行输出重定向到STDERR。
1 | echo "This is an error message" >&2 |
默认情况下,Linux会将STDERR导向STDOUT。但是,如果在运行脚本时重定向了STDERR,脚本中所有导向STDERR的文本都会被重定向。
1 | $ cat test8 |
2.永久重定向
可以用exec命令告诉shell在脚本执行期间重定向某个特定文件描述符。
1 |
|
exec命令会启动一个新shell并将STDOUT文件描述符重定向到文件。脚本中发给STDOUT的所有输出会被重定向到文件。
在脚本中重定向输入
可以使用与脚本中重定向STDOUT和STDERR相同的方法来将STDIN从键盘重定向到其他位置。
1 | exec 0< testfile |
1 |
|
创建自己的重定向
1.输出文件描述符
可以用exec命令来给输出分配文件描述符。
1 |
|
也可以不用创建新文件,而是使用exec命令来将输出追加到现有文件中。
1 | exec 3>>test13out |
2.重定向文件描述符
可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT。
1 |
|
3.输入文件描述符
在重定向到文件之前,先将STDIN文件描述符保存到另外一个文件描述符,然后在读取完文件之后再将STDIN恢复到它原来的位置
1 | # redirecting input file descriptors |
列出打开的文件描述符
lsof
命令会列出整个Linux系统打开的所有文件描述符。
阻止命令输出
将STDERR重定向到一个叫作null文件的特殊文件。
在Linux系统上null文件的标准位置是/dev/null
。
1 | ls -al > /dev/null |
创建临时文件
Linux使用/tmp目录来存放不需要永久保留的文件
mktemp
命令可以在/tmp目录中创建一个唯一的临时文件。
1.创建本地临时文件
mktemp会在本地目录中创建一个文件。要用mktemp命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行了。
1 | $ mktemp testing.XXXXXX |
1 | tempfile=$(mktemp test19.XXXXXX) |
2.在/tmp 目录创建临时文件
-t选项会强制mktemp命令来在系统的临时目录来创建该文件。
1 | mktemp -t test.XXXXXX |
3.创建临时目录
-d选项告诉mktemp命令来创建一个临时目录而不是临时文件。
记录消息
将输出同时发送到显示器和日志文件
tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是
STDOUT,另一处是tee命令行所指定的文件名:
1 | tee filename |
tee会重定向来自STDIN的数据,你可以用它配合管道命令来重定向命令输出。
1 | date | tee testfile |
默认情况下,tee命令会在每次使用时覆盖输出文件内容。-a
将数据追加到文件中
六、控制脚本
处理信号
信 号 | 值 | 描 述 |
---|---|---|
1 | SIGHUP | 挂起进程 |
2 | SIGINT | 终止进程 |
3 | SIGQUIT | 停止进程 |
9 | SIGKILL | 无条件终止进程 |
15 | SIGTERM | 尽可能终止进程 |
17 | SIGSTOP | 无条件停止进程,但不是终止进程 |
18 | SIGTSTP | 停止或暂停进程,但不终止进程 |
19 | SIGCONT | 继续运行停止的进程 |
bash shell会忽略收到的任何SIGQUIT (3)和SIGTERM (15)信号。bash shell会处理收到的SIGHUP (1)和SIGINT (2)信号。
如果bash shell收到了SIGHUP
信号,比如当你要离开一个交互式shell,它就会退出。退出之前,它会将SIGHUP
信号传给所有由该shell所启动的进程。通过SIGINT
信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这
1.生成信号
中断进程:
Ctrl+C
组合键会生成SIGINT
信号暂停进程:你可以在进程运行期间暂停进程,而无需终止它。
Ctrl+Z
组合键会生成一个SIGTSTP
信号。停止(stopping)进程,跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行。如果你的shell会话中有一个已停止的作业,在退出shell时,bash会提醒你。可以用
ps
命令来查看已停止的作业。
2.捕获信号
trap
命令允许指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap
命令中列出的信号,该信号不再由shell处理,而是交由本地处理。
1 | trap commands signals |
1 |
|
3.捕获脚本退出
也可以在shell脚本退出时进行捕获。要捕获shell脚本的退出,只要在trap命令后加上EXIT信号就行。
1 |
|
4.修改或移除捕获
使用带有新选项的trap命令。
1 |
|
删除已设置好的捕获。在trap命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。
1 |
|
以后台模式运行脚本
1.后台运行脚本
加个&
符
1 | ./test4.sh & |
当&符放到命令后时,它会将命令和bash shell分离开来,将命令作为系统中的一个独立的后台进程运行。
1 | [1] 3231 |
方括号中的数字是shell分配给后台进程的作业号。下一个数是Linux系统分配给进程的进程ID(PID)
当后台进程结束时,它会在终端上显示出一条消息:
1 | [1] Done ./test4.sh |
当后台进程运行时,它仍然会使用终端显示器来显示STDOUT和STDERR消息。
2.运行多个后台作业
ps命令,可以看到所有脚本处于运行状态。
每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出。
在非控制台下运行脚本
想在终端会话中启动shell脚本,然后让脚本一直以后台模式运行到结束,即使退出了终端会话。这可以用nohup
命令来实现。
nohup
命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP
信号。这会在退出终端会话时阻止进程退出。
在于,当你使用nohup
命令时,如果关闭该会话,脚本会忽略终端会话发过来的SIGHUP信号。
由于nohup命令会解除终端与进程的关联,进程也就不再同STDOUT和STDERR联系在一起。为了保存该命令产生的输出,nohup命令会自动将STDOUT和STDERR的消息重定向到一个名为nohup.out
的文件中。
作业控制
在作业停止后,Linux系统会让你选择是终止还是重启。你可以用kill命令终止该进程。要重启停止的进程需要向其发送一个SIGCONT
信号。
1.查看作业
jobs
命令允许查看shell当前正在处理的作业。
脚本用$$变量来显示Linux系统分配给该脚本的PID
参 数 | 描 述 |
---|---|
-l | 列出进程的PID以及作业号 |
-n | 只列出上次shell发出的通知后改变了状态的作业 |
-p | 只列出作业的PID |
-r | 只列出运行中的作业 |
-s | 只列出已停止的作业 |
1 | jobs -l |
带加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。
当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。
2.重启停止的作业
用bg命令加上作业号
1 | bg 2 |
要以前台模式重启作业,可用带有作业号的fg命令
1 | fg 2 |
调整谦让度
内核负责将CPU时间分配给系统上运行的每个进程。调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)
最低值-20是最高优先级,而最高值19是最低优先级,这太容易记混了。
1.nice 命令
nice
命令允许你设置命令启动时的调度优先级。
1 | nice -n 10 ./test4.sh > test4.out & |
nice命令阻止普通系统用户来提高命令的优先级。注意,指定的作业的确运行了,但是试图使用nice命令提高其优先级的操作却失败了。
nice命令的-n选项并不是必须的,只需要在破折号后面跟上优先级就行了。
2.renice命令
想改变系统上已运行命令的优先级。
1 | $ ./test11.sh & |
- 只能对属于你的进程执行renice;
- 只能通过renice降低进程的优先级;
- root用户可以通过renice来任意调整进程的优先级。
定时运行作业
at 命令来计划执行作业
at命令会将作业提交到队列中,指定shell何时运行该作业。at的守护进程atd
会以后台模式运行,检查作业队列来运行作业。
atd
守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at
)来获取用at命令提交的作业。默认情况下,atd
守护进程会每60秒检查一下这个目录。有
at命令的格式
1
at [-f filename] time
at命令会将STDIN的输入放到队列中。你可以用-f参数来指定用于读取命令(脚本文件)的文件名。
time格式
1
2
3
4
5标准的小时和分钟格式,比如10:15。
AM/PM指示符,比如10:15 PM。
特定可命名时间,比如now、noon、midnight或者teatime(4 PM)。
标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY。
文本日期,比如Jul 4或Dec 25,加不加年份均可。用-q参数指定不同的队列字母。
获取作业的输出
1
2//now指示at命令立刻执行该脚本。
at -f test13.sh now使用at命令时,最好在脚本中对STDOUT和STDERR进行重定向
1
2
3
4
5
6
7
# Test using at command
#
echo "This script ran at $(date +%B%d,%T)" > test13b.out
echo >> test13b.out
sleep 5
echo "This is the script's end..." >> test13b.out-M
选项来屏蔽作业产生的输出信息。列出等待的作业
atq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17$ at -M -f test13b.sh teatime
job 17 at 2015-07-14 16:00
$
$ at -M -f test13b.sh tomorrow
job 18 at 2015-07-15 13:03
$
$ at -M -f test13b.sh 13:30
job 19 at 2015-07-14 13:30
$
$ at -M -f test13b.sh now
job 20 at 2015-07-14 13:03
$
$ atq
20 2015-07-14 13:03 = Christine
18 2015-07-15 13:03 a Christine
17 2015-07-14 16:00 a Christine
19 2015-07-14 13:30 a Christineatq删除作业
atrm
1
atrm 18
2.安排需要定期执行的脚本
统使用cron程序来安排要定期执行的作业。。cron程序会在后台运行并检查一个特殊的表(被称作cron时间表)
cron时间表
1
2
3
4
5
6
7
8
9
10
11
12min hour dayofmonth month dayofweek command
#允许用特定值、取值范围(比如1~5)或者是通配符(星号)来指定条目。
#每天的10:15运行一个命令
15 10 * * * command
#在每周一4:15 PM运行的命令
15 16 * * 1 command
#在每个月的第一天中午12点执行命令。
00 12 1 * * command
#在每个月的最后一天执行的命令
#if-then语句来检查明天的日期是不是01:
00 12 * * * if [`date +%d -d tomorrow` = 01 ] ; then ; command命令列表必须指定要运行的命令或脚本的全路径名。
构建cron时间表
crontab
1
2#要列出已有的cron时间表
crontab -l浏览cron目录
对精确的执行时间要求不高,用预配置的cron脚本目录会更方便。
1
2
3hourly、daily、monthly和weekly
/etc/cron.*ly
3.使用新 shell 启动脚本
希望为shell会话设置某些shell功能,或者只是为了确保已经设置了某个文件。
bash shell都会运行.bashrc
文件。可以这样来验证:在主目录下的.bashrc文件中加入一条简单的echo语句,然后启动一个新shell。
1 | # .bashrc |
.bashrc
文件会运行两次:一次是当你登入bash shell时,另一次是当你启动一个bash shell时。
一个脚本在两个时刻都得以运行,可以把这个脚本放进该文件中。
七、函数
基本函数
1.创建函数
1 | function name { |
2.使用函数
1 |
|
在函数被定义前使用函数,你会收到一条错误消息。
重定义了函数,新定义会覆盖原来函数的定义.
3.返回值
bash shell会把函数当作一个小型脚本
默认退出状态码 0
用标准变量
$?
来确定函数的退出状态码。使用 return 命令
return命令允许指定一个整数值来定义函数的退出状态码
1
2
3
4
5
6
7
8
9
10
# using the return command in a function
#会将$value变量中用户输入的值翻倍
function dbl {
read -p "Enter a value: " value
echo "doubling the value"
return $[ $value * 2 ]
}
dbl
echo "The new value is $?"退出状态码必须是0~255。任何大于256的值都会产生一个错误值。
使用函数输出
1
2
3
4
5
6
7
8
9
10#这个命令会将dbl函数的输出赋给$result变量。
result='dbl'
#!/bin/bash
# using the echo to return a value
function dbl {
read -p "Enter a value: " value
echo $[ $value * 2 ]
}
result=$(dbl)
在函数中使用变量
向函数传递参数
函数名会在$0变量中定义,函数命令行上的任何参数都会通过$1、$2等定义。
$#
来判断传给函数的参数数目。
1 |
|
在函数中处理变量
全局变量
如果你在脚本的主体部分定义了一个全局变量,那么可以在函数内读取它的值。在函数内定义了一个全局变量,可以在脚本的主体部分读取它的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
# using a global variable to pass a value
function dbl {
value=$[ $value * 2 ]
}
read -p "Enter a value: " value
dbl
echo "The new value is: $value"
$
$ ./test8
Enter a value: 450
The new value is: 900
#当dbl函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被赋予了新值,那么在脚本中引用该变量时,新值也依然有效。局部变量
函数内部使用的任何变量都可以被声明成局部变量。要实现这一点,只要在变量声明的前面加上local关键字就可以了。
1
local temp
数组变量和函数
向函数传数组参数
将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。
1 |
|
从函数返回数组
函数用echo语句来按正确顺序输出单个数组值,然后脚本再将它们重新放进一个新的数组变量中。
1 |
|
函数递归
自成体系的函数不需要使用任何外部资源。这个特性使得函数可以递归地调用
1 |
|
创建库
是创建一个包含脚本中所需函数的公用库文件。这里有个叫作myfuncs
的库文件
1 | function addem { |
source
会在当前shell上下文中执行命令,而不是创建一个新shell。点操作符(dot operator)。
1 |
|
在命令行上使用函数
在.bashrc 文件中定义函数
直接定义函数
可以直接在主目录下的.bashrc文件中定义函数。
1
2
3
4
5
6
7
8# .bashrc
# Source global definitions
if [ -r /etc/bashrc ]; then
. /etc/bashrc
fi
function addem {
echo $[ $1 + $2 ]
}该函数会在下次启动新bash shell时生效。随后你就能在系统上任意地方使用这个函数了。
读取函数文件
1
2
3
4
5
6
7$ cat .bashrc
# .bashrc
# Source global definitions
if [ -r /etc/bashrc ]; then
. /etc/bashrc
fi
. /home/rich/libraries/myfuncs