Fork me on GitHub

Linux 入门总结(十二)

Linux 入门总结(十二) —— Shell 编程

基础正则表达式

1、正则表达式与通配符
正则表达式用来在文件中匹配符合条件的字符串,正则是包含匹配。grep、awk、sed 等命令可以支持正则表达式。

通配符用来匹配符合条件的文件名,通配符是完全匹配。ls、find、cp 这些命令不支持正则表达式,所以只能使用 Shell 自己的通配符来进行匹配了。

注意:通配符和正则表达式这种区分,仅限于 Linux 的 Shell 当中。

基础正则表达式

注意:这里是基础正则表达式,还存在扩展正则表达式。

注意:「^」位于中括号里面表示取反,位于外面表示行首。

test.txt 内容如下,将适用于以下所有用到 test.txt 的地方:

ID Name Gender Score
1 zhangsan male 86
2 lisi female 90
3 wangwu male 83
  1. 「*」示例:

    1
    2
    3
    4
    5
    6
    7
    8
    # 匹配所有内容,包括空白行(即 a 出现了 0 次),相当于列出整个文档
    grep "a*" test.txt

    # 匹配至少包含一个 a 的行
    grep "aa*" test.txt

    # 匹配至少包含两个 a 的行
    grep "aaa*" test.txt
  2. 「.」示例:

    1
    2
    3
    4
    5
    6
    7
    8
    # s..d 匹配在 s 和 d 之间一定有两个字符的所在行
    grep "s..d" test.txt

    # 匹配在 s 和 d 之间有任意字符,把「.*」当做一个整体看,表示除换行符外任意一个字符出现任意次
    grep "s.*d" test.txt

    # 匹配所有内容
    grep ".*" test.txt
  3. 「^」行首、「$」行尾示例:

    1
    2
    3
    4
    5
    6
    7
    8
    # 以 M 开头的行
    grep "^M" test.txt

    # 以 n 结尾的行
    grep "n$" test.txt

    # 匹配空白行
    grep -n "^$" test.txt
  4. 「[]」示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    注意:中括号里面是不能匹配换行符的。

    # 匹配 s 和 i 之间,要么是 a、要么是 o。注意:一个中括号只匹配一个字符
    grep "s[ao]id" test.txt

    # 匹配任意一个数字
    grep "[0-9]" test.txt

    # 匹配以小写字母开头的行
    grep "^[a-z]" test.txt

    # 匹配不用小写字母开头的行
    grep "^[^a-z]" test.txt

    # 匹配不用字母开头的行
    grep "^[^a-zA-Z]" test.txt
  5. 「\」转义符示例

    1
    2
    3
    4
    注意:「.」在正则里面表示任意一个字符,所以这里要加转义符。

    # 匹配使用「.」结尾的行
    grep "\.$" test.txt
  6. 其他示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # a 连续出现 3 次
    grep "a\{3\}" test.txt

    # 包含连续的 3 个数字的字符串所在行
    grep "[0-9]\{3\}" test.txt

    # 匹配最少用连续三个数字开头的行
    grep "^[0-9]\{3,\}" test.txt

    # 匹配在 s 和 i 之间,最少有一个 a,最多有三个 a
    grep "sa\{1,3\}i" test.txt

字符截取命令

1、cut 字段提取命令「注意:cut 命令默认列之间的分隔符为 tab 键」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
格式:cut [选项] 文件名

选项:
-f列号 提取第几列
-d分隔符 按照指定分隔符分割列

举例:
# 提取第二列
cut -f 2 test.txt

# 提取第二、第三列,注意不是范围,是指定的列,中间以逗号分隔
cut -f 2,3 test.txt

# 指定列之间的分隔符为「:」,默认为 tab 键
cut -d ":" -f 1,3 test.txt

注意:grep 命令表示在指定的文件中提取匹配的行,cut、awk 命令用来提取匹配的列。cut 命令中列之间的默认分割符为制表符即 tab 键。命令 grep -v [内容],表示列出不包含指定的内容的行。grep 确认行,cut 确认列,利用这两个命令可以做一些操作,比如,获取所有普通用户的用户名:cat /etc/passwd | grep /bin/bash | grep -v root | cut -d ":" -f 1

cut 命令有一个局限就是「无法」识别空格(指识别起来超级麻烦扩展性还差)。对于空格的判断我们使用 awk 命令,但是因为 awk 比较复杂,所以能用 cut 的尽量不要用 awk,因为 cut 简单,但是存在局限性。

2、printf 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
格式:printf '输出类型输出格式' 输出内容

输出类型:
%ns 输出字符串。n 是数字,指代输出几个字符
%ni 输出整数。n 是数字,指代输出几个数字
%m.nf 输出浮点数。m 和 n 是数字,指代输出的整数位数和小数位数。
eg:%8.2f 代表共输出 8 位数,其中 2 位是小数,6 位是整数

输出格式:
\a 输出警告音
\b 输出退格键,即 Backspace 键
\f 清除屏幕
\n 换行
\r 回车,即 Enter 键
\t 水平输出 Tab 键
\v 垂直输出 Tab 键

举例:
# 注意以下命令中是否加了单引号或双引号
printf %s 1 2 3 4 5 6
printf %s %s %s 1 2 3 4 5 6
printf '%s %s %s' 1 2 3 4 5 6
printf '%s %s %s\n' 1 2 3 4 5 6

printf 是格式化输出命令,类似于 echo 命令。注意:printf 后面不能直接加文件名,也不能接受管道符的内容。但是可以使用 printf '%s' $(cat 文件名) 的形式输出命令执行结果,需要注意的是 printf 不调整输出格式。

printf 命令没有 cat 和 echo 的自动格式化功能,为什么还要学习 printf 命令,是因为在 awk 命令中不能调用系统命令 cat 或者 echo,只能使用 printf。

在 awk 命令的输出中支持 print 和 printf 命令:

print:print 会在每个输出之后自动加入一个换行符(Linux 默认没有 print 命令)。
printf:printf 是标准格式输出命令,并不会自动加入换行符,如果需要换行,需要手工加入换行符。

注意:print 只能在 awk 中使用。printf 命令在 Linux 系统中直接使用的几率不大,主要是在 awk 命令中使用。

3、awk 命令,注意单引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
格式:
awk '条件1{动作1} 条件2{动作2} ...' 文件名

条件:
一般使用关系表达式作为条件,如:
x > 10
x <= 10

动作:
格式化语句
流程控制语句

举例:
# $2 表示第 2 列,$4 表示第 4 列
awk '{printf $2 "\t" $4 "\n"}' test.txt

df -h | awk '{printf $1 "\t" $3 "\n"}'

典例:获取系统中根分区硬盘的使用率,可以用来报警,如果超过80则给管理员提示。
df -h | grep 只有根分区才存在的字符串 | awk '{printf $5}' | cut -d "%" -f 1

注意:awk 命令虽然是列提取命令,但是它处理数据的时候,是先读入一行数据,然后把这行数据所有的内容都复制给对应的变量,$1 代表第一列,依次类推,然后再判断条件是否符合,对满足条件的执行相应的动作。

注意:awk 默认是以空格或者制表符作为分割符的。

注意:BEGIN 必须大写。它位于大括号前,表示它是一个条件,只有满足了这个条件,其后的动作才会被执行。BEGIN 的作用是:在所有的数据读取之前,执行其后面相对应的动作。注意所有的动作都要用单引号括起来。

  1. BEGIN 表示最先执行 BEGIN 条件后面的动作

    1
    2
    # 在进行真正的数据读取之前,即 awk '{printf $2 "\t" $4 "\n"}' test.txt,打印「This is a transcript.」这句话
    awk 'BEGIN{printf "This is a transcript. \n"} {printf $2 "\t" $4 "\n"}' test.txt
  2. FS 内置变量
    FS 的作用是指定分隔符的。注意:awk 命令在处理的时候是先读入第一行数据,然后再执行相应的动作。

    1
    2
    3
    4
    5
    # 输出结果中,第一行数据没有按照预期输出
    awk '{FS=":"}{print $1 "\t" $3}' /etc/passwd

    # 改进:读取第一条数据之前,先把分隔符变成「:」
    awk 'BEGIN{FS=":"}{print $1 "\t" $3}' /etc/passwd
  3. END 表示最后执行 END 条件后面的动作

    1
    2
    # 表示在输出 {printf $2 "\t" $4 "\n"} 动作中的内容之后,输出 {printf "The end. \n"} 动作中的内容
    awk 'END{printf "The end. \n"} {printf $2 "\t" $4 "\n"}' test.txt
  4. 关系运算符

    1
    2
    获取成绩大于 87 的用户名,grep -v Name 的作用是去掉标题行
    cat test.txt | grep -v Name | awk '$4 >= 87 {printf $2 "\n"}'

4、sed 命令
sed 是一种几乎包括在所有 Unix 平台(包括 Linux)的轻量级流编辑器。sed 主要是用来将数据进行选取、替换、删除、新增的命令。

sed 其实不是截取命令,其实是一个流的编辑器。相当于一个编辑器。既然已经学了 vim,为什么还有学习 sed 呢?因为 vim 命令只能修改文件,不能直接修改命令结果里的内容。要想使用 vim 修改命令的输出,则需要先把输出保存到文件当中,然后再用 vim 修改文件。sed 不光可以修改文件,因为其是一个流编辑器,所以可以从管道符接受数据来进行修改,即 sed 支持管道符操作。实际操作中主要用在对命令的结果进行操作,但是在 shell 编程中,sed 是一个重要的流数据处理编辑器。

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
39
40
41
42
43
44
45
46
47
格式:
sed [选项] '[动作]' 文件名

选项:
-n 一般 sed 命令会把所有数据都输出到屏幕,如果加入此选项,则只会把经过 sed 命令处理的行输出到屏幕
-e 允许对输入数据应用多条 sed 命令编辑
-i 用 sed 的修改结果直接修改读取数据的文件,而不是由屏幕输出,不光输出的数据会更改,源数据文件也会被更改。

动作:
a \: 追加,在当前行后添加一行或多行。添加多行时,除最后一行外,每行末尾需要用「\」代表数据未完结
c \: 行替换,用 c 后面的字符串替换原数据行,替换多行时,除最后一行外,每行末尾需要用「\」代表数据未完结
i \: 插入,在当前行前插入一行或多行。插入多行时,除最后一行外,每行末尾需要用「\」代表数据未完结
d: 删除,删除指定的行
p: 打印,输出指定的行
s: 字符串替换,用一个字符串替换另外一个字符串。格式为「行范围/s/就字符串/新字符串/g」

举例:
# 查看文件的第二行,会输出全部内容
sed '2p' test.txt

# 只显示经过 sed 命令处理的行
sed -n '2p' test.txt

# 针对管道符结果操作
df -h | sed -n '2p'

# 删除第二到第四行的数据,但不修改文件本身
sed '2,4d' test.txt

# 在第二行后追加 hello
sed '2a hello' test.txt

# 在第二行前插入两行数据
sed '2i hello \
world' test.txt

# 数据替换
sed '2c No such person' test.txt

# 在第三行中,把 90 替换成 99
sed '3s/90/99/g' test.txt

# sed 操作的数据直接写入文件
sed -i '3s/90/99/g' test.txt

# 同时把 zhangsan 和 lisi 替换为空
sed -e 's/zhangsan//g;s/lisi//g' test.txt

字符处理命令

1、排序命令 sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
格式:
sort [选项] 文件名

选项:
-f 忽略大小写
-n 以数值型进行排序,默认使用字符串型排序
-r 反向排序
-t 指定分割符,默认分隔符是制表符
-k n[,m] 按照指定的字段范围排序。从第 n 字段开始,m 字段结束(默认到行尾)

举例:
# 排序用户信息文件
sort /etc/passwd

# 反向排序
sort -t /etc/passwd

# 指定分隔符「:」。用第三字段开头,第三字段结尾排序,就是只用第三字段排序(类比数据库 order by 后面可以跟多个字段)
sort -t ":" -k 3,3 /etc/passwd

# -n 表示将提取的字段当成数字来对待
sort -n -t ":" -k 3,3 /etc/passwd

条件判断(即测试一个条件是否成立)

1、按照文件类型进行判断(记住蓝色的就行,其它的了解)
按照文件类型进行条件判断

2、两种判断格式

1
2
3
4
5
6
7
8
9
10
11
12
格式一:
# 注意该命令执行后没有任何输出,要想知道该命令的输出结果,可以使用 $? 来获取结果,「echo $?」打印结果
# $? 的作用是判断上一条命令是否正确执行,正确执行返回 0,否则返回其他数字
test -e /root/install.log

格式二:
# 注意两端必须有空格
[ -e /root/install.log ]

举例:
# 第一个判断命令如果正确执行,则打印 yes,否则打印 no
[ -d /root ] && echo "yes" || echo "no"

这种判断主要用在 Shell 脚本当中,而在脚本当中最常用的判断格式是用中括号。

3、按照文件权限进行判断
按照文件权限进行条件判断

举例:[ -w test.txt ] && echo "yes" || echo "no" 判断文件是否拥有写权限

注意:以上权限部分不区分所有者、所属组、其他人,只要该文件有这个权限,就为真。

4、两个文件之间进行比较(不常用)
两个文件之间进行比较

5、两个整数之间比较(常用)
两个整数之间比较
-ne : (not equal) 不相等
-gt : (greater than) 大于
-lt : (less than) 小于
-ge : (greater than or equal) 大于或等于
-le : (less than or equal)小于或等于

举例:[ 23 -ge 22 ] && echo "yes" || "no" 判断 23 是否大于等于 22

6、字符串的判断(常用)
字符串的判断
注意:== 会把参数当成字符串

7、多重条件判断
多重条件判断

流程控制

if 语句

1、单分支 if 条件语句

1
2
3
4
5
6
7
8
9
10
11
# 注意:中括号内两边都有空格,注意分号不能少
if [ 条件判断式 ];then
程序
fi

或者

if [ 条件判断式 ]
then
程序
fi

单分支条件语句注意点:

if 语句使用 fi 结尾,和一般语言使用大括号结尾不同。
[ 条件判断式 ] 就是使用 test 命令判断,所以中括号和条件判断式之间必须有空格。
then 后面跟符合条件之后执行的程序,可以放在 [] 之后,用「;」分割。也可以换行写入,就不需要「;」了。

举例:判断分区使用率

1
2
3
4
5
6
7
8
9
10
# 统计根分区使用率

#!/bin/bash
# 把根分区使用率作为变量赋值给 rate
rate=$(df -h | grep "/dev/sda3" | awk '{printf $5}' | cut -d '%' -f 1)

if [ $rate -ge 80 ]
then
echo "Warning! /dev/sda3 is full!"
fi

2、双分支 if 条件语句

1
2
3
4
5
6
7
if [ 条件判断式 ]
then
条件成立时,执行的程序
else
# 注意 else 后面没有 then 了
条件不成立时,执行的另一个程序
fi

示例1:备份 mysql 数据库

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
# 同步系统时间。注意:必须联网才能同步系统时间
ntpdate asia.pool.ntp.org &> /dev/null
# 把当前系统时间按照「年月日」格式赋予变量 date
date=$(date +%y%m%d)
# 统计 mysql 数据库的大小,并把大小赋予 size 变量
size=$(du -sh /var/lib/mysql)

if [-d /tmp/dbbak ]
then
echo "Date: $date!" > /tmp/dbbak/dbinfo.txt
echo "Data size: $size" >> /tmp/dbbak/dbinfo.txt
cd /tmp/dbbak
# 压缩的时候源文件可以写多个,中间用空格隔开就行
tar -zcf mysql-lib-$date.tar.gz /var/lib/mysql dbinfo.txt &> /dev/null
rm -rf /tmp/dbbak/dbinfo.txt
else
mkdir /tmp/dbbak
echo "Date: $date!" > /tmp/dbbak/dbinfo.txt
echo "Data size: $size" >> /tmp/dbbak/dbinfo.txt
cd /tmp/dbbak
tar -zcf mysql-lib-$date.tar.gz /var/lib/mysql dbinfo.txt &> /dev/null
rm -rf /tmp/dbbak/dbinfo.txt

示例2:判断 apache 是否启动

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# 使用 nmap 命令扫描服务,并截取 apache 服务的状态,赋予变量 port
# nmap 命令默认是没有装的
# -sT 表示扫描指定服务器上开启的 tcp 端口
port=$(nmap -sT 127.0.0.1 | grep tcp | grep http | awk '{printf $2}')

if [ "$port" == "open" ]
then
echo "$(date) httpd is ok!" >> /tmp/autostart-acc.log
else
/etc/rc.d/init.d/httpd start &> /dev/null
echo "$(date) restart httpd!" >> /tmp/autostart-err.log
fi

如何判断系统中某个服务是否启动?此处已 apache 服务器为例。
方式1:通过 ps aux | grep httpd 查找是否有 apache 的进程在运行。但是这种判断是有点问题的,比如有些情况下就算 apache 启动了,如果它死机了,服务是死掉的,但是进程还在却不能正常相应客户的请求。所以不是特别准确。

方式2:通过 netstat -tlun 判断端口,比如 80 端口,问题同上,不能确定你的 apache 是否能正常提供访问。它只能确定 apache 是启动的,而且不仅 apache 其他应用也可能会占用 80 端口。

方式3:最有效的办法是通过 nmap 命令,是一个远程扫描命令,是一个扫描工具,用它来扫描当前计算机,如果该工具能够正确连接 apache 的端口,它就会返回 apache 的状态是 open,证明 apache 是打开的而且是可访问的。推荐使用这个,比较准确一点。

3、多分支 if 条件语句

1
2
3
4
5
6
7
8
9
10
11
if [ 条件判断式1 ]
then
当条件判断式 1 成立时,执行程序 1
elif [ 条件判断式2 ]
then
当条件判断式 2 成立时,执行程序 2
......
else
# 注意没有 then 了
当所有条件都不成立时,最后执行此程序
fi

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 判断用户输入的是什么文件
#!/bin/bash
# 接受键盘的输入,并赋给变量 file
read -p "Please input a filename: " file
# 判断 file 是否为空
if [ -z "$file" ]
then
echo "Error,please input a filename."
exit 1
elif [ ! -e "$file" ]
then
echo "Your input is not a file."
exit 2
elif [ -f "$file" ]
then
echo "$file is a regulare file."
elif [ -d "$file" ]
then
echo "$file is a director."
else
echo "$file is an other file."
fi

注意:对于 if else 中的报错程序,如果报错了一定要退出程序,否则程序还会继续向下执行。

case 语句

多分支 case 条件语句:case 语句和 if…elif…else 语句一样都是多分支条件语句,不过和 if 多分支条件语句不通的是,case 语句只能判断一种条件关系,而 if 语句可以判断多种条件关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case $变量名 in
"值1")
如果变量的值等于值 1,则执行程序 1
# 表示程序段的结束,不能省略
;;
"值2")
如果变量的值等于值 2,则执行程序 2
# 表示程序段的结束,不能省略
;;
... 省略其他分支 ...
# 注意 * 号是没有引号的,其他的是有引号的
*)
如果变量的值都不是以上的值,则执行此程序
;;
# 注意最后的 esac
esac

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
read -p "Please choose yes/no: " -t 30 cho
case $cho in
"yes")
echo "Your choose is yes."
;;
"no")
echo "Your choose is no."
;;
*)
echo "Your choose is error."
;;
esac

for 循环

1、语法一

1
2
3
4
5
# 值之间的分隔符为空格
for 变量 in 值1 值2 值3 ...
do
程序
done

注意:后面有几个值,for 循环执行几次,每次执行都会把值赋给变量。注意:in 后面的值不管是空格还是换行,只要是分开的,系统就把它当场一个内容即一个值。

举例:

1
2
3
4
5
#!/bin/bash
for time in morning noon afternoon evening
do
echo "This time is $time."
done

这种循环很笨,但是还存在的理由:就是更加利于系统管理,因为 in 后面的值可以从内容中获取,即循环的次数可以不确定,比如下面的例子:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# 批量解压缩脚本
cd /test
ls *.tar.gz > ls.log
for i in $(cat ls.log)
do
tar -zxf $i &> /dev/null
done
rm -rf /test/ls.log

2、语法二

1
2
3
4
for ((初始值;循环控制条件;变量变化))
do
程序
done

在 Linux 中,只有用双小括号括起来,才可以进行加减乘除这种数值运算。
两种循环的区别:第一种循环不知道循环次数,第二种循环知道循环次数。

示例1:

1
2
3
4
5
6
7
8
#!/bin/bash
# 从 1 加到 100
s = 0
for (( i=1;i<=100;i=i+1 ))
do
s=$(( $s+$i ))
done
echo "The sum result is: + $s"

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# 批量添加指定数量的用户
read -p "Please input username: " -t 30 name
read -p "Please input the number of users: " -t 30 number
read -p "Please input the password of users: " -t 30 password

if [ ! -z "$name" -a ! -z "$number" -a ! -z "$password" ]
then
# 判断 number 是否合法,即是否为纯数字
# 把以任意数字开头且以任意数字结尾的替换为空,即将纯数字替换为空
y = $(echo $number | sed 's/^[0-9]*$//g')
if [-z "$y"]
then
for (( i=1;i<=$number;i=i+1 ))
do
/usr/sbin/useradd $name$i $>/dev/null
echo $password | /usr/bin/passwd --stdin $name$i &>/dev/null
done
fi
fi

while 循环

1、while 循环是不定循环,也称作条件循环。只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止。这就和 for 的固定循环不太一样了。注意:在 while 的执行语句中需要对其条件判断式不断改变,否则要是永远成立,则死循环了。

1
2
3
4
while [ 条件判断式 ]
do
程序
done

举例:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# 从 1 加到 100
i=1
s=0
while [ $i -le 100 ]
do
s=$(( $s+$i ))
i=$(( $i+1 ))
done
echo "The sum is: $s"

2、until 循环,和 while 循环相反,until 循环时只要条件判断式不成立则进行循环,并执行循环程序。一旦循环条件成立,则终止循环。

1
2
3
4
until [ 条件判断式 ]
do
程序
done

举例:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# 从 1 加到 100
i=1
s=0
until [ $i -gt 100 ]
do
s=$(( $s+$i ))
i=$(( $i+1 ))
done
echo "The sum is: $s"

注意:Shell 是脚本语言,脚本语言的好处就是所见即所得,即写完的所有脚本语言都不需要编译就可以直接运行,但是它不是不需要编译,而是在执行的同时进行编译,省略简化了编译过程,好处是编程更加简单,坏处是效率要更慢。Shell 语言不适合进行大量的数据运算,其最大的作用是帮助管理员减少重复操作。

-------------本文结束感谢您的阅读-------------

本文标题:Linux 入门总结(十二)

文章作者:Yan ChongSheng

发布时间:2018年10月26日

最后更新:2018年11月02日

原始链接:yanchongsheng.github.io/2018/10/26/Linux-2018-10-26-Linux入门总结-十二/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

开启打赏模式