Shell的基础
在 Linux 下,我们通常会出于以下原因或优点而使用 Shell 脚本:
- Shell 脚本在处理自动循环或大的任务方面可节省大量的时间,且功能强大;
- 创建一个脚本,在使用一系列系统命令的同时,可以使用变量、条件、算术和循环快速创建脚本以完成相应的工作(这可比在命令行下一个个敲入要节省大量的时间呢);
- Shell脚本可以在行命令中接收信息,并使用它作为另一个命令的输入。
Shell脚本的一般格式
Shell脚本通常不是复杂的程序,并且它是按行解释的。脚本第一行通常会以类似于 #!/bin/bash
开始,这段脚本用于通知 Shell 使用系统上的 Bourne Shell 解释器。
为什么说“类似于”呢?因为,实际上我们不仅可以使用 bash 解释器,还可以使用其他一些解释器,甚至是以命令开头,后面紧跟其参数。例如:
#!/usr/bin/awk
#!/bin/sed
Shell命令的通配符
星号“ *
”可以匹配文件名中的任何字符串。例如我们给出文件名模式 file*,它的意思是文件名以 file 开头,后面可以跟随任何字符串,包括空字符串。
$ ls file*
file file1 file2 file3 file_test
注意:在通配符里,一个星号“ * ”可以代表0个或多个任意字符。
问号“ ?
”可以匹配任何单个字符。例如我们给出文件名模式 file?,它的意思是文件名以 file 开头,以任意1个字符结尾的文件:
$ ls file?
file1 file2 file3
注意:一个问号“ ? ”要匹配1个任意字符。
方括号“ [ ]
”可以匹配任意单个指定的字符。下面的例子将列出文件名以 file 开头,以任意1个数字结尾的文件:
$ ls file[0-9]
file1 file2 file3
方括号“ [! ]
”可以匹配任意除指定的字符之外的单个字符。下面的例子中将列出文件名以 file 开头,不以数字结尾的文件:
$ ls file*[!0-9]
file_test
@H_404_83@Shell命令的输入和输出
在 Shell 脚本中,可以用几种不同的方式读入数据,可以使用标准输入(缺省为键盘),或者指定一个文件作为输入。
对于输出也一样,如果不指定某个文件作为输出,标准输出总是和终端屏幕相关联。如果所使用的命令出现了什么错误,它也会缺省输出到屏幕上,如果不想把这些信息输出到屏幕上,也可以把这些信息指定到一个文件中。
使用 echo
命令可以显示文本行或变量,或者把字符串输入到文件。它的一般形式如下:
echo string
echo 命令支持转义字符,比如:
- 不换行:“ \c ”
- 跳格:“ \t ”
- 换行:“ \n ”
echo -e "hello\tworld\n"
可以使用 read
语句从键盘或文件的某一行文本中读入信息,并将其赋给一个变量。如果只指定了一个变量,那么 read 将会把所有的输入赋给该变量,直至遇到第一个文件结束符或回车。它的一般形式如下:
read var1 var2 ... ...
通常,我们希望在读取输入的同时给出一些提示信息:
read -p "Please input your name: " name
UNIX从来都不是为人机交互而设计的,而是为程序之间的交互而设计的。
上面这句话是 Unix 的一个设计哲学,我们通常也会想到 Unix 的另一个设计哲学——一个程序只做好一件事。
好啦,说那么多,其实都是为了引出“管道”的概念。在 Unix 中,程序可以被看成是过滤器,程序之间的交互就是输入和输出。Unix 从很早以前就提供了管道机制,使得一个程序的输出可以通过一根管子(管道)与另一个程序的输入联系起来。
管道在 Shell 中被广泛使用,可以用竖杠“ |
”表示。它的一般形式如下:
命令1 | 命令2
表示把命令1的输出通过管道传递给命令2作为输入。
举个栗子:我们先执行命令 ls,列出当前文件名,然后将结果送入管道中,进而 wc 从管道读出这些信息,并计算总共有几个单词:
$ ls | wc -w
下面再来扯一下标准输入、标准输出和错误输出,每个程序开始运行时都会默认打开这三个文件,其文件描述符(fd)分别为0、1、2。
- 标准输入(stdin)的文件描述符为0,它是命令的输入,缺省是键盘,也可以是文件或其他命令的输出;
- 标准输出(stdout)的文件描述符为1,它是命令的输出,缺省是屏幕,也可以是文件;
- 标准错误(stderr)的文件描述符为2,它是命令的错误输出,缺省是屏幕,同样也可以是文件。
那我们怎么重新指定命令的标准输入、标准输出和错误输出呢?要实现这一点就需要使用文件重定向!
在对标准错误进行重定向时,必须要使用文件描述符,但是对于标准输入和输出来说,这不是 necessary。比如:
把标准输出重定向到一个新文件:command 1 > file
或者 command > file
;
把标准错误重定向到一个新文件:command 2 > file
;
以 file 文件作为标准输入:command 0 < file
;
如果希望以追加的方式重定向到一个文件,则把“ >
”替换为“ >>
”。
Shell命令的执行顺序
在执行某个命令的时候,有时需要依赖于前一个命令是否执行成功。例如,你希望将一个目录中的文件全部拷贝到另一个目录中后,再删除源目录中的全部文件。因此,再删除之前,你希望能够确保拷贝成功,否则就可能丢失所有的文件。
那么,你可以这么做:
cp -rf dir1/* dir2/* && rm -rf dir1/*
使用“ &&
”的一般形式为:
命令1 && 命令2
这种命令执行方式相当地直接:&& 左边的命令(命令1)返回真(即返回0,成功被执行)后,&& 右边的命令(命令2)才能够被执行。
换句话说,使用 && 的意思就是:
如果这个命令(命令1)执行成功 && 那么执行这个命令(命令2)
相应的还有“ ||
”,它的一般形式为:
命令1 || 命令2
与 && 相反,|| 的作用是:如果 || 左边的命令(命令1)未执行成功,那么就执行 || 右边的命令(命令2)。
换句话说,使用 || 的意思就是:
如果这个命令(命令1)执行失败了 || 那么就执行这个命令(命令2)
举个例子:我们希望从一个审计文件中抽取第1个和第5个域,并将其输出到一个临时文件中,如果这一操作未能成功,我们希望能够看到错误提示。
awk '{print $1 $5}' acc > a.tmp || echo "operation Failed"
grep命令
grep 是 Unix/Linux 中使用最广泛的命令之一。grep(Globally search a Regular Expression and Print)是一个强大的文本搜索工具,它能使用特定模式匹配搜索文本,并默认输出匹配行。grep 支持基本正则表达式,也支持其扩展集,Unix 的 grep 家族还包括 egrep 和 fgrep。
grep 一般格式为:
grep [选项] 基本正则表达式 [文件]
这里基本正则表达式可为字符串。在 grep 命令中输入字符串参数时,最好将其用双引号括起来,这样做有两个好处:一是以防被误解为 shell 命令,二是可以用来查找多个单词组成的字符串。
以下是一些常用的 grep 选项:
选项 | 描述 |
---|---|
-c |
只输出匹配行的计数 |
-i |
不区分大小写(只适用于单字符) |
-h |
查询多文件时不显示文件名 |
-l |
查询多文件时只输出包含匹配字符的文件名 |
-n |
显示匹配行及行号 |
-s |
不显示不存在或无匹配文本的错误信息 |
-v |
显示不包含匹配文本的所有行 |
正则表达式
随着对 Unix 和 Linux 熟悉程度的不断加深,需要经常接触到正则表达式这个领域。
使用 shell 时,从一个文件中抽取多于一个字符串将会很麻烦。例如:在一个文本中抽取一个词,它的头两个字符是大写的,后面紧跟四个数字。如果不使用某种正则表达式,在 shell 中将无法实现这个操作。
当从一个文件或命令输出中抽取或过滤文本时,可以使用正则表达式(RE),正则表达式是一些特殊或不很特殊的字符串模式的集合。
为了抽取或获得信息,我们给出抽取操作应遵守的一些规则。这些规则由一些特殊字符或进行模式匹配操作时使用的元字符组成。
选项 | 描述 |
---|---|
^ |
只匹配行首 |
$ |
只匹配行尾 |
* |
一个单字符后紧跟 *,匹配0个或多个此单字符 |
[] |
匹配 [] 内字符,可以是一个单字符,也可以是字符序列 |
\ |
用来屏蔽一个元字符的特殊含义 |
. |
匹配任意单字符 |
pattern\{n\} |
用来匹配前面 pattern 出现次数,n 为次数 |
pattern\{n,\} |
含义同上,但次数最少为 n |
pattern\{n,m\} |
含义同上,但 pattern 出现次数在 n 与 m 之间 |
上古神器:awk 与 sed
awk命令
awk [-F 分隔符] 'awk命令' <待处理文件>
注意:[-F 分隔符] 是可选的,awk 使用空格作为缺省的分隔符。
awk -f '包含awk命令的文件' 待处理文件
(3)将所有 awk 命令插入一个文件,并使 awk 程序可执行,然后用 awk 命令解释器作为脚本的首行,以便通过键入脚本名称来调用它。
awk 的模式和动作
任何 awk 语句都由模式和动作组成。在一个 awk 脚本中可能有许多语句。模式部分决定动作语句何时触发及触发事件。处理即对数据进行的操作,如果省略模式部分,动作将时刻保持执行状态。
实际动作在大括号 { } 内指明。动作大多数用来打印,但是还有些更长的代码诸如 if 和循环语句及循环退出结构。如果不指明采取动作,awk将打印出所有浏览出来的记录。
awk 的域和记录
awk 执行时,其浏览域标记为 $1,$2,...,$n
,这种方法称为域标识。使用这些域标识将更容易对域进行更进一步的处理。
例如,如下命令的作用是打印文件 file 中的第一个域:
awk '{print $1}' file
(注意:上面示例中没有模式,只有动作,即 { } 里面的语句。)
awk 的条件操作符示例
操作符 | 含义 | 命令 | 描述 |
---|---|---|---|
< |
小于 | awk '$7<30 {print $0}' file |
将$7 小于30的行打印出来 |
<= |
小于等于 | awk '$7<=30 {print $0}' file |
将$7 小于等于30的行打印出来 |
== |
等于 | awk '$7==30 {print $0}' file |
将$7 等于30的行打印出来 |
!= |
不等于 | awk '$7!=30 {print $0}' file |
将$7 不等于30的行打印出来 |
>= |
大于等于 | awk '$7>=30 {print $0}' file |
将$7 大于等于30的行打印出来 |
~ |
匹配 | awk '$0~/48/ {print $0}' file |
将能匹配48的行打印出来 |
!~ |
不匹配 | awk '$0!~/48/ {print $0}' file |
将不能匹配48的行打印出来 |
sed命令
sed 是一个非交互性文本流编辑器,它编辑文件或标准输入导出的文本拷贝。标准输入可能是来自键盘,文件重定向,字符串或变量,又或者是一个管道的文本。
使用 sed 需要记住的一个重要事实是:无论命令是什么,sed 并不与初始化文件打交道,它操作的只是一个拷贝,然后所有的改动如果没有重定向到一个文件,将输出到屏幕。
跟 grep 和 awk 一样,sed 是一个重要的文本过滤工具,或者使用一行命令或者使用管道与 grep 和 awk 相结合。
调用 sed 有三种方式:
(1)在命令行键入命令
sed [选项] 'sed命令' 输入文件
注意:在命令行使用 sed 命令时,实际命令要加单引号,sed 也允许加双引号。
sed [选项] -f sed脚本文件 输入文件
(3)将 sed 命令插入脚本文件,并使 sed 脚本可执行
sed脚本文件 [选项] 输入文件
Shell脚本控制
结构化命令
使用 if-then 语句,语法如下:
if [ command ]; then
other commands
fi
或者:
if [ command ]; then
other commands
else
other commands
fi
注意:当 command 退出码为0时(即正常退出),执行 if 语句,否则执行 else 语句。
在 shell 编程中,我们常常需要判断各种条件,以便执行不同路径。test 命令提供了在 if-then 语句中测试不同条件的途径,如果 test 命令中列出的条件成立,那么 test 命令将会退出且返回0。格式如下:
test condition
Bash 提供了另一种在 if-then 语句中使用 test 的方法:
if [ confition ]; then
...
fi
其中的条件判断可分为3类:
(1)数值比较
比较 | 描述 |
---|---|
n1 -eq n2 | 检查n1是否等于n2 |
n1 -ge n2 | 检查n1是否大于或等于n2 |
n1 -gt n2 | 检查n1是否大于n2 |
n1 -le n2 | 检查n1是否小于或等于n2 |
n1 -lt n2 | 检查n1是否小于n2 |
n1 -ne n2 | 检查n1是否不等于n2 |
(2)字符串比较
比较 | 描述 |
---|---|
str1 = str2 | 检查str1与str2是否相同 |
str1 != str2 | 检查str1与str2是否不同 |
str1 < str2 | 检查str1是否小于str2 |
str1 > str2 | 检查str1是否大于str2 |
-n str | 检查str的长度是否为非0 |
-z str | 检查str的长度是否为0 |
(3)文件比较
比较 | 描述 |
---|---|
-d file | 检查file是否存在且是一个目录 |
-e file | 检查file是否存在 |
-f file | 检查file是否存在且是一个普通文件 |
-r file | 检查file是否存在且可读 |
-s file | 检查file是否存在且非空 |
-w file | 检查file是否存在且可写 |
-x file | 检查file是否存在且可执行 |
虽然 if-then 语句可以胜任条件判断/分支执行的工作,但是如果分支过多,则会导致代码臃肿。因此,可以使用 case 语句来替代,避免过长的 if-then 语句。shell 中的 case 语句类似于 C 语言的 switch 语句,一般形式如下:
case varible in
pattern1 | pattern2) command1;;
pattern3) command2;;
*) command3;;
esac
Shell 同样支持循环语句,包括 for 命令和 while 命令。
其中 for 循环格式如下:
for varible in list
do
commands
done
在 list 参数中,需要提供迭代中一系列要使用的值,在每个迭代中,varible 会包含列表中的当前值,一次使用一个值,以此类推。
while 语句可以看成是 if-then 语句和 for 循环的混合。while 语句允许你定义一个要测试的命令,如果测试命令返回的退出状态码是0,则循环执行一组命令。格式如下:
while test command
do
other commands
done
用户输入
Shell 脚本允许你在运行它的同时给它传递参数,例如:
./somescript.sh abcd 100
这两个参数,在脚本里面可以使用 $1
与 $2
来获取,而 $0
代表的是脚本名字本身。
需要注意的一个细节是:当命令行参数超过 9 个,比如第 10 个参数,引用的时候必须使用花括号括起来,例如:${10}
,这种技术使得可以向脚本添加任意多个参数。
此外,我们还应该记住下面这些特殊的参数变量。
$#
特殊变量代表脚本运行时带有的命令行参数个数(不包含脚本名在内),对于上面的命令,$#
的值为2。
这样的话,如果我们想知道最后一个参数的值,就可以利用这个特殊变量,而不需要知道总共有多少个参数。噔!噔!噔!—— ${$#}
。
$*
,$@
变量的含义是相同的,它们会将命令行上提供的所有参数当作同一个字符串中的多个独立的单词。这样就可以使用 for 来遍历所有的值:
#!/bin/bash
count=1
for param in $*
do
echo "\$* : #$count = $param"
count=$(($count + 1))
done
我们输入 ./test.sh a b c
,其输出结果如下:
$* : #1 = a
$* : #2 = b
$* : #3 = c
脚本函数
函数的定义格式如下:
function_name() { commands }
需要注意的是:
虽然函数的定义中没有出现参数列表,但是在调用函数的时候,依然可以传参,像这样:
function_name 12 34