本章目标:
- 通过编写简单的shell脚本自动执行命令序列
- 使用for循环和条件,借助脚本中的项目列表或通过命令行高效地运行命令
- 使用grep命令和正则表达式,查找与日志文件和命令输出中的模式相匹配的文本
一、编写简单的BASH脚本
1. 创建和执行BASH SHELL脚本
许多简单、常见的系统管理任务都可以使用命令行工具来完成,而更复杂的任务通常需要将多个命令链接到一起并在它们之间互传结果。借助bash shell环境和脚本编写功能,我们可以将Linux命令与shell脚本组合在一起使用,来解决实际问题。
精通shell脚本编写是在所有操作环境中成功进行系统管理的必备要素。shell脚本编写的工作知识在企业环境中至关重要,因为脚本可以提高完成日常任务的效率和准确性。
1.1. 指定命令解释器
脚本的第一行以符号‘#!’开头,通常称为sh-bang或she-bang,其源于升半音符号(sharp)和感叹号(bang)这两个符号的名称。这一特定的双字节符号(magic number)表示可解释性脚本;符号后面的语法为处理该脚本的行时所需要的正确命令解释器的完全限定文件名。(想要了解magic number在Linux中如何指示文件类型,请参阅**file(1)和magic(5)**的man page)
对于使用bash脚本缩写语法的脚本文件,shell脚本第一行的开头如下:
#!/bin/bash
1.2. 执行bash shell脚本
完成的shell脚本必须为可执行文件,能作为常规命令运行。使用chmod命令可添加执行权限,并且可能与chown命令组合以更改脚本的文件所有权。仅为脚本的目标用户授予执行权限。
如果将脚本放在shell的PATH环境变量中所列的某个目录下,则可以像其他命令那样单独用文件名来调用shell脚本。shell会使用通过该文件名找到的第一个命令;应避免为shell脚本的文件名使用现有的命令名。或者,还可以通过在命令行上输入脚本的路径名来调用shell脚本。which命令,后面跟可执行的脚本的文件名,可以显示所要执行的命令的路径名。
[user@host ~]$ which hello
~/bin/hello
[user@host ~]$ echo $PATH
/home/user/.local/bin:/home/user/bin:/usr/share/Modules/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
1.3. 对特殊字符加引号
一些字符和词语对bash shell具有特殊含义,但是,我们偶尔需要使用这些字符的字面值,而非其特殊含义。为此,可以使用以下三种工具之一来取消(或转义)特殊含义:反斜杠()、单引号(’ ')或双引号(" ")。
(1)反斜杠,转义字符可取消紧跟在该字符后面的单个字符的特殊含义。例如,bash中 # 符号具有注释的特殊含义,若要使用 echo 命令显示字面值字符串 # not a comment ,则要在 # 符号前面加上反斜杠字符。
[user@host ~]$ echo # not a comment
[user@host ~]$ echo \# not a comment
# not a comment
(2)单引号,保留其括起的所有字符的字面含义。
[user@host ~]$ echo # not a comment #
[user@host ~]$ echo \# not a comment #
# not a comment
[user@host ~]$ echo \# not a comment \#
# not a comment #
[user@host ~]$ echo '# not a comment #'
# not a comment #
(3)双引号,可以阻止通配和shell扩展,但依然允许命令和变量替换。
变量替换在概念上与命令替换一样,但可能使用可选的大括号语法。
使用单引号则可以按字面解译所有的文本。除了阻止通配和shell扩展外,单引号也指示shell额外阻止命令替换和变量替换。==问号(?)==也是一个需要防止扩展的元字符(meta-character)。
[user@host ~]$ var=$(hostname -s); echo $var
host
[user@host ~]$ echo "***** hostname is ${var} *****"
***** hostname is host *****
[user@host ~]$ echo Your username variable is \$USER.
Your username variable is $USER.
[user@host ~]$ echo "Will variable $var evaluate to $(hostname -s)?"
Will variable host evaluate to host?
[user@host ~]$ echo 'Will variable $var evaluate to $(hostname -s)?'
Will variable $var evaluate to $(hostname -s)?
[user@host ~]$ echo "\"Hello, world\""
"Hello, world"
[user@host ~]$ echo '"Hello, world"'
"Hello, world"
1.4. 从Shell脚本提供输出
通过将文本作为参数传递给命令, echo命令可显示任意文本。默认情况下,文本将在标准输出(STDOUT)上显示,但是也可以使用输出重定向到标准错误(STDERR)。在下面的简单Bash脚本中, echo命令向STDOUT显示消息 “Hello, world"。
[user@host ~]$ cat ~/bin/hello
#!/bin/bash
echo "Hello, world"
[user@host ~]$ hello
Hello, world
echo命令在shell脚本中被广泛用来显示信息或错误消息。这些消息可能是有用的脚本进度指示符,并且可以定向到标准输出、标准错误或者重定向到日志文件以进行存档。显示错误消息时,一种良好的做法是将它们定向到STDERR以便更容易区分错误消息与正常状态消息。
[user@host ~]$ cat ~/bin/hello
#!/bin/bash
echo "Hello, world"
echo "ERROR: Houston, we have a problem." >&2
[user@host ~]$ hello 2> hello.log
Hello, world
[user@host ~]$ cat hello.log
ERROR: Houston, we have a problem.
当尝试调试有问题的shell脚本时,echo命令可能也非常有用。向行为不同于预期的脚本部分中添加echo语句可帮助阐明正在执行的命令以及正在调用的变量的值。
二、使用循环更高效地运行命令
1. 使用循环来迭代命令
系统管理员在其日常生活中经常会遇到重复任务。重复任务可能表现为对目标多次执行某个操作,例如在10分钟内,每隔一分钟检查进程来查看其是否已完成。任务重复还可能表现为一次对多个目标执行某个操作,例如对系统上的每个数据库进行备份。for循环是bash提供的多个shell循环结构之一,并且可用于任务迭代。
1.1. 从命令行处理项目
bash的for循环结构使用以下语法:
for VARIABLE in LIST; do
COMMAND VARIABLE
done
循环按顺序逐一处理LIST中提供的字符串,并且在处理列表中的最后一个字符串之后退出。列表中的每个字符串临时存储为VARIABLE的值,而for循环执行包含在其结构中的命令块。变量的命名是任意的。通常,变量值由命令块中的命令进行引用。
可以通过多种方式来为for循环提供字符串列表。可以是用户直接输入的字符串的列表,或者是通过不同类型的shell扩展生成,如变量扩展、花括号扩展、文件名扩展或命令替换。下面部分示例演示了可以向for循环提供字符的不同方式:
[user@host ~]$ for HOST in host1 host2 host3; do echo $HOST; done
host1
host2
host3
[user@host ~]$ for HOST in host{1,2,3}; do echo $HOST; done
host1
host2
host3[user@host ~]$ for HOST in host{1..3}; do echo $HOST; done
host1
host2
host3
[user@host ~]$ for FILE in file*; do ls $FILE; done
filea
fileb
filec
[user@host ~]$ for FILE in file{a..c}; do ls $FILE; done
filea
fileb
filec
[user@host ~]$ for PACKAGE in $(rpm -qa | grep kernel); \
do echo "SPACKAGE was installed on \
$(date -d @S(rpm -q --qf "%{INSTALLTIME}\n" SPACKAGE)}"; done
abrt-addon-kerneloops-2.1.11-12.e17.x86_64 was installed on Tue Apr 22 00:00:07 EDT 2014
kernel-3.10.0-121.e17.x86_64 was installed on Thu Apr 10 15:27:52 EDT 2014
kernel-tools-3.10.0-121.e17.x86_64 was installed on Thu Apr 10 15:20:01 EDT 2014
kernel-tools-libs-3.10.0-121.e17.x86_64 was installed on Thu Apr 10 15:26:22 EDT 2014
[user@host ~]$ for EVEN in $(seq 2 2 10); do echo "$EVEN"; done
2
4
6
8
10
2. 在脚本中使用退出代码
在处理完自己的所有内容后,脚本会退出到调用它的进程。但是,有时候可能需要在完成之前退出脚本,比如在遇到错误条件时,可以通过在脚本中使用exit命令来实现这一目的。当脚本遇到exit命令时,脚本将立即退出且不会对脚本的其余内容进行处理。
可以使用可选的整数参数(0到255之间,表示退出代码)来执行exit命令。退出代码是进程完成后返回的代码。退出代码值0表示没有错误。所有其他非零值都表示存在错误的退出代码。我们可以使用不同的非零值来区分遇到的不同类型错误。此退出代码传回到父进程,后者将它存储在 ? 变量中,并可通过 $? 进行访问,如下所示:
[user@host bin]$ cat hello
#!/bin/bash
echo "Hello, world"
exit 0
[user@host bin]$ ./hello
Hello, world
[user@host bin]$ echo $?
0
如果不使用任何参数调用exit命令,那么脚本将退出并且将最后执行的命令的退出状态传递给父进程。
3. 测试脚本输入
为确保脚本不会由于意外情况而轻易中断,一种良好的做法是不要进行与输入有关的假设,如命令行参数、用户输入、命令替换、变量扩展及文件名扩展。可以使用bash的test命令来执行完整性检查。
与所有命令一样,test命令会在完成后生成一个退出代码,该退出代码存储为值 $?。再次说明,退出状态值0表示测试成功,而非零值表示测试失败。
执行测试时要用到多种运算符。运算符可用于确定某个数值是大于、大于等于、小于、小于等于,还是等于另一个数值。它们可用于测试某个文本字符串与另一个文本字符串是相同还是不同。运算符也可用于评估变量是否具有某个值。
以下示例演示了如何使用bash的数字比较运算符来使用该test命令:
[user@host ~]$ test 1 -gt 0 ; echo $?
0
[user@host ~]$ test 0 -gt 1 ; echo $?
1
可以使用bash的测试命令语法 [ <TESTEXPRESSION> ] 来执行测试。也可以使用bash的较新扩展测试命令语法 [[ <TESTEXPRESSION> ]] (在bash版本2.02及更高版本中可用,可提供诸如通配模式匹配和正则表达式模式匹配等功能)执行这些测试。
以下示例演示了使用bash的test命令语法和bash的数字比较运算符:
[user@host ~]$ [ 1 -eq 1 ]; echo $?
0
[user@host ~]$ [ 1 -ne 1 ]; echo $?
1
[user@host ~]$ [ 8 -gt 2 ]; echo $?
0
[user@host ~]$ [ 2 -ge 2 ]; echo $?
0
[user@host ~]$ [ 2 -lt 2 ]; echo $?
1
[user@host ~]$ [ 1 -lt 2 ]; echo $?
0
以下示例演示了bash的字符串比较运算符的使用:
[user@host ~]$ [ abc = abc ]; echo $?
0
[user@host ~]$ [ abd == def ]; echo $?
1
[user@host ~]$ [ abc != def ]; echo $?
0
[user@host ~]$ STRING=''; [ -z "$STRING" ]; echo $?
0
[user@host ~]$ STRING='abc'; [ -n "$STRING" ]; echo $?
0
4. 条件结构
简单的shell脚本表示从头到尾执行的命令的集合。条件结构允许用户在shell脚本中包含决策,以便仅当满足特定条件时才执行脚本的特定部分。
4.1. 使用if/then结构
bash中最简单的条件结构是 if/then 结构,语法如下:
if <CONDITION>; then
<STATEMENT>
...
<STATEMENT>
fi
通过此结构,如果满足给定条件,将采取一个或多个操作。如果不满足给定条件,则不采取任何操作。前面演示的数字、字符串和文件测试经常用于在if/then语句中测试条件。末尾的fi语句用于结束if/then结构。
以下代码段演示了使用if/then结构来启动psacct服务(如果其未处于活动状态)。
[user@host ~]$ systemctl is-active psacct > /dev/null 2>&1
[user@host ~]$ if [ $? -ne 0 ]; then
> sudo systemctl start psacct
> fi
4.2. 使用if/then/else结构
if/then结构可以进一步扩展,以便能够根据是否满足条件来采取不同的操作集合。使用if/then/else结构可实现此目标。
if <CONDITION>; then
<STATEMENT>
...
<STATEMENT>
else
<STATEMENT>
...
<STATEMENT>
fi
以下代码段演示了使用if/then/else语句来启动psacct服务(如果其未处于活动状态)和停止该服务(如果其处于活动状态)。
[user@host ~]$ systemctl is-active psacct > /dev/null 2>&1
[user@host ~]$ if [ $? -ne 0 ]; then
> sudo systemctl start psacct
> else
> sudo systemctl stop psacct
> fi
4.3. 使用if/then/elif/then/else结构
最后,if/then/else结构可以进一步扩展以测试多个条件:在满足某个条件时执行不同的操作集合。以下示例中显示了其结构:
if <CONDITION>; then
<STATEMENT>
...
<STATEMENT>
elif <CONDITION>; then
<STATEMENT>
...
<STATEMENT>
else
<STATEMENT>
...
<STATEMENT>
fi
在此条件结构中,bash将按照显示的顺序测试条件。在发现某个条件成立后,bash将执行与该条件相关联的操作,然后跳过条件结构的其余部分。如果所有条件均不成立,bash将执行else子句中枚举的操作。
以下代码段演示了使用if/then/elif/then/else语句来运行mysql客户端(如果mariadb服务处于活动状态),运行psql客户端(如果postgresql服务处于活动状态)或运行sqlite3客户端(如果mariadb和postgresql服务均未处于活动状态)。
[user@host ~]$ systemctl is-active mariadb > /dev/null 2>&1
MARIADB_ACTIVE=$?
[user@host ~]$ sudo systemctl is-active postgresql > /dev/null 2>&1
POSTGRESQL_ACTIVE=$?
[user@host ~]$ if [ "$MARIADB_ACTIVE" -eq 0 ]; then
> mysql
> elif [ "$POSTGRESQL_ACTIVE" -eq 0 ]; then
> psql
> else
> sqlite3
> fi
三、使用正则表达式匹配命令输出中的文本
1. 编写正则表达式
正则表达式提供了一种便于查找特定内容的模式匹配机制。vim、grep和less命令都可以使用正则表达式。诸如Perl、Python和C等编程语言在使用模式匹配条件时,也都会使用正则表达式。
正则表达式自成体系,也就是说,该语言有其自身的语法和规则。我们将探讨使用正则表达式时所用的语法,并展示一些正则表达式的示例。
1.1. 描述简单的正则表达式
最为简单的正则表达式是完全匹配。如果正则表达式中的字符与正在搜索的数据中的类型和顺序均匹配,即为完全匹配。
假设用户正在以下文件中查找出现的所有cat模式:
cat
dog
concatenate
dogma
category
educated
boondoggle
vindication
chilidog
cat与c加a加t(中间没有其他字符)完全匹配。使用cat作为正则表达式来搜索上一个文件将返回以下匹配项:
cat
concatenate
category
educated
vindication
1.2. 匹配行首和行尾
以上部分对文件使用了完全匹配正则表达式。请注意,无论正则表达式在行的哪个位置(行首、行中或行尾)执行操作,均会匹配搜索字符串。使用行定位符控制正则表达式在哪个位置上查找匹配项。
若要在行首搜索,请使用脱字符号(^)。若要在行尾搜索,请使用美元符号($)。
在使用上述文件的情况下,^cat正则表达式将匹配两个词语。$cat正则表达式找不到任何匹配的词语。
cat
dog
concatenate
dogma
category
educated
boondoggle
vindication
chilidog
要想在文件中查找以dog为结尾的行,需要使用该精确表达式和行尾定位符来创建正则表达式dog$。将dog$应用到文件将会找到两个匹配项:
dog
chilidog
要查找一行中的唯一词语,需要同时使用行首和行尾定位符。例如,要查找作为一行中唯一词语的cat,需要使用**^cat$**。
cat dog rabbit
cat
horse cat cow
cat pig
1.3. 向正则表达式中添加通配符和倍数
正则表达式使用句点或点(.)来匹配除换行符之外的任何单个字符。正则表达式c.t将搜索包含c加任何单个字符加t的字符串。匹配示例包括:cat、concatenate、vindication、c5t和c$t。
使用不受限制的通配符时,将无法预测与通配符匹配的字符。要匹配特定字符,可以将不受限制的通配符替换为可接受的字符。将正则表达式更改为c[aou]t会匹配以下模式:以c开头,后面跟着a、o或u,然后是t。
倍数是常与通配符一起使用的机制。倍数应用到正则表达式中的前一位字符。更为常用的倍数之一,是星号(*)。在正则表达式中使用时,该倍数表示匹配前一表达式的零项或多项。我们可以在表达式中使用 *,而不仅限于字符。示例:c[aou]t。正则表达式c.*t将匹配cat、coat、culvert乃至ct(c和t之间没有字符)。任何以c开头,后面跟着零个或多个字符,最后以t结尾的数据。
另一种类型的倍数将会指示模式中前面字符的期望个数。'c.\{2\}t’是使用显式倍数的一个示例。该正则表达式将匹配以c开头,后面跟着任意两个字符,最后以t结尾的任何词语。**‘c.\{2\}t’**与以下示例中的两个词语匹配:
cat
coat convert
cart covert
cypher
正则表达式
选项 | 描述 |
---|---|
. | 句点(.)匹配任何单个字符 |
? | 前面的项目是可选的,且最多匹配一次 |
* | 前面的项目将匹配零次或多次 |
+ | 前面的项目将匹配一次或多次 |
{n} | 前面的项目恰好匹配n次 |
{n,} | 前面的项目匹配n次或更多次 |
{,m} | 前面的项目最多匹配m次 |
{n,m} | 前面的项目至少匹配n次,但不超过m次 |
[alnum:] | 字母数字字符:’[:alpha:]‘和’[:digit:]’;在C语言环境和ASCII字符编码中,等同于’[0-9A-Za-z]’ |
[:alpha:] | 字母字符:’[:lower:]‘和’[:upper:]’;在C语言环境和ASCII字符编码中,等同于’[A-Za-z]’ |
[:blank:] | 空白字符:空格和制表符 |
[:cntrl:] | 控制字符;在ASCII中,这些字符对应八进制代码000到037和177(DEL) |
[:dight:] | 数字:0 1 2 3 4 5 6 7 8 9 |
[:graph:] | 图形字符:’[:alnum:]‘和’[:punct:]’ |
[:lower:] | 小写字母;在C语言环境和ASCII字符编码中,对应’[a-z]’ |
[:print:] | 可打印字符:’[:alnum:]’、’[:punct:]'和空格 |
[:punct:] | 标点符号;在C语言环境和ASCII字符编码中,对应!"#$%&’()*+,-./:;<=>?@[\]^_`{|}~ |
[:space:] | 空格字符;在C语言环境中,对应制表符、换行符、垂直制表符、换页符、回车符和空格 |
[:upper:] | 大写字母;在C语言环境和ASCII字符编码中,对应’[A-Z]’ |
[:xdigit:] | 十六进制数字:0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f |
\b | 匹配词语两侧这空字符串 |
\B | 匹配词语中间的空字符串 |
\< | 匹配词语开头的空字符串 |
\> | 匹配词语末尾的空字符串 |
\w | 匹配词语组分;’[_[:alnum:]]'的同义词 |
\W | 匹配非词语组合;’[^_[:alnum:]]'的同义词 |
\s | 匹配空格;’[[:space:]]'的同义词 |
\S | 匹配非空格;’[^[:space:]]'的同义词 |
2. 通过GREP匹配正则表达式
用作部分分发的grep命令使用正则表达式来隔离匹配的数据。
2.1. 使用grep命令隔离数据
grep命令提供了一个正则表达式和一个文件(该文件中应有此正则表达式的匹配项)
[user@host ~]$ grep '^computer' /usr/share/dict/words
computer
computerese
computerise
computerite
computerizable
computerization
computerize
computerized
computerizes
computerizing
computerlike
computernik
computers
通过竖线运算符(|),grep命令可与其他命令一起使用。例如:
[root@host ~]$ ps aux | grep chrony
chrony 662 0.0 0.1 29440 2468 ? S 10:56 0:00 /usr/sbin/chronyd
grep常用选项
grep命令具有许多有用的选项,用于调整grep如何使用提供的正则表达式来处理数据
选项 | 功能 |
---|---|
-i | 使用所提供的正则表达式,但不会强制区分大小写(运行不区分大小写的操作) |
-v | 仅显示不包含正则表达式匹配项的行 |
-r | 将递归地匹配正则表达式的数据搜索应用到一组文件或目录中 |
-A NUMBER | 显示正则表达式匹配项之后的行数 |
-B NUMBER | 显示正则表达式匹配项之前的行数 |
-e | 如果使用多个-e选项,则可以提供多个正则表达式,并将与逻辑OR一起使用 |
2.2. grep选项及示例
略