一、构建基本脚本

创建shell基本

文件的第一行指定要使用的shell

1
#!/bin/bash

写好一个文件之后需要执行,但是必须要让shell找到脚本

  • 将shell脚本文件所处的目录添加到PATH环境变量中;
  • 在提示符中用绝对或相对文件路径来引用shell脚本文件。

显示消息

果在echo命令后面加上了一个字符串,该命令就能显示出这个文本字符串。

默认情况下,不需要使用引号将要显示的文本字符串划定出来。echo命令可用单引号或双引号来划定文本字符串。需要在文本中使用其中一种引号,而用另外一种来将字符串划定起来。

1
echo "This is a test to see if you're paying attention"

如果想把文本字符串和命令输出显示在同一行中,用echo语句的-n参数。

1
2
echo -n "The time and date are: "
date

使用变量

环境变量

以在环境变量名称之前加上美元符($)来使用这些环境变量。

1
2
3
4
echo "User info for userid: $USER"

#如果需要转转义
echo "The cost of the item is \$15"

${variable}形式引用的变量。变量名两侧额外的花括号通常用来帮助识别美元符后的变量名。

用户变量

使用等号将值赋给用户变量。在变量、等号和值之间不能出现空格。

在脚本的整个生命周期里,shell脚本中定义的变量会一直保持着它们的值

1
2
3
4
5
6
7
8
9
10
11
12
days=10 
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"

#变量赋值
value1=10
value2=$value1 #10
value3=value1 #value1
echo The resulting value is $value2

变量每次被引用时,都会输出当前赋给它的值。引用一个变量值时需要使用美元符,而引用变量来对其进行赋值时则不要使用$。

命令替换

shell脚本可以从命令输出中提取信息,并将其赋给变量。

有两种方法可以将命令输出赋给变量:

  • 反引号字符(`)
  • $()格式

命令替换允许你将shell命令的输出赋给变量。

1
2
testing=`date`
testing=$(date)

shell会运行命令替换符号中的命令,并将其输出赋给变量testing。

1
2
3
4
5
#在脚本中通过命令替换获得当前日期并用它来生成唯一文件名。
# copy the /usr/bin directory listing to a log file
# +%y%m%d格式告诉date命令将日期显示为两位数的年月日的组合。140131
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today

命令替换会创建一个子shell来运行对应的命令。子shell(subshell)是由运行该脚本的shell所创建出来的一个独立的子shell(child shell)。正因如此,由该子shell所执行命令是无法使用脚本中所创建的变量的。

使用路径./运行命令的话,也会创建出子shell;要是运行命令的时候不加入路径,就不会创建子shell。如果你使用的是内建的shell命令,并不会涉及子shell。

重定向输入和输出

你想要保存某个命令的输出而不仅仅只是让它显示在显示器上。

输出重定向

1
2
3
4
5
6
7
8
#重定向将命令的输出发送到一个文件中。>
command > outputfile

#如果输出文件已经存在了,重定向操作符会用新的文件数据覆盖已有文件。
date > test6

#想要将命令的输出追加到已有文件中,用双大于号(>>)来追加数据。
date >> test6

输入重定向

输入重定向将文件的内容重定向到命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
command < inputfile

#wc < test6
#wc命令可以对对数据中的文本进行计数。文本的行数/文本的词数/文本的字节数

#内联输入重定向,无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据就可以了。
#指定一个文本标记来划分输入数据的开始和结尾。
command << marker

#在命令行上使用内联输入重定向时,shell会用PS2环境变量中定义的次提示符来提示输入数据。
$ wc << EOF
> test string 1
> test string 2
> test string 3
> EOF

管道

有时需要将一个命令的输出作为另一个命令的输入。

我们不用将命令输出重定向到文件中,可以将其直接重定向到另一个命令。这个过程叫作管道连接(piping)

1
command1 | command2

在系统内部将它们连接起来。在第一个命令产生输出的同时,输出会被立即送给第二个命令。数据传输不会用到任何中间文件或缓冲区。

可以在一条命令中使用任意多条管道。可以持续地将命令的输出通过管道传给其他命令来细化操作。

1
2
$ rpm -qa | sort | more
ls -l | more

执行数学运算

expr

expr命令允许在命令行上处理数学表达式

expr命令能够识别少数的数学和字符串操作符

1
2
3
4
5
6
#许多expr命令操作符在shell中另有含义(比如星号)。当它们出现在在expr命令中时,会得到一些诡异的结果。
$ expr 5 * 2
expr: syntax error

$ expr 5 \* 2
10

使用方括号

在bash中,在将一个数学运算结果赋给某个变量时,可以用美元符和方括号($[ operation ])将数学表达式围起来。

1
2
3
4
5
6
7
$  var1=$[1 + 5] 
$ echo $var1
6
$ var2=$[$var1 * 2]
$ echo $var2
12
$

在bash shell脚本中进行算术运算会有一个主要的限制。

1
2
3
4
5
6
7
8
#!/bin/bash
var1=100
var2=45
var3=$[$var1 / $var2]
echo The final result is $var3

#输出
The final result is 2

bash shell数学运算符只支持整数运算。

浮点解决方案

用内建的bash计算器,叫作bc。

  1. 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
  2. 在脚本中使用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
2
3
4
if command
then
commands
fi

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
2
3
4
5
6
if command
then
commands
else
commands
fi

当if语句中的命令返回退出状态码0时,then部分中的命令会被执行,这跟普通的if-then语句一样。当if语句中的命令返回非零退出状态码时,bash shell会执行else部分中的命令。

嵌套 if

elif。这样就不用再书写多个if-then语句了。elif使用另一个if-then语句延续else部分。

1
2
3
4
5
6
7
8
9
if command1
then
commands
elif command2
then
more commands
else
commands
fi

elif语句行提供了另一个要测试的命令,这类似于原始的if语句行。如果elif后命令的退出状态码是0,则bash会执行第二个then语句部分的命令。使用这种嵌套方法,代码更清晰,逻辑更易懂。

test 命令

if-then语句是否能测试命令退出状态码之外的条件。答案是不能。

test命令提供了在if-then语句中测试不同条件的途径。如果test命令中列出的条件成立,test命令就会退出并返回退出状态码0。

1
test condition

bash shell提供了另一种条件测试方法,无需在if-then语句中声明test命令。

1
2
3
4
if [ condition ] 
then
commands
fi

第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。

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
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash 
# mis-using string comparisons
#
val1=Testing
val2=testing
#
#转义
if [ $val1 \> $val2 ]
then
echo "$val1 is greater than $val2"
else
echo "$val1 is less than $val2"
fi

在比较测试中,大写字母被认为是小于小写字母的。但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
2
[ condition1 ] && [ condition2 ] 
[ condition1 ] || [ condition2 ]

要让then部分的命令执行,两个条件都必须满足。

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# testing compound comparisons
#
if [ -d $HOME ] && [ -w $HOME/testing ]
then
echo "The file exists and you can write to it"
else
echo "I cannot write to the file"
fi

if-then 的高级特性

用于数学表达式的双括号

test命令只能在比较中使用简单的算术操作。双括号命令提供了更多的数学符号

1
(( expression ))

双括号命令符号

符 号 描 述
val++ 后增
val– 后减
++val 先增
–val 先减
! 逻辑求反
~ 位求反
** 幂运算
<< 左位移
>> 右位移
& 位布尔和
| 位布尔或
&& 逻辑和
|| 逻辑或
1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# using double parenthesis
#
val1=10
#
if (( $val1 ** 2 > 90 ))
then
(( val2 = $val1 ** 2 ))
echo "The square of $val1 is $val2"
fi

用于高级字符串处理功能的双方括号

1
[[ expression ]]

它提供了test命令未提供的另一个特性——模式匹配(pattern matching)。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# using pattern matching
# 定义一个正则表达式来匹配字符串值。
# 双方括号命令$USER环境变量进行匹配,看它是否以字母r开头。
if [[ $USER == r* ]]
then
echo "Hello $USER"
else
echo "Sorry, I do not know you"
fi

case 命令

在一组可能的值中寻找特定值。

1
2
3
4
5
case variable in 
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac

case命令会将指定的变量与不同模式进行比较。可以通过竖线操作符在一行中分隔出多个模式模式。星号会捕获所有与已知模式不匹配的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash 
# using the case command
#
case $USER in
rich | barbara)
echo "Welcome, $USER"
echo "Please enjoy your visit";;
testing)
echo "Special testing account";;
jessica)
echo "Do not forget to log off when you're done";;
*)
echo "Sorry, you are not allowed here";;
esac

$ ./test26.sh
Welcome, rich
Please enjoy your visit

三、循环结构化指令

for 命令

1
2
3
4
for var in list 
do
commands
done

在list参数中,你需要提供迭代中要用到的一系列值。

1.读取列表中的值

1
2
3
4
5
6
#!/bin/bash 
# basic for command
for test in Alabama Alaska Arizona Arkansas California Colorado
do
echo The next state is $test
done

每次for命令遍历值列表,它都会将列表中的下个值赋给$test变量。$test变量可以像for命令语句中的其他脚本变量一样使用。在最后一次迭代后,$test变量的值会在shell脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值。

2.读取列表中的复杂值

有时会遇到难处理的数据。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# another example of how not to use the for command
for test in I don't know if this'll work
do
echo "word:$test"
done
$ ./badtest1
word:I
word:dont know if thisll
word:work

有2个解决方法

  • 使用转义字符(反斜线)来将单引号转义;
  • 使用双引号来定义用到单引号的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash 
# another example of how not to use the for command
#在第一个有问题的地方添加了反斜线字符来转义don't中的单引号。在第二个有问题的地方将this'll用双引号圈起来。
for test in I don\'t know if "this'll" work
do
echo "word:$test"
done

$ ./test2
word:I
word:don't
word:know
word:if
word:this'll
word:work

3.从变量读取列表

将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。也可以通过for命令完成这个任务。

1
2
3
4
5
6
7
#!/bin/bash 
# using a variable to hold the list
list="Alabama Alaska Arizona Arkansas Colorado"
for state in $list
do
echo "Have you ever visited $state?"
done

4. 从命令读取值

可以用命令替换来执行任何能产生输出的命令,然后在for命令中使用该命令的输出。

1
2
3
4
5
6
7
#!/bin/bash 
# reading values from a file
file="states"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done

并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的单词,for命令仍然会将每个单词当作单独的值。

5.更改字段分隔符

特殊的环境变量IFS,叫作内部字段分隔符(internal field separator)

IFS环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。

可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。

1
IFS=$'\n'
1
2
3
4
5
6
7
8
9
#!/bin/bash 
# reading values from a file
file="states"
#告诉bash shell在数据值中忽略空格和制表符
IFS=$'\n'
for state in $(cat $file)
do
echo "Visit beautiful $state"
done

在处理代码量较大的脚本时,可能在一个地方需要修改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
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# iterate through all the files in a directory
for file in /home/rich/test/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done

在Linux中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将$file变量用双引号圈起来。

也可以在for命令中列出多个目录通配符,将目录查找和列表合并进同一个for语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash 
# iterating through multiple directories
for file in /home/rich/.b* /home/rich/badtest
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
else
echo "$file doesn't exist"
fi
done

$ ./test7
/home/rich/.backup.timestamp is a file
/home/rich/.bash_history is a file
/home/rich/.bash_logout is a file
/home/rich/.bash_profile is a file
/home/rich/.bashrc is a file
/home/rich/badtest doesn't exist

可以在数据列表中放入任何东西。即使文件或目录不存在,for语句也会尝试处理列表中的内容。在处理文件或目录时,这可能会是个问题。你无法知道你正在尝试遍历的目录是否存在:在处理之前测试一下文件或目录总是好的

C语言风格的for

1
for (( variable assignment ; condition ; iteration process ))
1
2
3
4
5
6
#!/bin/bash 
# testing the C-style for loop
for (( i=1; i <= 10; i++ ))
do
echo "The next number is $i"
done

r循环通过定义好的变量(本例中是变量i)来迭代执行这些命令。

如何使用多个变量

1
2
3
4
5
6
#!/bin/bash 
# multiple variables
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done

while 命令

是if-then语句和for循环的混杂体

1
2
3
4
while test command 
do
other commands
done

test command的退出状态码必须随着循环中运行的命令而改变。如果退出状态码不发生变化, while循环就将一直不停地进行下去

1
2
3
4
5
6
7
8
#!/bin/bash 
# while command test
var1=10
while [ $var1 -gt 0 ]
do
echo $var1
var1=$[ $var1 - 1 ]
done

多个测试命令

while命令允许你在while语句行定义多个测试命令。只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。

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
33
34
#!/bin/bash 
# testing a multicommand while loop
var1=10
while echo $var1
[ $var1 -ge 0 ]
do
echo "This is inside the loop"
var1=$[ $var1 - 1 ]
done

$ ./test11
10
This is inside the loop
9
This is inside the loop
8
This is inside the loop
7
This is inside the loop
6
This is inside the loop
5
This is inside the loop
4
This is inside the loop
3
This is inside the loop
2
This is inside the loop
1
This is inside the loop
0
This is inside the loop
-1

echo测试命令被执行并显示了var变量的值(现在小于0了)。直到shell执行test测试命令,whle循环才会停止。

until

until命令要求你指定一个通常返回非零退出状态码的测试命令。

1
2
3
4
until test commands 
do
other commands
done

可以在until命令语句中放入多个测试命令。只有最后一个命令的退出状态码决定了bash shell是否执行已定义的other commands

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
33
34
35
36
37
38
#!/bin/bash 
# using the until command
var1=100
until [ $var1 -eq 0 ]
do
echo $var1
var1=$[ $var1 - 25 ]
done


$ ./test12
100
75
50
25

#多个命令
#!/bin/bash
# using the until command
var1=100
until echo $var1
[ $var1 -eq 0 ]
do
echo Inside the loop: $var1
var1=$[ $var1 - 25 ]
done


$ ./test13
100
Inside the loop: 100
75
Inside the loop: 75
50
Inside the loop: 50
25
Inside the loop: 25
0

嵌套循环

循环语句可以在循环内使用任意类型的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash 
# nesting for loops
for (( a = 1; a <= 3; a++ ))
do
echo "Starting loop $a:"
for (( b = 1; b <= 3; b++ ))
do
echo " Inside loop: $b"
done
done
$ ./test14
Starting loop 1:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 2:
Inside loop: 1
Inside loop: 2
Inside loop: 3
Starting loop 3:
Inside loop: 1
Inside loop: 2
Inside loop: 3

循环处理文件数据

  • 使用嵌套循环

  • 修改IFS环境变量

通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即便数据中有空格也是如此。一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash 
# changing the IFS value
IFS.OLD=$IFS
IFS=$'\n'
for entry in $(cat /etc/passwd)
do
echo "Values in $entry –"
IFS=:
for value in $entry
do
echo " $value"
done
done

使用了两个不同的IFS值来解析数据。第一个IFS值解析出/etc/passwd文件中的单独的行。内部for循环接着将IFS的值修改为冒号,允许你从/etc/passwd的行中解析出单独的值。

控制循环

break 命令

break命令来退出任意类型的循环

  1. 跳出单个循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/bin/bash 
    # 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"
  2. 跳出内部循环

    处理多个循环时,break命令会自动终止你所在的最内层的循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/bash 
    # 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
  3. 跳出外部循环

    break命令接受单个命令行参数值break n其中n指定了要跳出的循环层级。默认情况下,n为1,表明跳出的是当前的循环。如果你将n设为2,break命令就会停止下一级的外部循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/bash 
    # 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
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# using the continue command
for (( var1 = 1; var1 < 15; var1++ ))
do
if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
then
continue
fi
echo "Iteration number: $var1"
done

continue命令也允许通过命令行参数指定要继续执行哪一级循环:continue n

处理循环的输出输入

可以对循环的输出使用管道或进行重定向。这可以通过在done命令之后添加一个处理命令来实现。

1
2
3
4
5
6
7
8
9
for file in /home/rich/* 
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif
echo "$file is a file"
fi
done > output.txt

shell会将for命令的结果重定向到文件output.txt中,而不是显示在屏幕上。

读取文件的另外一种方式

1
2
3
4
while read -r line
do
echo $line
done < filename.txt

四、处理用户的输入

命令行参数

向shell脚本传递数据的最基本方法是使用命令行参数。命令行参数允许在运行脚本时向命令行添加数据。

1
$ ./addem 10 30

1.读取参数

bash shell会将一些称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括shell所执行的脚本名称。位置参数变量是标准的数字:$0是程序名,$1是第一个参数,$2是第二个参数,依次类推,直到第九个参数$9。

每个参数都是用空格分隔的,所以shell会将空格当成两个值的分隔符。要在参数值中包含空格,必须要用引号

1
2
./test3.sh 'Rich Blum'
Hello Rich Blum, glad to meet you.

将文本字符串作为参数传递时,引号并非数据的一部分。它们只是表明数据的起止位置。

如果脚本需要的命令行参数不止9个,你仍然可以处理,但是需要稍微修改一下变量名。在第9个变量之后,你必须在变量数字周围加上花括号,比如${10}。

2.读取脚本名

$0参数获取shell在命令行启动的脚本名

如果使用另一个命令来运行shell脚本,命令会和脚本名混在一起,出现在$0参数中。

1
2
./test5.sh
The zero parameter is set to: ./test5.sh

得把脚本的运行路径给剥离掉。另外,还要删除与脚本名混杂在一起的命令。basename命令会返回不包含路径的脚本名。

1
2
3
4
5
6
#!/bin/bash 
# Using basename with the $0 parameter
#
name=$(basename $0)
echo
echo The script name is: $name

3.测试参数

当脚本认为参数变量中会有数据而实际上并没有时,脚本很有可能会产生错误消息。这种写脚本的方法并不可取。在使用参数前一定要检查其中是否存在数据。

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# testing parameters before use
#
if [ -n "$1" ]
then
echo Hello $1, glad to meet you.
else
echo "Sorry, you did not identify yourself. "
fi

特殊参数变量

1.参数统计

特殊变量$#含有脚本运行时携带的命令行参数的个数。

1
2
3
4
5
6
7
8
9
10
11
if [ $# -ne 2 ] 
then
echo
echo Usage: test9.sh a b
echo
else
total=$[ $1 + $2 ]
echo
echo The total is $total
echo
fi

if-then语句用-ne测试命令行参数数量。如果参数数量不对,会显示一条错误消息告知脚本的正确用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash 
# testing grabbing last parameter
#
#${$#}就代表了最后一个命令行参数变量?
echo The last parameter was ${$#}

#必须将美元符换成感叹号。
#!/bin/bash
# Grabbing the last parameter
#
params=$#
echo
echo The last parameter is $params
echo The last parameter is ${!#}

$ bash test10.sh 1 2 3 4 5
The last parameter is 5
The last parameter is 5

$ bash test10.sh
The last parameter is 0
The last parameter is test10.sh

2.抓取所有的数据

有时候需要抓取命令行上提供的所有参数。这时候不需要先用$#变量来判断命令行上有多少参数,然后再进行遍历,使用一组其他的特殊变量来解决这个问题。

$*$@变量可以用来轻松访问所有的参数。

  • $*变量会将命令行上提供的所有参数当作一个单词保存。基本上$*变量会将这些参数视为一个整体,而不是多个个体。
  • $@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词。就能够遍历所有的参数值,得到每个参数。
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
#!/bin/bash
# testing $* and $@
# $*
echo
count=1
#
for param in "$*"
do
echo "\$* Parameter #$count = $param"
count=$[ $count + 1 ]
done
# $@
echo
count=1
#
for param in "$@"
do
echo "\$@ Parameter #$count = $param"
count=$[ $count + 1 ]
done

$ ./test12.sh rich barbara katie jessica
$* Parameter #1 = rich barbara katie jessica
$@ Parameter #1 = rich
$@ Parameter #2 = barbara
$@ Parameter #3 = katie
$@ Parameter #4 = jessica

移动变量

bash shell的shift命令能够用来操作命令行参数。shift命令会根据它们的相对位置来移动命令行参数。

在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除

是在你不知道到底有多少参数时。你可以只操作第一个参数,移动参数,然后继续操作第一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat test13.sh
#!/bin/bash
# demonstrating the shift command
echo
count=1
#测试第一个参数值的长度
while [ -n "$1" ]
do
echo "Parameter #$count = $1"
count=$[ $count + 1 ]
#shift命令会将所有参数的位置移动一个位置。
shift
done

./test13.sh rich barbara katie jessica
Parameter #1 = rich
Parameter #2 = barbara
Parameter #3 = katie
Parameter #4 = jessica

也可以一次性移动多个位置,只需要给shift命令提供一个参数,指明要移动的位置数就行了。shift 2

处理选项

同时提供了参数和选项的bash命令。选项是跟在单破折线后面的单个字母,它能改变命令的行为。

1.查找选项

紧跟在脚本名之后,就跟命令行参数一样。

  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
    done

    case语句在命令行参数中找到一个选项,就处理一个选项。如果命令行上还提供了其他参数,你可以在case语句的通用情况处理部分中处理。

  2. 分离参数和选项

    同时使用选项和参数的情况。用特殊字符来将二者分开,这个特殊字符是双破折线(--)。shell会用双破折线来表明选项列表结束。在双破折线之后,脚本就可以放心地将剩下的命令行参数当作参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #!/bin/bash 
    # 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

    当脚本遇到双破折线时,它会停止处理选项,并将剩下的参数都当作命令行参数。

  3. 处理带值的选项

    有些选项会带上一个额外的参数值。

    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
    #!/bin/bash 
    # 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 option

    case语句定义了三个它要处理的选项。-b选项还需要一个额外的参数值。由于要处理的参数是$1,额外的参数值就应该位于$2(因为所有的参数在处理完之后都会被移出)。只要将参数值从$2变量中提取出来就可以了。当然,因为这个选项占用了两个参数位,所以你还需要使用shift命令多移动一个位置。

    如果你想将多个选项放进一个参数中时,它就不能工作了。

2.使用 getopt 命令

它能够识别命令行参数,从而在脚本中解析它们时更方便。

  1. 命令的格式

    接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。

    1
    getopt optstring parameters

    optstring它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。在optstring中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数。

    1
    2
    getopt ab:cd -a -b test1 -cd test2 test3
    -a -b test1 -c -d -- test2 test3

    optstring定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,因为b选项需要一个参数值。

    如果指定了一个不在optstring中的选项,默认情况下,getopt命令会产生一条错误消息。

    1
    2
    3
    getopt ab:cd -a -b test1 -cde test2 test3
    getopt: invalid option -- e
    -a -b test1 -c -d -- test2 test3

    如果想忽略这条错误消息,可以在命令后加-q选项。

  2. 在脚本中使用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
    #!/bin/bash 
    # 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命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。

  3. 使用更高级的 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
    #!/bin/bash 
    # 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
    #!/bin/bash 
    # 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
2
3
4
5
6
7
8
9
10
#!/bin/bash 
#testing the read command
# echo命令使用了-n选项。该选项不会在字符串末尾输出换行符,允许脚本用户紧跟其后输入数据,而不是下一行。
echo -n "Enter your name: "
read name
echo "Hello $name, welcome to my program. "

./test21.sh
Enter your name: Rich Blum
Hello Rich Blum, welcome to my program.

read命令包含了-p选项,允许你直接在read命令行指定提示符。

read命令会将提示符后输入的所有数据分配给单个变量,要么你就指定多个变量。输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量

在read命令行中不指定变量,read命令会将它收到的任何数据都放进特殊环境变量REPLY中。

1
2
3
4
5
6
#!/bin/bash 
# Testing the REPLY Environment variable
#
read -p "Enter your name: "
echo
echo Hello $REPLY, welcome to my program.

2.超时

-t选项来指定一个计时器。-t选项指定了read命令等待输入的秒数。当计时器过期后,read命令会返回一个非零退出状态码。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# timing the data entry
#
if read -t 5 -p "Please enter your name: " name
then
echo "Hello $name, welcome to my script"
else
echo
echo "Sorry, too slow! "
fi

让read命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# getting just one character of input
#
read -n1 -p "Do you want to continue [Y/N]? " answer
case $answer in
Y | y) echo
echo "fine, continue on…";;
N | n) echo
echo OK, goodbye
exit;;
esac
echo "This is the end of the script"

-n选项和值1一起使用,告诉read命令在接受单个字符后退出。

3.隐藏方式读取

-s选项可以避免在read命令中输入的数据出现在显示器上上(实际上,数据会被显示,只是read命令会将文本颜色设成跟背景色一样)。

1
2
3
4
5
6
#!/bin/bash 
# hiding input data from the monitor
#
read -s -p "Enter your password: " pass
echo
echo "Is your password really $pass? "

4.从文件中读取

read命令来读取Linux系统上文件里保存的数据。每次调用read命令,它都会从文件中读取一行文本。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# reading data from a file
#
count=1
cat test | while read line
do
echo "Line $count: $line"
count=$[ $count + 1]
done
echo "Finished processing the file"

五、呈现数据

理解输入和输出

脚本输出的方法:

  • 在显示器屏幕上显示输出
  • 将输出重定向到文件中

1.标准文件描述符

Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(filedescriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。每个进程一次最多可以有九个文件描述符。bash shell保留了前三个文件描述符(0、1和2)

文件描述符 缩 写 描 述
0 STDIN 标准输入
1 STDOUT 标准输出
2 STDERR 标准错误
  1. 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.
  2. STDOUT :符代表shell的标准输出。在终端界面上,标准输出就是终端显示器。shell的所有输出(包括shell中运行的程序和脚本)会被定向到标准输出中,也就是显示器。>覆盖,>>追加。

    如果你对脚本使用了标准输出重定向:

    1
    2
    $ ls -al badfile > test3 
    ls: cannot access badfile: No such file or directory

    shell创建了输出重定向文件,但错误消息却显示在了显示器屏幕上。注意,在显示test3文件的内容时并没有任何错误。test3文件创建成功了,只是里面是空的。

    shell对于错误消息的处理是跟普通输出分开的。如果你创建了在后台模式下运行的shell脚本,通常你必须依赖发送到日志文件的输出消息。用这种方法的话,如果出现了错误信息,这些信息是不会出现在日志文件中的。你需要换种方法来处理。

  3. STDERR 代表shell的标准错误输出。shell或shell中运行的程序和脚本出错时生成的错误消息都会发送到这个位置。默认情况下,STDERR文件描述符会和STDOUT文件描述符指向同样的地方。也就是说,默认情况下,错误消息也会输出到显示器输出中。

    但是,STDERR并不会随着STDOUT的重定向而发生改变。使用脚本时,也希望将错误消息保存到日志文件中的时候。

2.重定向错误

  1. 只重定向错误:STDERR文件描述符被设成2。可以选择只重定向错误消息,将该文件描述符值放在重定向符号前。

    1
    ls -al badfile 2> test4
  2. 重定向错误和数据:用两个重定向符号

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat test8 
#!/bin/bash
# testing STDERR messages
echo "This is an error" >&2
echo "This is normal output"

$ ./test8
This is an error
This is normal output

$ ./test8 2> test9
This is normal output
$ cat test9
This is an error

2.永久重定向

可以用exec命令告诉shell在脚本执行期间重定向某个特定文件描述符。

1
2
3
4
5
6
#!/bin/bash 
# redirecting all output to a file
exec 1>testout
echo "This is a test of redirecting all output"
echo "from a script to another file."
echo "without having to redirect every individual line"

exec命令会启动一个新shell并将STDOUT文件描述符重定向到文件。脚本中发给STDOUT的所有输出会被重定向到文件。

在脚本中重定向输入

可以使用与脚本中重定向STDOUT和STDERR相同的方法来将STDIN从键盘重定向到其他位置。

1
exec 0< testfile
1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# redirecting file input
#重定向只要在脚本需要输入时就会作用。
exec 0< testfile
count=1
while read line
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done

创建自己的重定向

1.输出文件描述符

可以用exec命令来给输出分配文件描述符。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# using an alternative file descriptor
exec 3>test13out
echo "This should display on the monitor"
echo "and this should be stored in the file" >&3
echo "Then this should be back on the monitor"

$ ./test13
This should display on the monitor
Then this should be back on the monitor
$ cat test13out
and this should be stored in the file

也可以不用创建新文件,而是使用exec命令来将输出追加到现有文件中。

1
exec 3>>test13out 

2.重定向文件描述符

可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# storing STDOUT, then coming back to it
#将文件描述符3重定向到文件描述符1的当前位置,送给文件描述符3的输出都将出现在显示器上。
exec 3>&1
#将STDOUT重定向到文件,shell现在会将发送给STDOUT的输出直接重定向到输出文件中。
#文件描述符3仍然指向STDOUT原来的位置,也就是显示器。
exec 1>test14out
echo "This should store in the output file"
echo "along with this line."
#脚本将STDOUT重定向到文件描述符3的当前位置(现在仍然是显示器)。
exec 1>&3
echo "Now things should be back to normal"

3.输入文件描述符

在重定向到文件之前,先将STDIN文件描述符保存到另外一个文件描述符,然后在读取完文件之后再将STDIN恢复到它原来的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# redirecting input file descriptors 
#文件描述符6用来保存STDIN的位置。
exec 6<&0
exec 0< testfile
count=1
while read line
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done
#将STDIN重定向到文件描述符6
exec 0<&6
read -p "Are you done now? " answer
case $answer in
Y|y) echo "Goodbye";;
N|n) echo "Sorry, this is the end.";;
esac

列出打开的文件描述符

lsof命令会列出整个Linux系统打开的所有文件描述符。

阻止命令输出

将STDERR重定向到一个叫作null文件的特殊文件。

在Linux系统上null文件的标准位置是/dev/null

1
ls -al > /dev/null

创建临时文件

Linux使用/tmp目录来存放不需要永久保留的文件

mktemp命令可以在/tmp目录中创建一个唯一的临时文件。

1.创建本地临时文件

mktemp会在本地目录中创建一个文件。要用mktemp命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行了。

1
2
3
$ mktemp testing.XXXXXX 
$ ls -al testing*
-rw------- 1 rich rich 0 Oct 17 21:30 testing.UfIi13
1
tempfile=$(mktemp test19.XXXXXX)

2.在/tmp 目录创建临时文件

-t选项会强制mktemp命令来在系统的临时目录来创建该文件。

1
2
 mktemp -t test.XXXXXX 
/tmp/test.xG3374

3.创建临时目录

-d选项告诉mktemp命令来创建一个临时目录而不是临时文件。

记录消息

将输出同时发送到显示器和日志文件

tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是

STDOUT,另一处是tee命令行所指定的文件名:

1
tee filename

tee会重定向来自STDIN的数据,你可以用它配合管道命令来重定向命令输出。

1
2
3
4
date | tee testfile 
Sun Oct 19 18:56:21 EDT 2014
$ cat testfile
Sun Oct 19 18:56:21 EDT 2014

默认情况下,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.生成信号

  1. 中断进程:Ctrl+C组合键会生成SIGINT信号

  2. 暂停进程:你可以在进程运行期间暂停进程,而无需终止它。Ctrl+Z组合键会生成一个SIGTSTP信号。停止(stopping)进程,跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行。

    如果你的shell会话中有一个已停止的作业,在退出shell时,bash会提醒你。可以用ps命令来查看已停止的作业。

2.捕获信号

trap命令允许指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再由shell处理,而是交由本地处理。

1
trap commands signals
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
#!/bin/bash 
# Testing signal trapping
# 用到的trap命令会在每次检测到SIGINT信号时显示一行简单的文本消息。
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
#
echo This is a test script
#
count=1
while [ $count -le 10 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
#
echo "This is the end of the test script"

./test1.sh
This is a test script
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
^C Sorry! I have trapped Ctrl-C
Loop #6
Loop #7
Loop #8
^C Sorry! I have trapped Ctrl-C
Loop #9
Loop #10

3.捕获脚本退出

也可以在shell脚本退出时进行捕获。要捕获shell脚本的退出,只要在trap命令后加上EXIT信号就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash 
# Trapping the script exit
#
trap "echo Goodbye..." EXIT
#
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done

Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye...

4.修改或移除捕获

使用带有新选项的trap命令。

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
33
34
35
#!/bin/bash 
# Modifying a set trap
#
trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT
#
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
#
trap "echo ' I modified the trap!'" SIGINT
#
count=1
while [ $count -le 5 ]
do
echo "Second Loop #$count"
sleep 1
count=$[ $count + 1 ]
don

Loop #1
Loop #2
Loop #3
^C Sorry... Ctrl-C is trapped.
Loop #4
Loop #5
Second Loop #1
Second Loop #2
^C I modified the trap!
Second Loop #3
Second Loop #4
Second Loop #5

删除已设置好的捕获。在trap命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash 
# Removing a set trap
#
trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT
#
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
#
# Remove the trap
trap -- SIGINT
echo "I just removed the trap"
#
count=1
while [ $count -le 5 ]
do
echo "Second Loop #$count"
sleep 1
count=$[ $count + 1 ]
done

以后台模式运行脚本

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
2
3
 jobs -l
[1]+ 1897 Stopped ./test10.sh
[2]- 1917 Running ./test10.sh > test10.out &

加号的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。

当前的默认作业完成处理后,带减号的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。

2.重启停止的作业

用bg命令加上作业号

1
2
 bg 2
[2]+ ./test12.sh &

要以前台模式重启作业,可用带有作业号的fg命令

1
2
 fg 2
./test12.sh

调整谦让度

内核负责将CPU时间分配给系统上运行的每个进程。调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)

最低值-20是最高优先级,而最高值19是最低优先级,这太容易记混了。

1.nice 命令

nice命令允许你设置命令启动时的调度优先级。

1
2
 nice -n 10 ./test4.sh > test4.out &
[1] 4973

nice命令阻止普通系统用户来提高命令的优先级。注意,指定的作业的确运行了,但是试图使用nice命令提高其优先级的操作却失败了。

nice命令的-n选项并不是必须的,只需要在破折号后面跟上优先级就行了。

2.renice命令

想改变系统上已运行命令的优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./test11.sh &
[1] 5055
$
$ ps -p 5055 -o pid,ppid,ni,cmd
PID PPID NI CMD
5055 4721 0 /bin/bash ./test11.sh

$ renice -n 10 -p 5055
5055: old priority 0, new priority 10

$ ps -p 5055 -o pid,ppid,ni,cmd
PID PPID NI CMD
5055 4721 10 /bin/bash ./test11.sh
  • 只能对属于你的进程执行renice;
  • 只能通过renice降低进程的优先级
  • root用户可以通过renice来任意调整进程的优先级。

定时运行作业

at 命令来计划执行作业

at命令会将作业提交到队列中,指定shell何时运行该作业。at的守护进程atd会以后台模式运行,检查作业队列来运行作业。

atd守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用at命令提交的作业。默认情况下,atd守护进程会每60秒检查一下这个目录。有

  1. 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参数指定不同的队列字母。

  2. 获取作业的输出

    1
    2
    //now指示at命令立刻执行该脚本。
    at -f test13.sh now

    使用at命令时,最好在脚本中对STDOUT和STDERR进行重定向

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash 
    # 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选项来屏蔽作业产生的输出信息。

  3. 列出等待的作业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
  4. 删除作业atrm

    1
    atrm 18

2.安排需要定期执行的脚本

统使用cron程序来安排要定期执行的作业。。cron程序会在后台运行并检查一个特殊的表(被称作cron时间表)

  1. cron时间表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    min 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

    命令列表必须指定要运行的命令或脚本的全路径名。

  2. 构建cron时间表 crontab

    1
    2
    #要列出已有的cron时间表
    crontab -l
  3. 浏览cron目录

    精确的执行时间要求不高,用预配置的cron脚本目录会更方便。

    1
    2
    3
    hourly、daily、monthly和weekly

    /etc/cron.*ly

3.使用新 shell 启动脚本

希望为shell会话设置某些shell功能,或者只是为了确保已经设置了某个文件。

bash shell都会运行.bashrc文件。可以这样来验证:在主目录下的.bashrc文件中加入一条简单的echo语句,然后启动一个新shell。

1
2
3
4
5
6
7
# .bashrc 
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# User specific aliases and functions
echo "I'm in a new shell!"

.bashrc文件会运行两次:一次是当你登入bash shell时,另一次是当你启动一个bash shell时。

一个脚本在两个时刻都得以运行,可以把这个脚本放进该文件中。

七、函数

基本函数

1.创建函数

1
2
3
4
5
6
7
function name { 
commands
}

name() {
commands
}

2.使用函数

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# using a function in a script
function func1 {
echo "This is an example of a function"
}

echo "This is the end of the loop"
func1
echo "Now this is the end of the script"
  • 在函数被定义前使用函数,你会收到一条错误消息。

  • 重定义了函数,新定义会覆盖原来函数的定义.

3.返回值

bash shell会把函数当作一个小型脚本

  1. 默认退出状态码 0

    用标准变量$?来确定函数的退出状态码。

  2. 使用 return 命令

    return命令允许指定一个整数值来定义函数的退出状态码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/bin/bash 
    # 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的值都会产生一个错误值。

  3. 使用函数输出

    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
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash 
# trying to access script parameters inside a function 这个脚本运行出错
function badfunc1 {
#函数也使用了$1和$2变量,但它们和脚本主体中的$1和$2变量并不相同。
echo $[ $1 * $2 ]
}
if [ $# -eq 2 ]
then
value=$(badfunc1)
echo "The result is $value"
else
echo "Usage: badtest1 a b"
fi

在函数中处理变量

  1. 全局变量

    如果你在脚本的主体部分定义了一个全局变量,那么可以在函数内读取它的值。在函数内定义了一个全局变量,可以在脚本的主体部分读取它的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/bin/bash 
    # 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函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被赋予了新值,那么在脚本中引用该变量时,新值也依然有效。
  2. 局部变量

    函数内部使用的任何变量都可以被声明成局部变量。要实现这一点,只要在变量声明的前面加上local关键字就可以了。

    1
    local temp

数组变量和函数

向函数传数组参数

将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# array variable to function test
function testit {
local newarray
newarray=(;'echo "$@"')
echo "The new array value is: ${newarray[*]}"
}

myarray=(1 2 3 4 5)
echo "The original array is ${myarray[*]}"
testit ${myarray[*]}
$

从函数返回数组

函数用echo语句来按正确顺序输出单个数组值,然后脚本再将它们重新放进一个新的数组变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash 
# returning an array value
function arraydblr {
local origarray
local newarray
local elements
local i
origarray=($(echo "$@"))
#arraydblr函数将该数组重组到新的数组变量中,生成该输出数组变量的一个副本。
newarray=($(echo "$@"))
elements=$[ $# - 1 ]
for (( i = 0; i <= $elements; i++ ))
{
newarray[$i]=$[ ${origarray[$i]} * 2 ]
}
echo ${newarray[*]}
}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
arg1=$(echo ${myarray[*]})
result=($(arraydblr $arg1))
echo "The new array is: ${result[*]}"

函数递归

自成体系的函数不需要使用任何外部资源。这个特性使得函数可以递归地调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash 
# using recursion
#计算阶乘
function factorial {
if [ $1 -eq 1 ]
then
echo 1
else
local temp=$[ $1 - 1 ]
local result=$(factorial $temp)
echo $[ $result * $1 ]
fi
}

read -p "Enter value: " value
result=$(factorial $value)
echo "The factorial of $value is: $result"

创建库

是创建一个包含脚本中所需函数的公用库文件。这里有个叫作myfuncs的库文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addem { 
echo $[ $1 + $2 ]
}
function multem {
echo $[ $1 * $2 ]
}
function divem {
if [ $2 -ne 0 ]
then
echo $[ $1 / $2 ]
else
echo -1
fi
}

source会在当前shell上下文中执行命令,而不是创建一个新shell。点操作符(dot operator)。

1
2
3
4
5
#!/bin/bash 
# using a library file the wrong way
source myfuncs
result=$(addem 10 15)
echo "The result is $result"

在命令行上使用函数

在.bashrc 文件中定义函数

  1. 直接定义函数

    可以直接在主目录下的.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时生效。随后你就能在系统上任意地方使用这个函数了。

  2. 读取函数文件

    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