Shell脚本编程学习笔记

学习来源:Linux命令行与shell脚本编程大全 中文第三版

学习时间:2023年4月8日

1 构建基本脚本

1.1 使用多个命令

shell脚本的关键在于输入多个命令并处理每个命令的结果,甚至需要将一个命令的结果传给另一个命令。shell可以让你将多个命令串起来,一次执行完成。如果要两个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开。

1
2
3
4
5
6
7
$ date ; who 
Mon Feb 21 15:36:09 EST 2014
Christine tty2 2014-02-21 15:26
Samantha tty3 2014-02-21 15:26
Timothy tty1 2014-02-21 15:26
user tty7 2014-02-19 14:03 (:0)
user pts/0 2014-02-21 15:21 (:0.0)

1.2 创建shell脚本文件

在创建shell脚本文件时,必须在文件的第一行指定要使用的shell。其格式为:

1
2
3
4
#!/bin/bash
# 注释

命令...

1.3 使用变量

1.3.1 环境变量

使用env命令查看环境变量:

1
2
3
4
5
6
7
8
9
10
[root@HongyiZeng shell]# env
XDG_SESSION_ID=114645
TERM_PROGRAM=vscode
HOSTNAME=HongyiZeng
TERM=xterm-256color
SHELL=/bin/bash
HISTSIZE=3000
TERM_PROGRAM_VERSION=1.78.0
USER=root
...

在脚本中,可以在环境变量名称之前加上$来使用这些环境变量,或者使用${}的形式:

1
2
3
4
5
#!/bin/bash 
# display user information from the system.
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: ${HOME}

执行结果:

1
2
3
4
[root@HongyiZeng shell]# ./test2.sh
User info for userid: root
UID: 0
HOME: /root

1.3.2 用户变量

除了环境变量,shell脚本还允许在脚本中定义和使用自己的变量。定义变量允许临时存储数据并在整个脚本中使用,从而使shell脚本看起来更像一个真正的计算机程序。

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

示例

1
2
3
4
5
6
7
8
#!/bin/bash
# testing variables
days=10
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"

执行结果:

1
2
3
4
$ chmod u+x test3 
$ ./test3
Katie checked in 10 days ago
Jessica checked in 5 days ago

此外,注意每次引用变量时,都需要加上$

1
2
3
4
5
#!/bin/bash 
# assigning a variable value to another variable
value1=10
value2=$value1 # 需要使用$ value2=value1
echo The resulting value is $value2

执行结果:

1
The resulting value is 10

否则:

1
The resulting value is value1

没有$,shell会将变量名解释成普通的文本字符串。

1.3.3 命令替换

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

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

示例

1
2
3
#!/bin/bash 
testing=$(date) # 或者testing=`date`
echo "The date and time are: " $testing

变量testing获得了date命令的输出,然后使用echo语句显示出它的值。运行这个shell脚本生成如下输出:

1
2
$ ./test5 
The date and time are: Mon Jan 31 20:23:25 EDT 2014

在脚本中通过命令替换获得当前日期并用它来生成唯一文件名:

1
2
3
4
#!/bin/bash 
# copy the /usr/bin directory listing to a log file
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today

1.4 重定向输入输出

重定向可以用于输入,也可以用于输出,可以将文件重定向到命令输入。

1.4.1 输出重定向

符号 作用
命令 > 文件 标准输出STDOUT重定向到一个文件中(覆盖)
命令 2> 文件 错误输出STDERR重定向到一个文件中(覆盖)
命令 >> 文件 将标准输出重定向到一个文件中(追加)
命令 2>> 文件 将错误输出重定向到一个文件中(追加)
命令 &> 文件 将标准输出与错误输出共同写入到文件中(覆盖)
命令 &>> 文件 将标准输出与错误输出共同写入到文件中(追加)

这里的1是指STDOUT,2是STDERR

示例

1
2
3
4
5
$ date > test6 
$ ls -l test6
-rw-r--r-- 1 user user 29 Feb 10 17:56 test6
$ cat test6
Thu Feb 10 17:56:58 EDT 2014

1.4.2 输入重定向

符号 作用
命令 < 文件 将文件作为命令的标准输入
命令 << 分界符 从标准输入中读入,直到遇见分界符才停止
命令 < 文件1 > 文件2 将文件1作为命令的标准输入并将标准输出到文件2

示例

1
2
$ wc < test6 
2 11 60

wc命令可以对对数据中的文本进行计数。默认情况下,它会输出3个值:

  • 文本的行数
  • 文本的词数
  • 文本的字节数

1.5 管道

有时需要将一个命令的输出作为另一个命令的输入。这可以用重定向来实现,只是有些笨拙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ rpm -qa > rpm.list # 先进行输出重定向
$ sort < rpm.list # 再进行输入重定向
abrt-1.1.14-1.fc14.i686
abrt-addon-ccpp-1.1.14-1.fc14.i686
abrt-addon-kerneloops-1.1.14-1.fc14.i686
abrt-addon-python-1.1.14-1.fc14.i686
abrt-desktop-1.1.14-1.fc14.i686
abrt-gui-1.1.14-1.fc14.i686
abrt-libs-1.1.14-1.fc14.i686
abrt-plugin-bugzilla-1.1.14-1.fc14.i686
abrt-plugin-logger-1.1.14-1.fc14.i686
abrt-plugin-runapp-1.1.14-1.fc14.i686
acl-2.2.49-8.fc14.i686
[...]

语法格式:

1
command1 | command2 | command3 ...

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

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
$ rpm -qa | sort 
abrt-1.1.14-1.fc14.i686
abrt-addon-ccpp-1.1.14-1.fc14.i686
abrt-addon-kerneloops-1.1.14-1.fc14.i686
abrt-addon-python-1.1.14-1.fc14.i686
abrt-desktop-1.1.14-1.fc14.i686
abrt-gui-1.1.14-1.fc14.i686
abrt-libs-1.1.14-1.fc14.i686
abrt-plugin-bugzilla-1.1.14-1.fc14.i686
abrt-plugin-logger-1.1.14-1.fc14.i686
abrt-plugin-runapp-1.1.14-1.fc14.i686
acl-2.2.49-8.fc14.i686
[...]

又如:

1
$ rpm -qa | sort | more

这行命令序列会先执行rpm命令,将它的输出通过管道传给sort命令,然后再将sort的输出通过管道传给more命令来显示,在显示完一屏信息后停下来。这样你就可以在继续处理前停下来阅读显示器上显示的信息,如下图所示。

image-20230505112139490

或者将排好序的结果重定向到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ rpm -qa | sort > rpm.list 
$ more rpm.list
abrt-1.1.14-1.fc14.i686
abrt-addon-ccpp-1.1.14-1.fc14.i686
abrt-addon-kerneloops-1.1.14-1.fc14.i686
abrt-addon-python-1.1.14-1.fc14.i686
abrt-desktop-1.1.14-1.fc14.i686
abrt-gui-1.1.14-1.fc14.i686
abrt-libs-1.1.14-1.fc14.i686
abrt-plugin-bugzilla-1.1.14-1.fc14.i686
abrt-plugin-logger-1.1.14-1.fc14.i686
abrt-plugin-runapp-1.1.14-1.fc14.i686
acl-2.2.49-8.fc14.i686
[...]

1.6 数学运算

暂略

1.7 退出脚本

shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕。退出状态码是一个0~255的整数值,在命令结束运行时由命令传给shell。可以捕获这个值并在脚本中使用。

1.7.1 查看退出状态码

Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码

通常,一个成功结束的命令的退出状态码是0。如果一个命令结束时有错误,退出状态码就是一个正数值。

1
2
3
4
5
6
7
8
9
10
11
# 正确的命令
$ date
Sat Jan 15 10:01:30 EDT 2014
$ echo $?
0

# 不存在的命令
$ asdfg
-bash: asdfg: command not found
$ echo $?
127

image-20230505112513207

1.7.2 exit命令

默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。

可以改变这种默认行为,返回自己的退出状态码。exit命令允许你在脚本结束时指定一个退出状态码。

示例

1
2
3
4
5
6
7
#!/bin/bash 
# testing the exit status
var1=10
var2=30
var3=$[$var1 + $var2]
echo The answer is $var3
exit 5

执行结果:

1
2
3
4
$ ./test13 
The answer is 40
$ echo $?
5

2 条件结构化命令

2.1 if-then语句

语法格式:

1
2
3
4
if command
then
commands
fi

bash shell的if语句会运行if后面的那个命令。如果该命令的退出状态码是0(该命令成功运行),位于then部分的命令就会被执行。如果该命令的退出状态码是其他值,then部分的命令就不会被执行,bash shell会继续执行脚本中的下一个命令。fi语句用来表示if-then语句到此结束。

示例

1
2
3
4
5
6
#!/bin/bash 
# testing the if statement
if pwd
then
echo "It worked"
fi

执行结果:

1
2
3
$ ./test1.sh
/home/Christine
It worked

1
2
3
4
5
6
7
#!/bin/bash 
# testing a bad command
if IamNotaCommand
then
echo "It worked"
fi
echo "We are outside the if statement"

执行结果:

1
2
3
$ ./test2.sh
./test2.sh: line 3: IamNotaCommand: command not found
We are outside the if statement

在这个例子中,if语句行故意放了一个不能工作的命令。由于这是个错误的命令,所以它会产生一个非零的退出状态码,且bash shell会跳过then部分的echo语句。还要注意,运行if语句中的那个错误命令所生成的错误消息依然会显示在脚本的输出中。


1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# testing multiple commands in the then section
testuser=Christine
if grep $testuser /etc/passwd
then
echo "This is my first command"
echo "This is my second command"
echo "I can even put in other commands besides echo:"
ls -a /home/$testuser/.b* # 显示以b开头的所有文件
fi

作用:if语句行使用grep命令在/etc/passwd文件中查找某个用户名当前是否在系统上使用。如果有用户使用了那个登录名,脚本会显示一些文本信息并列出该用户HOME目录的bash文件。

执行结果:

1
2
3
4
5
6
7
$ ./test3.sh
Christine:x:501:501:Christine B:/home/Christine:/bin/bash
This is my first command
This is my second command
I can even put in other commands besides echo:
/home/Christine/.bash_history /home/Christine/.bash_profile
/home/Christine/.bash_logout /home/Christine/.bashrc

2.2 if-then-else语句

语法格式:

1
2
3
4
5
6
if command
then
commands
else
commands
fi

示例

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# testing the else section
testuser=NoSuchUser
if grep $testuser /etc/passwd
then
echo "The bash files for user $testuser are:"
ls -a /home/$testuser/.b*
echo
else
echo "The user $testuser does not exist on this system."
echo
fi

执行结果:

1
2
3
4
$ ./test4.sh
The user NoSuchUser does not exist on this system.

$

2.3 elif语句

语法格式:

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

示例

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash 
# Testing nested ifs - use elif
testuser=NoSuchUser
if grep $testuser /etc/passwd
then
echo "The user $testuser exists on this system."
elif ls -d /home/$testuser
then
echo "The user $testuser does not exist on this system."
echo "However, $testuser has a directory."
fi

执行结果:

1
2
3
/home/NoSuchUser 
The user NoSuchUser does not exist on this system.
However, NoSuchUser has a directory.

这个脚本准确无误地发现,尽管登录名已经从/etc/passwd中删除了,但是该用户的目录仍然存在。

2.4 test命令

在很多脚本中,你可能希望测试一种条件而不是一个命令,比如数值、字符串内容、文件或目录的状态。test命令为你提供了测试这些条件的简单方法。如果条件为TRUE,test命令会为if-then语句产生退出状态码0。如果条件为FALSE,test命令会为if-then语句产生一个非零的退出状态码。

语法格式:

1
test condition

当用在if-then语句中时,test命令看起来是这样的:

1
2
3
4
if test condition
then
commands
fi

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

1
2
3
4
if [ condition ]
then
commands
fi

即:方括号定义了测试条件(方括号是与test命令同义的特殊bash命令)。注意,第一个方括号之后和第二个方括号之前必须加上一个空格,否则就会报错。

2.4.1 数值比较

使用test命令最常见的情形是对两个数值进行比较。表12-1列出了测试两个值时可用的条件参数。

image-20230505115909715

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash 
# Using numeric test evaluations
value1=10
value2=11
if [ $value1 -gt 5 ] # 或者 if test $value1 -gt 5
then
echo "The test value $value1 is greater than 5"
fi

if [ $value1 -eq $value2 ]
then
echo "The values are equal"
else
echo "The values are different"
fi

2.4.2 字符串比较

image-20230505120059015

2.4.3 文件比较

image-20230505121012261

2.5 复合条件测试

if-then语句可以使用布尔逻辑来组合测试。有两种布尔运算符可用:

  • [ condition1 ] && [ condition2 ]
  • [ condition1 ] || [ condition2 ]

示例

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

2.6 if-then的高级特性

2.6.1 双括号

双括号命令允许你在比较过程中使用高级数学表达式。test命令只能在比较中使用简单的算术操作。双括号命令提供了更多的数学符号,这些符号对于用过其他编程语言的程序员而言并不陌生。

语法格式:

1
(( expression ))

image-20230505121356683

示例

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

2.6.2 双方括号

双方括号命令提供了针对字符串比较的高级特性。它提供了test命令未提供的另一个特性——模式匹配(pattern matching)。

1
[[ expression ]]

示例

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# using pattern matching
#
if [[ $USER == r* ]]
then
echo "Hello $USER"
else
echo "Sorry, I do not know you"
fi

2.7 case命令

语法格式:

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

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

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/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

执行结果:

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

3 循环结构化命令

3.1 for命令

语法格式:

1
2
3
4
for var in list
do
commands
done

3.1.1 读取列表中的值

for命令最基本的用法就是遍历for命令自身所定义的一系列值。

示例

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

执行结果:

1
2
3
4
5
6
7
$ ./test1 
The next state is Alabama
The next state is Alaska
The next state is Arizona
The next state is Arkansas
The next state is California
The next state is Colorado

在最后一次迭代后,$test变量的值会在shell脚本的剩余部分一直保持有效。它会一直保持最后一次迭代的值(除非你修改了它)。

3.1.2 从变量读取列表

通常shell脚本遇到的情况是,你将一系列值都集中存储在了一个变量中,然后需要遍历变量中的整个列表。

示例

1
2
3
4
5
6
7
8
#!/bin/bash 
# using a variable to hold the list
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut" # 向$list变量包含的已有列表中添加(或者说是拼接)了一个值。这是向变量中存储的已有文本字符串尾部添加文本的一个常用方法,例如:PATH=$PATH:/home/uusama/mysql/bin
for state in $list
do
echo "Have you ever visited $state?"
done

执行结果:

1
2
3
4
5
6
7
$ ./test4 
Have you ever visited Alabama?
Have you ever visited Alaska?
Have you ever visited Arizona?
Have you ever visited Arkansas?
Have you ever visited Colorado?
Have you ever visited Connecticut?

3.1.3 从命令读取值

生成列表中所需值的另外一个途径就是使用命令的输出。可以用命令替换来执行任何能产生输出的命令,然后在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

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cat states # 查看states文件的内容
Alabama
Alaska
Arizona
Arkansas
Colorado
Connecticut
Delaware
Florida
Georgia
$ ./test5
Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
Visit beautiful Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia

这个例子在命令替换中使用了cat命令来输出文件states的内容。注意到states文件中每一行有一个州,而不是通过空格分隔的。for命令仍然以每次一行的方式遍历了cat命令的输出,假定每个州都是在单独的一行上。但这并没有解决数据中有空格的问题。如果你列出了一个名字中有空格的州,for命令仍然会将每个单词当作单独的值。这是有原因的,下一节我们将会了解。

3.1.5 更改字段分隔符

造成这个问题的原因是特殊的环境变量IFS,叫作内部字段分隔符(internal field separator)。IFS环境变量定义了bash shell用作字段分隔符的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

例如,如果你想修改IFS的值,使其只能识别换行符,那就必须这么做:

1
IFS=$'\n'

假定你要遍历一个文件中用冒号分隔的值(比如在/etc/passwd文件中)。你要做的就是将IFS的值设为冒号。

1
IFS=:

如果要指定多个IFS字符,只要将它们在赋值行串起来就行。

1
IFS=$'\n':;"

这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。

3.1.6 用通配符读取目录

可以用for命令来自动遍历目录中的文件。

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变量用双引号圈起来。如果不这么做,遇到含有空格的目录名或文件名时就会有错误产生。

3.2 C语言风格的for命令

3.2.1 语法格式

以下是bash中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

3.2.2 使用多个变量

C语言风格的for命令也允许为迭代使用多个变量。循环会单独处理每个变量,你可以为每个变量定义不同的迭代过程。尽管可以使用多个变量,但你只能在for循环中定义一种条件。

示例

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

3.3 while命令

3.3.1 基本格式

1
2
3
4
while test command
do
other commands
done

示例

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

3.3.2 使用多个测试命令

while命令允许在while语句行定义多个测试命令。

示例

1
2
3
4
5
6
7
8
9
#!/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

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ./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

while循环会在var1变量等于0时执行echo语句,然后将var1变量的值减一。接下来再次执行测试命令,用于下一次迭代。echo测试命令被执行并显示了var变量的值(现在小于0了)。直到shell执行test测试命令,whle循环才会停止。

这说明在含有多个命令的while语句中,在每次迭代中所有的测试命令都会被执行,包括测试命令失败的最后一次迭代。

3.4 until命令

until命令和while命令工作的方式完全相反。until命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结束了。

语法格式:

1
2
3
4
until test commands 
do
other commands
done

示例

1
2
3
4
5
6
7
8
#!/bin/bash 
# using the until command
var1=100
until [ $var1 -eq 0 ]
do
echo $var1
var1=$[ $var1 - 25 ]
done

3.5 嵌套循环

循环语句可以在循环内使用任意类型的命令,包括其他循环命令。

示例

1
2
3
4
5
6
7
8
9
10
#!/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

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./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

3.6 控制循环

3.6.1 break命令

可以用break命令来退出任意类型的循环。

① 跳出单个循环

在shell执行break命令时,它会尝试跳出当前正在执行的循环。

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"

执行结果:

1
2
3
4
5
6
$ ./test17 
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
The for loop is completed
② 跳出内部循环

在处理多个循环时,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

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ ./test19 
Outer loop: 1
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
Outer loop: 2
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
Outer loop: 3
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
③ 跳出外部循环

有时你在内部循环,但需要停止外部循环。break命令接受单个命令行参数值:

1
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

执行结果:

1
2
3
4
5
6
$ ./test20 
Outer loop: 1
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4

3.6.2 continue命令

continue命令可以提前中止某次循环中的命令,但并不会完全终止整个循环。可以在循环内部设置shell不执行命令的条件。

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

执行结果:

1
2
3
4
5
6
7
8
9
10
11
$ ./test21 
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
Iteration number: 5
Iteration number: 10
Iteration number: 11
Iteration number: 12
Iteration number: 13
Iteration number: 14

3.7 处理循环的输出

在shell脚本中,可以对循环的输出使用管道或进行重定向。这可以通过在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

3.8 实例:查找可执行文件

扫描PATH环境变量中所有的目录中的可执行文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash 
# finding files in the PATH
IFS=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [ -x $file ] # 是否是可执行文件
then
echo " $file"
fi
done
done

执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./test25 | more 
/usr/local/bin:
/usr/bin:
/usr/bin/Mail
/usr/bin/Thunar
/usr/bin/X
/usr/bin/Xorg
/usr/bin/[
/usr/bin/a2p
/usr/bin/abiword
/usr/bin/ac
/usr/bin/activation-client
/usr/bin/addr2line
...

4 输入

4.1 命令行参数

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

1
$ ./addem 10 30

4.1.1 读取参数

bash shell会将一些称为位置参数(positional parameter)的特殊变量分配给输入到命令行中的所有参数。这也包括shell所执行的脚本名称。位置参数变量是标准的数字:

  • $0是程序名
  • $1是第一个参数,$2是第二个参数,依次类推,直到第九个参数$9
  • 10以上的参数使用:${10}等的格式

示例

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# using one command line parameter
# 计算阶乘
factorial=1
for (( number=1; number <= $1 ; number++ ))
do
factorial=$[ $factorial * $number ]
done
echo The factorial of $1 is $factorial

执行结果:

1
2
$ ./test1.sh 5 
The factorial of 5 is 120

1
2
3
4
5
6
7
#!/bin/bash 
# testing two command line parameters
#
total=$[ $1 * $2 ]
echo The first parameter is $1.
echo The second parameter is $2.
echo The total value is $total.
1
2
3
4
$ ./test2.sh 2 5
The first parameter is 2.
The second parameter is 5.
The total value is 10.

1
2
3
4
#!/bin/bash 
# testing string parameters
#
echo Hello $1, glad to meet you.
1
2
$ ./test3.sh Rich
Hello Rich, glad to meet you.

但碰到含有空格的文本字符串时就会出现问题:

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

要在参数值中包含空格,必须要用引号(单引号或双引号均可)。

1
2
3
4
5
$ ./test3.sh 'Rich Blum'
Hello Rich Blum, glad to meet you.
$
$ ./test3.sh "Rich Blum"
Hello Rich Blum, glad to meet you.

4.1.2 读取脚本名

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

1
2
3
4
#!/bin/bash 
# Testing the $0 parameter
#
echo The zero parameter is set to: $0
1
2
$ bash test5.sh
The zero parameter is set to: test5.sh

4.1.3 测试参数

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

示例

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# testing parameters before use
#
if [ -n "$1" ] # 测试$1长度是否非零
then
echo Hello $1, glad to meet you.
else
echo "Sorry, you did not identify yourself. "
fi
1
2
3
4
5
$ ./test7.sh Rich
Hello Rich, glad to meet you.
$
$ ./test7.sh
Sorry, you did not identify yourself.

4.2 特殊参数变量

4.2.1 参数统计

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash 
# Testing parameters
#
if [ $# -ne 2 ] # 检测参数数量是否为2
then
echo
echo Usage: test9.sh a b
echo
else
total=$[ $1 + $2 ]
echo
echo The total is $total
echo
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
$ $ bash test9.sh

Usage: test9.sh a b

$ bash test9.sh 10

Usage: test9.sh a b

$ bash test9.sh 10 15

The total is 25

$

此外${!#}返回最后一个参数的值,如果没有参数,则返回脚本名称。

1
2
3
4
5
6
7
8
#!/bin/bash 
# Grabbing the last parameter
#
params=$#
echo
echo The last parameter is $params
echo The last parameter is ${!#}
echo
1
2
3
4
5
6
7
8
$ 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
$

4.2.2 抓取所有的数据

$*$@变量可以用来轻松访问所有的参数。这两个变量都能够在单个变量中存储所有的命令行参数。

  • $*变量会将这些参数视为一个整体,而不是多个个体
  • $@变量会将命令行上提供的所有参数当作同一字符串中的多个独立的单词

示例

1
2
3
4
5
#!/bin/bash 
# testing $* and $@
#
echo "Using the \$* method: $*"
echo "Using the \$@ method: $@"
1
2
3
4
$ ./test11.sh rich barbara katie jessica
Using the $* method: rich barbara katie jessica
Using the $@ method: rich barbara katie jessica
$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# testing $* and $@
count=1
for param in "$*"
do
echo "\$* Parameter #$count = $param"
count=$[ $count + 1 ]
done

count=1
for param in "$@"
do
echo "\$@ Parameter #$count = $param"
count=$[ $count + 1 ]
done
1
2
3
4
5
6
$ ./test12.sh rich barbara katie jessica
$* Parameter #1 = rich barbara katie jessica
$@ Parameter #1 = rich
$@ Parameter #2 = barbara
$@ Parameter #3 = katie
$@ Parameter #4 = jessica

4.3 移动变量

shift命令会根据它们的相对位置来移动命令行参数。默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值,也就是程序名,不会改变)。

示例

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# demonstrating the shift command
echo
count=1
while [ -n "$1" ]
do
echo "Parameter #$count = $1"
count=$[ $count + 1 ]
shift
done
1
2
3
4
5
$ ./test13.sh rich barbara katie jessica
Parameter #1 = rich
Parameter #2 = barbara
Parameter #3 = katie
Parameter #4 = jessica

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

1
shift n

4.4 处理选项

选项是跟在单破折线后面的单个字母,它能改变命令的行为。

4.4.1 查找选项

① 处理简单选项

在提取每个单独参数时,用case语句来判断某个参数是否为选项。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/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
1
2
3
4
5
$ ./test15.sh -a -b -c -d
Found the -a option
Found the -b option
Found the -c option
-d is not an option
② 分离参数和选项

对Linux来说,这个特殊字符是双破折线(--)。shell会用双破折线来表明选项列表结束。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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
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
$ ./testing.sh -a test1 -b -c -d test2

示例

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 
# 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
1
2
3
4
$ ./test17.sh -a -b test1 -d
Found the -a option
Found the -b option, with parameter value test1
-d is not an option

在Linux中,合并选项是一个很常见的用法,而且如果脚本想要对用户更友好一些,也要给用户提供这种特性。幸好,有另外一种处理选项的方法能够帮忙。

4.4.2 getopt命令

getopt命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而在脚本中解析它们时更方便。

4.5 获取用户输入

bash shell为此提供了read命令。

4.5.1 基本的读取

read命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量。

1
2
3
4
5
6
#!/bin/bash 
# testing the read command

echo -n "Enter your name: " # -n:不输出换行符
read name
echo "Hello $name, welcome to my program. "
1
2
3
$ ./test21.sh
Enter your name: Rich Blum
Hello Rich Blum, welcome to my program.

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

1
2
3
4
5
6
7
#!/bin/bash 
# testing the read -p option
#
read -p "Please enter your age: " age
days=$[ $age * 365 ]
echo "That makes you over $days days old! "
#
1
2
3
$ ./test22.sh
Please enter your age: 10
That makes you over 3650 days old!

又如:

1
2
3
4
5
#!/bin/bash
# entering multiple variables
#
read -p "Enter your name: " first last
echo "Checking data for $last, $first…"
1
2
3
$ ./test23.sh
Enter your name: Rich Blum
Checking data for Blum, Rich...

也可以在read命令行中不指定变量。如果是这样,read命令会将它收到的任何数据都放进特殊环境变量REPLY中。

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

4.5.2 超时

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

4.5.3 隐藏读取

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

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

4.5.4 从文件读取

每次调用read命令,它都会从文件中读取一行文本。当文件中再没有内容时,read命令会退出并返回非零退出状态码。

最常见的方法是对文件使用cat命令,将结果通过管道直接传给含有read命令的while命令。

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"

5 输出

5.1 理解输入输出

在使用输入重定向符号(<)时,Linux会用重定向指定的文件来替换标准输入文件描述符。它会读取文件并提取数据,就如同它是键盘上键入的。

1
2
3
4
5
$ cat 
this is a test
this is a test
this is a second test.
this is a second test.

当在命令行上只输入cat命令时,它会从STDIN接受输入。输入一行,cat命令就会显示出一行。

1
2
3
4
$ cat < testfile 
This is the first line.
This is the second line.
This is the third line.

现在cat命令会用testfile文件中的行作为输入。

其余略

5.2 在脚本中重定向输出

5.2.1 临时重定向

如果有意在脚本中生成错误消息,可以将单独的一行输出重定向到STDERR。你所需要做的是使用输出重定向符来将输出信息重定向到STDERR文件描述符。

在重定向到文件描述符时,必须在文件描述符数字之前加一个&

1
echo "This is an error message" >&2

这行会在脚本的STDERR文件描述符所指向的位置显示文本,而不是通常的STDOUT。

示例

1
2
3
4
5
#!/bin/bash 
# ./test8
# testing STDERR messages
echo "This is an error" >&2
echo "This is normal output"
1
2
3
$ ./test8 # 这样运行没什么区别
This is an error # 实际上输出到了STDERR上
This is normal output # STDOUT

但是:

1
2
3
4
$ ./test8 2>test9 
This is normal output # 没有显示This is an error,因为它被重定向到了test9中
$ cat test9
This is an error

5.2.2 永久重定向

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

1
exec fp>file

含义:将文件描述符fp重定向到文件file

示例

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的所有输出会被重定向到文件。

1
2
3
4
5
$ ./test10 
$ cat testout
This is a test of redirecting all output
from a script to another file.
without having to redirect every individual line

5.3 在脚本中重定向输入

exec命令允许将STDIN重定向到Linux系统上的文件中:

1
exec 0< testfile

这个命令会告诉shell它应该从文件testfile中获得输入,而不是STDIN。

示例

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# redirecting file input
exec 0< testfile
count=1
while read line
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done

5.4 创建自己的重定向

5.5 阻止命令输出

可以将STDERR重定向到一个叫作null文件的特殊文件。在Linux系统上null文件的标准位置是/dev/null。你重定向到该位置的任何数据都会被丢掉,不会显示。

由于/dev/null文件不含有任何内容,程序员通常用它来快速清除现有文件中的数据,而不用先删除文件再重新创建。

1
2
3
4
5
6
7
$ cat testfile 
This is the first line.
This is the second line.
This is the third line.
$ cat /dev/null > testfile
$ cat testfile
$

文件testfile仍然存在系统上,但现在它是空文件。这是清除日志文件的一个常用方法,因为日志文件必须时刻准备等待应用程序操作。

5.6 临时文件

Linux系统有特殊的目录,专供临时文件使用。Linux使用/tmp目录来存放不需要永久保留的文件。大多数Linux发行版配置了系统在启动时自动删除/tmp目录的所有文件。系统上的任何用户账户都有权限在读写/tmp目录中的文件。

mktemp命令可以在/tmp目录中创建一个唯一的临时文件。shell会创建这个文件,但不用默认的umask值。它会将文件的读和写权限分配给文件的属主,并将你设成文件的属主。一旦创建了文件,你就在脚本中有了完整的读写权限,但其他人没法访问它(当然,root用户除外)。

5.6.1 创建本地临时文件

要用mktemp命令在当前目录中创建一个临时文件,只要指定一个文件名模板就行了。模板可以包含任意文本文件名,在文件名末尾加上任意个X就行了。

1
2
3
4
[root@HongyiZeng shell]# mktemp testing.XXXXX
testing.WFP8R
[root@HongyiZeng shell]# ll testing.WFP8R
-rw------- 1 root root 0 May 8 09:25 testing.WFP8R

命令的输出就是文件名。

示例

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# creating and using a temp file
tempfile=$(mktemp test19.XXXXXX) # 命令替换,mktemp的输出是临时文件名
exec 3>$tempfile # 永久重定向
echo "This script writes to temp file $tempfile" # STDOUT
echo "This is the first line" >&3 # 临时重定向
echo "This is the second line." >&3
echo "This is the last line." >&3
exec 3>&- # 关闭文件描述符
echo "Done creating temp file. The contents are:"
cat $tempfile
rm -f $tempfile 2> /dev/null

这个脚本用mktemp命令来创建临时文件并将文件名赋给$tempfile变量。接着将这个临时文件作为文件描述符3的输出重定向文件。在将临时文件名显示在STDOUT之后,向临时文件中写入了几行文本,然后关闭了文件描述符。最后,显示出临时文件的内容,并用rm命令将其删除。

1
2
3
4
5
6
$ ./test19 # 注意打印顺序
This script writes to temp file test19.vCHoya
Done creating temp file. The contents are:
This is the first line
This is the second line.
This is the last line.

5.6.2 在/tmp中创建临时文件

-t选项会强制mktemp命令来在系统的临时目录来创建该文件。在用这个特性时,mktemp命令会返回用来创建临时文件的全路径,而不是只有文件名。

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

示例

1
2
3
4
5
6
7
8
#!/bin/bash 
# creating a temp file in /tmp
tempfile=$(mktemp -t tmp.XXXXXX)
echo "This is a test file." > $tempfile
echo "This is the second line of the test." >> $tempfile
echo "The temp file is located at: $tempfile"
cat $tempfile
rm -f $tempfile
1
2
3
4
$ ./test20 
The temp file is located at: /tmp/tmp.Ma3390
This is a test file.
This is the second line of the test.

5.6.3 创建临时目录

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

示例

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash 
# using a temporary directory
tempdir=$(mktemp -d dir.XXXXXX)
cd $tempdir
tempfile1=$(mktemp temp.XXXXXX)
tempfile2=$(mktemp temp.XXXXXX)
exec 7>$tempfile1
exec 8>$tempfile2
echo "Sending data to directory $tempdir"
echo "This is a test line of data for $tempfile1" >&7
echo "This is a test line of data for $tempfile2" >&8

这段脚本在当前目录创建了一个目录,然后它用cd命令进入该目录,并创建了两个临时文件。之后这两个临时文件被分配给文件描述符,用来存储脚本的输出。

5.7 tee命令

将输出同时发送到显示器和日志文件,这种做法有时候能够派上用场。不用将输出重定向两次,只要用特殊的tee命令就行。

tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是STDOUT,另一处是tee命令行所指定的文件名:

1
tee filename

例如:

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选项(append)。

示例

1
2
3
4
5
6
#!/bin/bash 
# using the tee command for logging
tempfile=test22file
echo "This is the start of the test" | tee $tempfile
echo "This is the second line of the test" | tee -a $tempfile # 追加
echo "This is the end of the test" | tee -a $tempfile # 追加
1
2
3
4
5
6
7
8
$ ./test22 
This is the start of the test
This is the second line of the test
This is the end of the test
$ cat test22file
This is the start of the test
This is the second line of the test
This is the end of the test

6 控制脚本

6.1 处理信号

6.1.1 捕获信号

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

1
trap commands signals

示例

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# Testing signal trapping
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"

本例中用到的trap命令会在每次检测到SIGINT信号时显示一行简单的文本消息。捕获这些信号会阻止用户用bash shell组合键Ctrl+C来停止程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./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
This is the end of the test script

6.1.2 捕获脚本退出

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

例如:

1
2
3
4
5
6
7
8
9
10
#!/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
1
2
3
4
5
6
7
$ ./test2.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye...

6.2 后台模式

6.2.1 后台运行脚本

在命令后加个&符就行了。

例如:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash 
# Test running in the background
#
count=1
while [ $count -le 10 ]
do
sleep 1
count=$[ $count + 1 ]
done
#
1
2
$ ./test4.sh &
[1] 3231 # [作业号] 进程PID

&符放到命令后时,它会将命令和bash shell分离开来,将命令作为系统中的一个独立的后台进程运行。

当后台进程结束时,它会在终端上显示出一条消息:

1
[1] Done ./test4.sh

注意,当后台进程运行时,它仍然会使用终端显示器来显示STDOUT和STDERR消息。为了避免脚本输出、输入的命令以及命令输出全都混在一起,最好将后台运行的脚本的STDOUT和STDERR进行重定向。

6.2.2 运行多个后台作业

可以在命令行提示符下同时启动多个后台作业。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
$ ./test6.sh &
[1] 3568
$ This is Test Script #1
$ ./test7.sh &
[2] 3570
$ This is Test Script #2
$ ./test8.sh &
[3] 3573
$ And...another Test script
$ ./test9.sh &
[4] 3576
$ Then...there was one more test script

查看这些后台进程:

1
2
3
4
5
6
7
8
$ ps
PID TTY TIME CMD
2431 pts/0 00:00:00 bash
3568 pts/0 00:00:00 test6.sh
3570 pts/0 00:00:00 test7.sh
3573 pts/0 00:00:00 test8.sh
3576 pts/0 00:00:00 test9.sh
3579 pts/0 00:00:00 ps

在终端会话中使用后台进程时一定要小心。注意,在ps命令的输出中,每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出

6.3 非控制台下运行脚本

可以用nohup命令来实现。nohup命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出。

1
2
3
$ nohup ./test1.sh &
[1] 3856
nohup: ignoring input and appending output to 'nohup.out'

如果关闭该会话,脚本会忽略终端会话发过来的SIGHUP信号。

由于nohup命令会解除终端与进程的关联,进程也就不再同STDOUT和STDERR联系在一起。为了保存该命令产生的输出,nohup命令会自动将STDOUT和STDERR的消息重定向到一个名为nohup.out的文件中。

6.4 作业控制

启动、停止、终止以及恢复作业的这些功能统称为作业控制。通过作业控制,就能完全控制shell环境中所有进程的运行方式了。

6.4.1 查看作业

作业控制中的关键命令是jobs命令。jobs命令允许查看shell当前正在处理的作业。

image-20230508102805080

示例

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash 
# Test job control
echo "Script Process ID: $$" # $$为PID
count=1
while [ $count -le 10 ]
do
echo "Loop #$count"
sleep 10
count=$[ $count + 1 ]
done
echo "End of script..."

启动一个脚本,并使用Ctrl Z停止(阻塞)脚本:

1
2
3
4
5
6
$ ./test10.sh
Script Process ID: 1897
Loop #1
Loop #2
^Z
[1]+ Stopped ./test10.sh

还是使用同样的脚本,利用&将另外一个作业作为后台进程启动。出于简化的目的,脚本的输出被重定向到文件中,避免出现在屏幕上。

1
2
$ ./test10.sh > test10.out &
[2] 1917

使用jobs命令查看作业:

1
2
3
$ jobs -l
[1]+ 1897 Stopped ./test10.sh
[2]- 1917 Running ./test10.sh > test10.out &

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

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

例如:

1
2
3
4
5
6
7
8
9
10
11
$ ./test10.sh > test10a.out &
[1] 1950
$ ./test10.sh > test10b.out &
[2] 1952
$ ./test10.sh > test10c.out &
[3] 1955
$
$ jobs -l
[1] 1950 Running ./test10.sh > test10a.out &
[2]- 1952 Running ./test10.sh > test10b.out &
[3]+ 1955 Running ./test10.sh > test10c.out &

调用kill命令向默认进程(1955)发送了一个SIGHUP信号,终止了该作业。在接下来的jobs命令输出中,先前带有减号的作业(1952)成了现在的默认作业,减号也变成了加号。

1
2
3
4
5
6
7
8
9
$ kill 1955
[3]+ Terminated ./test10.sh > test10c.out
$ jobs -l
[1]- 1950 Running ./test10.sh > test10a.out &
[2]+ 1952 Running ./test10.sh > test10b.out &
$ kill 1952
[2]+ Terminated ./test10.sh > test10b.out
$ jobs -l
[1]+ 1950 Running ./test10.sh > test10a.out &

6.4.2 重启停止的作业

在bash作业控制中,可以将已停止的作业作为后台进程或前台进程重启。前台进程会接管你当前工作的终端,所以在使用该功能时要小心了。

要以后台模式重启一个作业,可用bg命令加上作业号。

1
2
3
4
5
6
7
8
$ ./test11.sh
^Z
[1]+ Stopped ./test11.sh
$ bg
[1]+ ./test11.sh &
$ jobs
[1]+ Running ./test11.sh &
$ # 终端立即可用

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

1
2
3
4
5
$ fg 2
./test12.sh
This is the script's end...
... # 占用终端
$

由于作业是以前台模式运行的,直到该作业完成后,命令行界面的提示符才会出现。

6.5 优先级

调度优先级是个整数值,从-20(最高优先级)到+19(最低优先级)。默认情况下,bash shell以优先级0来启动所有进程。

6.5.1 nice命令

nice命令允许设置命令启动时的调度优先级。要让命令以更低的优先级运行,只要用nice的-n命令行来指定新的优先级级别。

1
2
3
4
5
$ nice -n 10 ./test4.sh > test4.out & # 或者 nice -10
[1] 4973
$ ps -p 4973 -o pid,ppid,ni,cmd
PID PPID NI CMD
4973 4721 10 /bin/bash ./test4.sh

nice命令会让脚本以更低的优先级运行,但阻止普通系统用户来提高命令的优先级。

1
2
3
4
$ nice -n -10 ./test4.sh > test4.out &
[1] 4985
nice: cannot set niceness: Permission denied
[1]+ Done nice -n -10 ./test4.sh > test4.out

6.5.2 renice命令

允许指定运行进程的PID来改变优先级。

6.6 定时运行作业

Linux系统提供了多个在预选时间运行脚本的方法:at命令和cron表。

6.6.1 at命令

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

atd守护进程会检查系统上的一个特殊目录(通常位于/var/spool/at)来获取用at命令提交的作业。默认情况下,atd守护进程会每60秒检查一下这个目录。有作业时,atd守护进程会检查作业设置运行的时间。如果时间跟当前时间匹配,atd守护进程就会运行此作业。

① 命令格式
1
at [-f filename] time
  • 默认情况下,at命令会将STDIN的输入放到队列中。可以用-f参数来指定用于读取命令(脚本文件)的文件名。
  • time参数指定了Linux系统何时运行该作业。如果指定的时间已经错过,at命令会在第二天的那个时间运行指定的作业。

暂略

6.6.2 cron时间表

暂略

6.6.3 使用新 shell 启动脚本

暂略

7 函数

7.1 基本的脚本函数

7.1.1 创建函数

有两种格式:

1
2
3
function name {
commands
}
1
2
3
name() {
commands
}

7.1.2 使用函数

要在脚本中使用函数,只需要像其他shell命令一样,在行中指定函数名就行了。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash 
# using a function in a script
function func1 {
echo "This is an example of a function"
}
count=1
while [ $count -le 5 ]
do
func1
count=$[ $count + 1 ]
done
echo "This is the end of the loop"
func1 # 使用函数
echo "Now this is the end of the script"
1
2
3
4
5
6
7
8
9
$ ./test1 
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is the end of the loop
This is an example of a function
Now this is the end of the script

7.2 返回值

bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码。有3种不同的方法来为函数生成退出状态码。

7.2.1 默认退出状态码

默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码。在函数执行结束后,可以用标准变量$?来确定函数的退出状态码。

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# testing the exit status of a function
func1() {
echo "trying to display a non-existent file"
ls -l badfile
}
echo "testing the function: "
func1
echo "The exit status is: $?"
1
2
3
4
5
$ ./test4
testing the function:
trying to display a non-existent file
ls: badfile: No such file or directory
The exit status is: 1

使用函数的默认退出状态码是很危险的。

7.2.2 使用return命令

bash shell使用return命令来退出函数并返回特定的退出状态码。return命令允许指定一个整数值来定义函数的退出状态码,从而提供了一种简单的途径来编程设定函数退出状态码。

示例

1
2
3
4
5
6
7
8
9
#!/bin/bash 
# using the return command in a function
function dbl {
read -p "Enter a value: " value
echo "doubling the value"
return $[ $value * 2 ]
}
dbl
echo "The new value is $?"

注意:如果在用$?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。其次,由于退出状态码必须小于256,函数的结果必须生成一个小于256的整数值。任何大于256的值都会产生一个错误值。

例如:

1
2
3
4
$ ./test5 
Enter a value: 200
doubling the value
The new value is 1 # 1为错误码

7.2.3 使用函数输出

正如可以将命令的输出保存到shell变量中一样,你也可以对函数的输出采用同样的处理办法。可以用这种技术来获得任何类型的函数输出,并将其保存到变量中:

1
result=$(dbl)

这个命令会将dbl函数的输出赋给$result变量。

示例

1
2
3
4
5
6
7
8
#!/bin/bash 
# using the echo to return a value
function dbl {
read -p "Enter a value: " value
echo $[ $value * 2 ]
}
result=$(dbl)
echo "The new value is $result"
1
2
3
4
5
6
$ ./test5b 
Enter a value: 200
The new value is 400
$ ./test5b
Enter a value: 1000
The new value is 2000

7.3 在函数中使用变量

7.3.1 传参

函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在$0变量中定义,函数命令行上的任何参数都会通过$1$2等定义。也可以用特殊变量$#来判断传给函数的参数数目。

语法格式:

1
func $value 10

示例

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
#!/bin/bash 
# passing parameters to a function
function addem {
if [ $# -eq 0 ] || [ $# -gt 2 ] # 参数量等于0或大于2,则返回-1
then
echo -1
elif [ $# -eq 1 ] # 只有一个参数
then
echo $[ $1 + $1 ] # 自己加自己
else
echo $[ $1 + $2 ] # 两个参数:则相加
fi
}
echo -n "Adding 10 and 15: "
value=$(addem 10 15)
echo $value
echo -n "Let's try adding just one number: "
value=$(addem 10)
echo $value
echo -n "Now trying adding no numbers: "
value=$(addem)
echo $value
echo -n "Finally, try adding three numbers: "
value=$(addem 10 15 20)
echo $value
1
2
3
4
5
$ ./test6 
Adding 10 and 15: 25
Let's try adding just one number: 20
Now trying adding no numbers: -1
Finally, try adding three numbers: -1

由于函数使用特殊参数环境变量作为自己的参数值,因此它无法直接获取脚本在命令行中的参数值。下面的例子将会运行失败。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# trying to access script parameters inside a function
function badfunc1 {
echo $[ $1 * $2 ]
}
if [ $# -eq 2 ]
then
value=$(badfunc1)
echo "The result is $value"
else
echo "Usage: badtest1 a b"
fi

尽管函数也使用了$1$2变量,但它们和脚本主体中的$1$2变量并不相同。要在函数中使用这些值,必须在调用函数时手动将它们传过去。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash 
# trying to access script parameters inside a function
function func7 {
echo $[ $1 * $2 ]
}
if [ $# -eq 2 ]
then
value=$(func7 $1 $2) # 将命令行的参数作为函数参数
echo "The result is $value"
else
echo "Usage: badtest1 a b"
fi

7.3.2 在函数中处理变量

暂略

7.4 数组变量

暂略

7.5 递归

暂略

7.6 创建库

bash shell允许创建函数库文件,然后在多个脚本中引用该库文件。

示例

有个叫作myfuncs的库文件,它定义了3个简单的函数。

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

和环境变量一样,shell函数仅在定义它的shell会话内有效。如果在shell命令行界面的提示符下运行myfuncs shell脚本,shell会创建一个新的shell并在其中运行这个脚本。它会为那个新shell定义这三个函数,但当运行另外一个要用到这些函数的脚本时,它们是无法使用的。

例如:

1
2
3
4
5
6
$ cat badtest4 
#!/bin/bash
# using a library file the wrong way
./myfuncs
result=$(addem 10 15)
echo "The result is $result"
1
2
3
$ ./badtest4 
./badtest4: addem: command not found
The result is

使用函数库的关键在于source命令。source命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。

source命令有个快捷的别名,称作点操作符(dot operator)。要在shell脚本中运行myfuncs库文件,只需添加下面这行:

1
. ./myfuncs # 假定myfuncs库文件和shell脚本位于同一目录

7.7 在命令行上使用函数

一旦在shell中定义了函数,就可以在整个系统中使用它了,无需担心脚本是不是在PATH环境变量里。就像函数定义在脚本里一样,在整个脚本里都可以使用函数。

有几种方法可以实现。

7.7.1 在命令行上创建函数

因为shell会解释用户输入的命令,所以可以在命令行上直接定义一个函数。

  • 单行创建:
1
2
3
$ function divem { echo $[ $1 / $2 ]; } 
$ divem 100 5
20

当在命令行上定义函数时,必须在每个命令后面加个分号,这样shell就能知道在哪里是命令的起止了。

1
2
3
4
$ function doubleit { read -p "Enter value: " value; echo $[ $value * 2 ]; }
$ doubleit
Enter value: 20
40
  • 多行创建
1
2
3
4
5
$ function multem { # 键入花括号后,再键入回车即可换行
> echo $[ $1 * $2 ] # 键入回车
> } # 键入回车结束
$ multem 2 5
10

当退出shell时,函数就消失了。

7.7.2 在~/.bashrc文件中定义函数

在用户家目录的.bashrc文件中定义函数。使用相同的用户打开新的终端时生效。

  • 直接定义函数

把函数放在文件末尾就行了。

1
2
3
4
5
6
7
8
9
10
11
# .bashrc 
# ...
# Source global definitions
if [ -r /etc/bashrc ]; then # 如果系统配置存在并可读
. /etc/bashrc # 则载入系统的库函数
fi

# 自己定义的函数
function addem {
echo $[ $1 + $2 ]
}

使用相同的用户打开新的终端时生效,或者手动source ~/.bashrc生效

  • 读取函数文件
1
2
3
4
5
6
7
8
# .bashrc 
# Source global definitions
if [ -r /etc/bashrc ]; then
. /etc/bashrc
fi

# 载入其他的库函数
. /home/rich/libraries/myfuncs

8 vim编辑器

8.1 vim基础

image-20230510113711456

vim编辑器会检测会话终端的类型,并用全屏模式将整个控制台窗口作为编辑器区域。

最初的vim编辑窗口显示了文件的内容(如果有内容的话),并在窗口的底部显示了一条消息行。如果文件内容并未占据整个屏幕,vim会在非文件内容行放置一个波浪线(如图所示)。

底部的消息行根据文件的状态以及vim安装时的默认设置显示了所编辑文件的信息。如果文件是新建的,会出现消息[New File]

vim有两种模式:

  • 普通模式(命令模式):vim编辑器会将按键解释成命令
  • 插入模式:vim会将在当前光标位置输入的每个键都插入到缓冲区。按下i键就可以进入插入模式。要退出插入模式回到普通模式,按下键盘上的退出键。

vim编辑器在普通模式下有个特别的功能叫命令行模式。命令行模式提供了一个交互式命令行,可以输入额外的命令来控制vim的行为。要进入命令行模式,在普通模式下按下冒号键。光标会移动到消息行,然后出现冒号,等待输入命令。

在命令行模式下有几个命令可以将缓冲区的数据保存到文件中并退出vim。

  • q:如果未修改缓冲区数据,退出。
  • q!:取消所有对缓冲区数据的修改并退出。
  • w filename:将文件保存到另一个文件中。
  • wq:将缓冲区数据保存到文件中并退出。

移动光标(普通模式)

  • PageDown(或Ctrl+F):下翻一屏。
  • PageUp(或Ctrl+B):上翻一屏。
  • G:移到缓冲区的最后一行。
  • num G:移动到缓冲区中的第num行。
  • gg:移到缓冲区的第一行。

8.2 编辑数据

在普通模式下,vim编辑器提供了一些命令来编辑缓冲区中的数据。

image-20230510114654495

8.3 复制粘贴

剪切和粘贴 dd+p

vim在删除数据时,实际上会将数据保存在单独的一个寄存器中。可以用p命令取回数据。

举例来说,可以用dd命令删除一行文本,然后把光标移动到缓冲区的某个要放置该行文本的
位置,然后用p命令。该命令会将文本插入到当前光标所在行之后。可以将它和任何删除文本的
命令一起搭配使用。

复制 yw+p y$+p

vim中复制命令是y(代表yank)。可以在y后面使用和d命令相同的第二字符(yw表示复制一个单词,y$表示复制到行尾)。在复制文本后,把光标移动到你想放置文本的地方,输入p命令。复制的文本就会出现在该位置。

8.4 查找替换

普通模式下,按下斜线(/),输入你要查找的文本后,按下回车键即可查找。使用n键,表示下一个(next)。

替换命令允许你快速用另一个单词来替换文本中的某个单词。必须进入命令行模式才能使用替换命令。替换命令的格式是:

1
:s/old/new/

9 sed和gawk入门

9.1 文本处理

9.1.1 搜索数据

grep命令的命令行格式如下:

1
grep [options] pattern [file]

grep命令会在输入或指定的文件file中查找包含匹配指定模式pattern的字符的行。grep的输出就是包含了匹配模式的行。

默认情况下,grep命令用基本的Unix风格正则表达式来匹配模式。

参数:

参数 含义 示例
-v 反向搜索(输出不匹配该模式的行) grep -v t file1
-n 显示匹配模式的行所在的行号 grep -n t file1
-c 有多少行含有匹配的模式 grep -c t file1
-e 指定多个匹配模式 grep -e t -e f file1

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ grep -v t file1 
one
four
five
$ grep -n t file1
2:two
3:three
$ grep -c t file1
2
$ grep -e t -e f file1
two
three
four
five

9.1.2 sed编辑器