1. 什么是 shell?
shell 是一个程序,采用 c 语言进行编写的,是用户和 linux 内核沟通的桥梁。它既是一种命令语言,又是一种解释性的编程语言。
- kernel:为软件服务,接受用户或者软件指令驱动硬件,完成工作
- shell:命令解释器
- user:用户接口,对接用户
shell 脚本就是将完成一个任务的所有命令按照执行的先后顺序,自上而下写入到一个文本文件中,然后给予执行的权限。
2. shell 基础用法
2.1 如何书写一个 shell 脚本
-
shell 脚本命名
命名要做到见明知意,比如:check_memory.sh,检查内存的脚本
虽然 linux 系统中,文件没有扩展名的概念,但是建议使用 .sh 结尾
-
shell 脚本格式
shell 脚本开头必须指定脚本运行环境以 #! (特例)符合组合来组成。比如:#!/bin/bash 指定该脚本的运行解析是由 /bin/bash 来完成
-
脚本组成
#1. 解释环境
#!/usr/bin/env bash | python | perl
#2. 注释说明
# Author: name
# CreateTime: 2023-04-22 17:07
# Description: 描述信息
#3. 执行代码
2.2 如何运行 shell 脚本
- 给脚本文件赋予执行权限后该脚本即可运行
chmod u+x filename
- 如果不赋予执行权限,可以使用 bash 命令来运行未赋予执行权限的脚本
bash filename
2.3 shell 中的特殊符号
~: 家目录 cd~代表进入用户家目录
!: 执行历史命令 !! 执行上一条命令
$: 变量中取内容符
+ - * / %: 对应数学运算 加 减 乘 除 取余
&: 后台执行
*: shell 中的通配符,匹配所有
?: shell 中的通配符,匹配除回车以外的一个字符
;: 分号可以在shell 中一行执行多个命令,命令之间用分号分隔
|: 管道符,上一个命令的输出作为下一个命令的输入 cat filename | grep "abc"
: 转义字符
``: 反引号,命令中执行命令 echo "today is `date + %F`"
'': 单引号,脚本中字符串要用单引号引起来,但是不同于双引号的是,单引号不解释变量
"": 双引号,脚本中出现的字符串可以用双引号引起来
2.4 shell 重定向
> 重定向输入 覆盖原有数据
>> 重定向追加输入,在原数据的末尾添加
< 重定向输出 wc -l < /etc/passwd
<< 重定向追加输入 fdisk /dev/sdb <<EOF ... EOF
2.5 数学运算
echo $? 判断上一条命令是否执行成功
- 整数运算 expr 或者使用 let
- 小数运算 bc
- 双小圆括号运算,在 shell中 (( )) 也可以用来做数学运算
2.6 退出脚本
exit NUM 退出脚本,释放系统资源,NUM 代表一个整数,代表返回值
脚本内容如下:exit 9,值的范围(1-255)在我们执行脚本后使用 echo $? 可以看到这个返回值
3. shell 格式化输出
功能:将内容输出到默认显示设备
echo 命令的功能是在显示器上显示一段文字,一般起到一个提示的作用。
语法:echo [ -ne ] [ 字符串 ]
命令选项:
- -n 不要再最后自动换行
- -e 若字符串中出现以下字符,则特别加以处理,而不会将它当成一般文字输出
转义字符:
- \a 发出警报声
- \b 删除前一个字符
- \c 最后不加上换行符号
- \f 换行但光标仍旧停留在原来的位置
- \n 换行且光标移至行首
- \r 光标移至行首,但不换行
- \t 插入 tab
- \v 与 \f 相同
- \ 插入 \ 字符
- \nnn 插入 nnn(八进制)所表示的 ASCII 字符
- -help 显示帮助
颜色代码输出
脚本中 echo 显示内容带颜色显示,echo 显示带颜色需要使用参数 -e
格式:echo -e "\033[字背景颜色;文字颜色m字符串\033[0m"
例如:echo -e "\033[41;33mHello,World\033[0m"
41:红底
33:黄字
- 字背景颜色和文字颜色之间是应为的 ";"
- 文字颜色后面有个m
- 字符串前后可以没有空格,如果有的话输出也同样有空格
4. shell 交互
-
read 命令
默认接受键盘的输入,回车符代表输入结束
-p 打印信息
-t+时间 限定时间
-s 不回显
-n 输入字符个数
#!/bin/bash
clear
echo -n -e "Login:"
read username # 将输入内容写入一个变量中
echo -n -e "Password:"
read password
echo "you input username is $username, password is $password." #从上面变量的内容进行取出并显示
5. 变量
- 内存占用,如果存的是一个字符则占用1 个字节,如果存的是字符串则是字符串的长度加 1 个字节长度(\0 是一个特殊字符,代表字符串结束)
- 变量名与内存控件关系:计算机中会将对应的内存控件和变量名称绑定在一起,此时代表这段内存空间已经被程序占用,其他程序不可复用;然后将变量名对应的值存在对应内存地址的空间里。
5.1 变量分类
- 本地变量:用户私有变量,只有本用户可以使用,保存在家目录下的 .bash_profile、.bashrc 文件中
- 全局变量:所有用户都可以使用,保存在 /etc/profile、/etc/bashrc 文件中
- 用户自定义变量:用户自定义,比如脚本中的变量
5.2 定义变量
变量格式:变量名=值
在 shell 编程中的变量名和等号之间不能有空格。
-
变量命名规则:
命名只能使用英文字母,数字和下划线,首个字符不能以数字开头
中间不能有空格,可以使用下划线(_)
不能使用标点符号
不能使用 bash 里的关键字(可用 help 命令查看保留关键字)
读取变量内容:$变量名
#!/bin/bash
NAME="Li Ming"
AGE="18"
SCOPE="100"
echo "name:$NAME,age:$AGE,scope:$scope"
5.3 取消变量
格式:unset 变量名
首先在本地变量中定义变量 NAME,值为 变量定义
5.4 全局变量
格式:export 变量名=值
上述设置的变量其实都是一次性变量,系统重启就会丢失。
如果希望本地变量或者全局变量可以永久使用,可以将需要设置的变量写入变量文件中即可。
5.5 永久变量
本地变量:用户私有变量,只有本用户可以使用,保存在家目录下的 .bash_profile、.bashrc 文件中
全局变量:所有用户都可以使用,保存在 /etc/profile、/etc/bashrc 文件中
本地变量
[root@bogon ~]# tail -1 ~/.bash_profile
NAME="变量定义"
全局变量
[root@bogon ~]# tail -1 /etc/profile
export PATH
6. 数组
6.1 基本数组
数组可以让用户一次赋予多个值,需要读取数据时只需通过索引调用就可以方便读出了。
6.1.1 语法
数组名称=(元素1 元素2 元素3 ...)
例如:ARRAY1=('A' 'B' 'C' 'D')
6.1.2 数组读出
${数组名称[索引]}
索引默认是元素在数组中的排队编号,默认第一个从 0 开始
echo ${ARRAY1[1]}
6.1.3 赋值
# 一次赋多个值
ARRAY1=('A' 'B' 'C' 'D')
echo ${ARRAY1[1]}
# 一次赋一个值
ARRAY1[4]='ZHANGSAN'
ARRAY1[5]='LISI'
echo ${ARRAY1[4]}
ARRAY2=(`cat /etc/passwd`) # 将文件中的每一行作为一个元素赋值给数组 ARRAY2
6.1.4 查看数组
查看系统声明过的数组
declare -a
6.1.5 访问数组元素
echo ${array1[0]} # 访问数组中的第一个元素
echo ${array1[@]} # 访问数组中的所有元素,等同于 echo ${array1[*]}
echo ${#array1[@]} # 统计数组元素的个数
echo ${!array1[@]} # 获取数组元素的索引
echo ${array1[@]:1} # 从数组下标 1 开始
echo ${array1[@]:1:2} # 从数组下标1 开始,访问两个元素
#!/bin/bash
array1=('A' 'B' 'C' 'D')
echo "访问数组中的第一个元素:${array1[0]}"
echo "访问数组中的所有元素,等同于$ {array1[*]}:${array1[@]}"
echo "统计数组元素个数:${#array1[@]}"
echo "获取数组元素的索引:${!array1[@]}"
echo "从数组下标1开始:${array1[@]:1}"
echo "从数组下标1开始,访问两个元素:${array1[@]:1:2}"
6.2 关联数组
关联数组可以允许用户自定义数组的索引,这样使用起来更加方便、高效。
6.2.1 声明关联数组
declare -A ass_array1
declare -A ass_array2
6.2.2 关联数组赋值
#!/bin/bash
# 声明关联数组
declare -A ass_array1
# 赋值方式1,一次赋一个值
ass_array1[name]="zhangsan"
ass_array1[age]=18
echo "name:${ass_array1[name]}, age:${ass_array1[age]}"
# 声明关联数组
declare -A ass_array2
# 赋值方式2,一次赋多个值
ass_array2=([name]="wangwu" [age]=20)
echo "name:${ass_array2[name]}, age: ${ass_array2[age]}"
其他操作与普通数组操作一样
7. 流程控制语句
7.1 五大运算
7.1.1 数组比较运算
-eq 等于
-gt 大于
-lt 小于
-ge 大于或等于
-le 小于或等于
-ne 不等于
7.1.2 文件类型比较运算
-d 检查文件是否存在且为目录
-e 检查文件是否存在
-f 检查文件是否存在且为文件
-r 检查文件是否存在且可读
-s 检查文件是否存在且不为空
-w 检查文件是否存在且可写
-x 检查文件是否存在且可执行
-O 检查文件是否存在并且被当前用户拥有
-G 检查文件是否存在并且默认组为当前用户组
file1 -nt file2 检查file1是否比file2新
file1 -ot file2 检查file1是否比file2旧
7.1.3 字符串比较运算
== 等于
!= 不等于
-n 检查字符串的长度是否大于0
-z 检查字符串长度是否为0
7.1.4 逻辑运算
&& 逻辑与运算
|| 逻辑或运算
! 逻辑非运算
7.1.5 赋值运算
= 赋值运算符 name="zhangsan"
7.2 if
7.2.1 单 if 语句
只需要一步判断,条件返回真干什么或者条件返回假干什么
if [ condition ] # condition 值为 true 或者为 false
then
commands
fi
# 判断在 /home 有没有 abc.sh ,如果没有则创建该文件并输出创建成功相关性信息
#!/bin/bash
if [ ! -f '/home/abc.sh' ]
then
echo "没有该文件"
touch '/home/abc.sh'
echo "创建 abc.sh 文件成功"
fi
7.2.2 if-then-else 语句
两步判断,条件为真干什么,条件为假什么
if [ condition ]
then
commands1
else
commands2
fi
#!/bin/bash
if [ -f /home/abc.sh ]
then
echo "该文件已经存在!"
else
echo "该文件不存在!!!"
fi
#!/bin/bash
echo -n -e "Login:"
read usr
if [ "root" == $usr ]
then
echo "管理员用户登录"
else
echo "普通用户登录"
fi
7.2.3 if-then-elif 语句
多于两个以上的判断结果
if [ condition 1 ]
then
commands1
elif [ command 2 ]
then
fi
#!/bin/bash
echo -n -e "请输入一个数字:"
read NUM
if [ $NUM -eq 10 ]
then
echo "您输入的数字等于10."
elif [ $NUM -gt 10 ]
then
echo "您输入的数字大于10."
else
echo "您输入的数字小于10."
fi
#!/bin/bash
if [ $1 -eq $2 ]
then
echo "$1 == $2"
else
if [ $1 -gt $2 ]
then
echo "$1 > $2"
else
echo "$1 < $2"
fi
fi
# 解释
# $1 $2 是指使用命令进行传参
# 执行该脚本方式 bash 脚本名.sh 参数1 参数2 ...
7.1.4 高阶用法
- if 判断中使用双 (()) 可以植入数学运算s
#!/bin/bash
if (( 100%3 > 10 ));
then
echo "yes"
else
echo "no"
fi
- 使用双方括号可以在条件中使用通配符
#!/bin/bash
if [[ "aa" == aa* ]]
then
echo "yes"
else
echo "no"
fi
7.3 for
7.3.1 语法一
for var in value1 value2 value3 ...
do
commands
done
#!/bin/bash
for var in value1 value2 value3 value4
do
echo "输出:$var"
done
#!/bin/bash
for i in `seq 1 9`
do
echo "输出:$i"
done
#!/bin/bash
for i in `seq 9 -1 1`
do
echo "输出:$i"
done
7.3.2 语法二
for ((变量;条件;自增减运算))
do
commands
done
#!/bin/bash
for ((i=1;i<10;i++))
do
echo "输出:$i"
done
7.3.3 无限循环
for ((;;))
do
echo "hello"
done
7.4 条件控制
7.4.1 sleep N
脚本执行到该步休眠 N 秒
#!/bin/bash
for((;;))
do
ping -c1 $1 &>/home/null
if [ $? -eq 0 ]
then
echo "`date +"%F %H:%M:%S"`: $1 is UP"
else
echo "`date +"%F %H:%M:%S"`: $1 is DOWN"
fi
# 脚本节奏控制,每隔5s执行一次
sleep 5
done
7.4.2 continue
跳过循环中的某次循环
#!/bin/bash
for (( i=1;i<=10;i++ ))
do
if [ $i -eq 5 ]
then
continue
fi
echo "输出当前值为:$i"
done
# 输出结果
输出当前值为:1
输出当前值为:2
输出当前值为:3
输出当前值为:4
输出当前值为:6
输出当前值为:7
输出当前值为:8
输出当前值为:9
输出当前值为:10
7.4.3 break
跳出循环继续执行后面的代码
#!/bin/bash
for ((i=1;i<10;i++))
do
echo "当前值为:$i"
if [ $i -eq 4 ]
then
break
fi
done
echo "循环跳出,最后执行的 i 值为:$i"
7.5 while
7.5.1 语法
while [ condition ] # 条件为真while开始循环,条件为假,while停止循环
do
commands
done
#!/bin/bash
read -p "请输入:" num1
while [ $num1 -ge 10 ]
do
echo "当前值:$num1"
sleep 3
done
#!/bin/bash
read -p "Login: " username
while [ $username != 'root' ]
do
read -p "Login: " username
done
7.5.2 练习
- 乘法口诀表
#!/bin/bash
n=1
while [ $n -le 9 ]
do
for (( m=1;m<=$n;m++ ))
do
echo -n -e "$m*$n=$(($n*$m)) "
done
echo ""
n=$((n+1))
done
- 遍历文件内容
#!/bin/bash
while read i
do
echo "$i"
done < $1
7.6 until
与 while正好相反,until 是条件为假开始执行,条件为真停止执行
语法:
until [ condition ]
do
commands
done
例子:
#!/bin/bash
i=0
until [ $i -gt 10 ]
do
echo "当前值为:$i"
i=$((i+1))
done
7.7 case语句
多条件分支语句
7.7.1 语法
每个代码块执行完毕要以 ;; 结尾代表结束,case 结尾要以 esac 结束
case 变量 in
条件1)
执行代码块1
;;
条件2)
执行代码块2
;;
......
esac
#!/bin/bash
read -p "NUM:" NUM
case $NUM in
10)
echo "您输入的值大于10";;
5)
echo "您输入的值大于5";;
0)
echo "您输入的值大于0";;
esac
7.7.2 shell特殊变量
$0 当前脚本的名字
$* 代表所有参数,其间隔为 IFS 内定参数的第一个字元
$@ 与 * 号类同,不同之处在于不参照 IFS
$# 代表参数数量
$ 执行上一个指令的返回值
$- 最近执行的 foreground pipeline 的选项参数
$$ 本身的ProcessID
$ 执行上一个指定的 PID
$_ 显示出最后一个执行的命令
$N shell的第几个外传参数
#!/bin/bash
echo "脚本名字:$0"
echo "所有参数:$*"
echo "参数数量:$#"
echo "上一个指令返回值:$"
echo "本身的进程id:$$"
echo "执行上一个指令的PID:$"
echo "显示出最后一个执行的命令:$_"
# 输出
(base) [root@bogon shell]# bash special_arg.sh 11 22 33 44
脚本名字:special_arg.sh
所有参数:11 22 33 44
参数数量:4
上一个指令返回值:$
本身的进程id:24082
执行上一个指令的PID:$
显示出最后一个执行的命令:执行上一个指令的PID:$
8. 函数
使用函数的优点:
- 代码模块化、调用方便、节省内存
- 代码模块少、代码量少、排错简单
- 代码模块可以改变代码的执行顺序
# 语法一:
函数名(){
代码块
return N
}
# 语法二:
function 函数名 {
代码块
return N
}
#!/bin/bash
start () {
echo "Nginx server start ...... [OK]"
}
stop () {
echo "Nginx server stop ...... [OK]"
}
case $1 in
start) start;;
stop) stop;;
*)echo "You Should input start|stop"
esac
9. 正则表达式
正则表达式是一种文本模式匹配,包括普通字符(例如:a到z之间的字母)和特殊字符(称为元字符),它是一种字符串匹配的模式,可以用来检查一个字符串是否含有某种子串、将匹配的子串替换或者从某个字符串中取出某个条件的子串。
正则表达式就像数学公式一样,我们可以通过正则表达式提供的一些特殊字符来生成一个匹配对应字符串的公式,用此来从海量数据中匹配出自己想要的数据。
正则表达式是一个三方产品,被常用计算机语言广泛使用,比如:shell、PHP、python、java、js等
shell也支持正则表达式,但不是所有的命令都支持正则表达式,常见的 命令中只有grep、sed、awk命令支持正则表达式。
9.1 特殊符号
定位符:同时锚定开头和结尾,做精确匹配;单一锚定开头和结尾,做模糊匹配。
| 定位符 | 说明 |
|---|---|
| ^ | 锚定开头 ^a 以a开头 默认锚定一个字符 |
| $ | 锚定结尾 a$ 以a结尾 默认锚定一个字符 |
# 测试文本
a
abc
abbc
abcd
acb
accc
ac
a1c
a3c
ba
bac
bc
9.2 匹配字符串
| 匹配符 | 说明 | |
|---|---|---|
| . | 匹配除回车以外的任意字符 | |
| () | 字符串分组 | |
| [] | 定义字符类,匹配括号中的一个字符 | |
| [^] | 表示否定括号中出现字符类中的字符,取反 | |
| \ | 转义字符 | |
| 或 |
9.3 限定符
对前面的字符或字符串做限定说明
| 限定符 | 说明 |
|---|---|
| * | 某个字符之后加星号表示该字符不出现或出现多次 |
| ? | 与星号相似,但略有变化,表示该字符出现一次或不出现 |
| + | 与星号相似,表示其前面字符出现一次或多次,但必须出现一次 |
| {n,m} | 某个字符之后出现,表示该字符最少 n 次,最多 m 次 |
| {m} | 正好出现了 m 次 |
9.4 POSIX 特殊字符
| 特殊字符 | 说明 |
|---|---|
| [:alnum:] | 匹配任意字母字符 0-9 a-z A-Z |
| [:alpha:] | 匹配任意字母,大写或小写 |
| [:digit:] | 数字 0-9 |
| [:graph:] | 非空字符(非空格控制字符) |
| [:lower:] | 小写字符 a-z |
| [:upper:] | 大写字符 A-Z |
| [:cntrl:] | 控制字符 |
| [:print:] | 非空字符(包括空格) |
| [:punct:] | 标点符号 |
| [:blank:] | 空格和 TAB 字符 |
| [:xdigit:] | 16 进制数字 |
| [:space:] | 所有空白字符(新行、空格、制表符) |
10. shell 对文件操作
在 shell 脚本编写中,时常会用到对文件的相关操作,比如增加内容,修改内容,删除部分内容,查看部分内容等,但是上述举例的这些操作一般都是需要在文本编辑器中才能操作,常用的文本编辑器如:gedit、vim、nano 等又是交互式文本编辑器,脚本无法自己独立完成,必须有人参与才可以完成。如果这样的话又违背了我们编写脚本的意愿(全部由机器来完成,减少人的工作压力,提升工作效率)。如果才能让这些操作全部脚本自己搞定,而不需要人的参与,而且又能按照我们的脚本预案来完成呢?
为了解决上述问题,linux 提供了一些命令,比如 Perl、sed 等命令。
10.1 sed
sed 是linux中提供的一个外部命令,它是一个行(流)编辑器,非交互式的对文件内容进行增删改查的操作,使用者只能在命令行输入编辑命令、指定文件名,然后在屏幕上查看输出。它和文本编辑器有本质的区别。
区别:
- 文本编辑器:编辑对象是文件
- 行编辑器:编辑对象是文件中的行
也就是前者一次处理一个文本,而后者一次处理一个文本中的一行
10.1.1 语法
语法:sed [options] '{command}[flags]' [filename]
命令选项:
-e script 将脚本中指定的命令添加到处理输入时执行的命令中 多条件,一行中要有多个操作
-f script 将文件中指定的命令添加到处理输入时执行的命令中
-n 抑制自动输出
-i 编辑文件内容
-i.bak 修改时同时创建.bak 备份文件
-r 使用扩展的正则表达式
! 取反(跟在模式条件后与shell有所区别)
sed 常用内部命令:
a 在匹配后面添加
i 在匹配前面添加
p 打印
d 删除
s 查找替换
c 更改
y 转换 N D P
flags:
数字 表示新文本替换的模式
g: 表示用新文本替换现有文本的全部实例
p: 表示打印原始的内容
w filename: 将替换的结果写入文件
10.1.2 使用
测试文本:
1. There is a cat at my doorstep
2. There is a cat at my doorstep
3. There is a cat at my doorstep
4. There is a cat at my doorstep
5. There is a cat at my doorstep
sed '2,4a\Hello,World' sed_test.txt # 在 2-4行下面添加字符串 Hello,World
匹配模式
sed '/3. There/a\Hello,World' sed_test.txt # / / 表示开启匹配模式,这里匹配 3. There ,匹配成功添加字符串
其余用法遇上类型,只需要把 a 换成如 d删除 i插入这种即可
使用正则表达式来删除一个文件中以 # 开头的或者包含 # 号的或者空行
sed '/(^#|#|^$)/d' 文件名
替换
sed 's/cat/dog/' sed_test.txt
flags 部分:
抑制输出
多条件操作
将上面的内容写入到一个文本文件中,然后通过 -f 去指定使用这个文本文件
修改原文件,修改不可逆
备份源文件
通过管道修改
统计文件行号
sed -n '$=' sed_test.txt
sed '=' sed_test.txt # 输出显示内容,并对每行添加行号
使用正则表达式
sed -n -r '/^(root)(.*)(bash)$/p' /etc/passwd
10.2 awk
在日常计算机管理中,总会有很多数据输出到屏幕或者文件,这些输出包含了标准输出、标准错误输出。默认情况下,这些信息全部输出到默认输出设备--屏幕。然而,大量的数据输出中,只有一部分是我们需要重点关注的,我们需要把我们需要的或者关注的这些信息过滤或者提取已备后续需要时调用。
10.2.1 介绍
awk 是一种可以处理数据、产生格式化报表的语言,功能十分强大。awk 认为文件中的每一行是一条记录,记录与记录的分隔符为换行符,每一列是一个字段,字段与字段的分隔符默认是一个或多个空格或 tab 制表符。
awk 的工作方式是读取数据,将每一行数据视为一条记录(record)每条记录以字段分隔符分成若干字段,然后输出各个字段的值。
# 语法
awk [options][BEGIN]{program}[END][file]
options:
-F fs 指定描绘一行中数据字段的文件分隔符,默认为空格
-f file 指定读取程序的文件名
-v var=value 定义 awk 程序中使用的变量和默认值
awk 程序脚本由左大括号和右大括号定义。脚本命令必须放置在两个大括号之间。
awk 程序运行优先级是:
1. BEGIN: 在开始处理数据流之前执行,可选项
2. program: 如何处理数据流,必选项
3. END: 处理完数据流后执行,可选项
10.2.2 基本用法
文本测试内容
1. There is a dog at my doorstep dog
2. There is a dog at my doorstep dog
3. There is a dog at my doorstep dog
4. There is a dog at my doorstep dog
5. There is a dog at my doorstep dog
awk 对字段(列)的提取:
字段提取:提取一个文本中的一列数据并打印输出
字段相关内置变量
- $0 表示整行文本
- $1 表示文本行中的第一个数据字段
- $2 表示文本行中的第二个数据字段
- $N 表示文本行中的第 N 个数据字段
- $NF 表示文本行中的最后一个数据字段
# 打印文本全部内容
awk '{print $0}' awk_test.txt
# 打印最后一列
awk '{print $NF}' awk_test.txt
# 打印第三列
awk '{print $3}' awk_test.txt
awk 对记录(行)的提取:
记录提取:提取一个文本中的一行并打印输出
记录的提取方法有两种:通过行号、通过正则表达式
NR:指定行号
# 提取第三行
awk 'NR==3{print $0}' awk_test.txt
# 提取第三行第三列
awk 'NR==3{print $3}' awk_test.txt
指定分隔符打印:
# 文件中以 : 分割列,打印第一行第一列和第二列的内容
awk -F ":" 'NR==1{print $1,$2}' /etc/passwd
输出:root x
# 输出内容使用 - 进行间隔
awk -F ":" 'NR==1{print $1 "-" $2}' /etc/passwd
输出:root-x
10.2.3 awk 程序的优先级
BEGIN 是优先级最高的代码块,是在执行 PROGRAM 之前执行的,不需要提供数据源,因为不涉及到任何数据的处理,也不依赖与 PROGRAM 代码块;PROGRAM 是对数据流干什么,是必选代码块,也是默认代码块。所以在执行时必须提供数据源;END 是处理完数据流后的操作,如果需要执行 END 代码块,就必须需要 PROGRAM 的支持,单个无法执行。
awk 'BEGIN{print "begin"}{print $0}END{print "end"}' awk_test.txt
BEGIN: 处理数据流之前做什么
END: 处理数据流之后做什么
# 输出
begin
1. There is a dog at my doorstep dog
2. There is a dog at my doorstep dog
3. There is a dog at my doorstep dog
4. There is a dog at my doorstep dog
5. There is a dog at my doorstep dog
end
10.2.4 awk 高级用法
awk 是一门语言,符合语言的特性,除了可以定义变量外还可以定义数组,还可以进行运算,流程控制
定义数组:
数组定义方式:数组名[索引]=值
awk 'BEGIN{arr[0]=100;arr[2]=200;print arr[0];print arr[2]}'
# 输出
100
200
# 通过 awk 计算内存使用率
head -2 /proc/meminfo | awk 'NR==1{t=$2}NR==2{f=$2;print (t-f)*100/t "%"}'
# 输出 21.8826%
awk 'BEGIN{name="zhangsan";print name}'
# 输出 zhangsan
awk 运算:
- 赋值运算 =
- 比较运算 > >= == < <= !=
- 数学运算 + - * / % ** ++ --
- 逻辑运算 && ||
- 匹配运算
!
赋值运算:主要对变量或者数组赋值
比较运算:如果比较的字符串则按照 ascii 编码顺序表比较,如果结果返回为真用 1 表示,如果返回为假则用 0 表示
[root@localhost shell]# awk 'BEGIN{ print "a" >= "b" }'
0
[root@localhost shell]# awk 'BEGIN{ print "a" <= "b" }'
1
[root@localhost shell]# seq 1 10 > num
[root@localhost shell]# cat num
1
2
3
4
5
6
7
8
9
10
[root@localhost shell]# awk '$1 > 5{print $0}' num
6
7
8
9
10
[root@localhost shell]# awk '$1 == 5{print $0}' num
5
[root@localhost shell]# awk 'BEGIN{print 1 + 1}'
2
[root@localhost shell]# awk 'BEGIN{print 1 * 2}'
2
[root@localhost shell]# awk 'BEGIN{print 1 - 2}'
-1
[root@localhost shell]# awk 'BEGIN{print 1 / 2}'
0.5
逻辑运算
[root@localhost shell]# awk 'BEGIN{print 1 >= 2 && 1 <= 2}'
0
[root@localhost shell]# awk 'BEGIN{print 3 >= 2 && 1 <= 2}'
1
[root@localhost shell]# awk 'BEGIN{print 1 >= 2 || 1 <= 2}'
1
[root@localhost shell]# awk 'BEGIN{print 0 >= 2 || 1 <= 2}'
1
匹配运算
# 精确匹配
[root@localhost shell]# awk -F ":" '$1 == "root"{print $0}' /etc/passwd
# 输出root:x:0:0:root:/root:/bin/bash
# 模糊匹配
[root@localhost shell]# awk -F ":" '$1 ~ "root"{print $0}' /etc/passwd
# 输出 root:x:0:0:root:/root:/bin/bash
# 模糊匹配
[root@localhost shell]# awk -F ":" '$1 ~ "ro"{print $0}' /etc/passwd
# 输出
root:x:0:0:root:/root:/bin/bash
setroubleshoot:x:993:990::/var/lib/setroubleshoot:/sbin/nologin
chrony:x:992:987::/var/lib/chrony:/sbin/nologin
oprofile:x:16:16:Special user account to be used by OProfile:/var/lib/oprofile:/sbin/nologin
10.2.5 awk 环境变量
| 变量 | 描述 |
|---|---|
| FIELDWIDTHS | 以空格分隔的数字列表,用空格定义每个数据字段的精确宽度 |
| FS | 输入字段分隔符号 |
| OFS | 输出字符分隔符号 |
| RS | 输入记录分隔符 |
| ORS | 输出记录分隔符号 |
[root@localhost shell]# awk 'BEGIN{FIELDWIDTHS="5 2 8"}NR==1{print $1,$2,$3}' /etc/passwd
#输出:root: x: 0:0:root
[root@localhost shell]# awk 'BEGIN{FS=":"}NR==1{print $1,$2,$3}' /etc/passwd
root x 0
[root@localhost shell]# awk 'BEGIN{FS=":";OFS="--"}NR==1{print $1,$2,$3}' /etc/passwd
root--x--0
[root@localhost shell]# seq 1 10 > num
[root@localhost shell]# cat num
1
2
3
4
5
6
7
8
9
10
[root@localhost shell]# awk 'BEGIN{RS=""}NR==1{print $1,$2,$3}' num
1 2 3
[root@localhost shell]# awk 'BEGIN{RS="";OFS="--"}NR==1{print $1,$2,$3}' num
1--2--3
[root@localhost shell]# awk 'BEGIN{RS="";ORS="######"}NR==1{print $1,$2,$3}' num
1 2 3######(base) [root@localhost shell]#
10.2.6 流程控制
[root@localhost shell]# cat num
1
2
3
4
5
6
7
8
9
10
[root@localhost shell]# awk '{if($1>5) print $0}' num
6
7
8
9
10
[root@localhost shell]# awk '{if($1 % 2 != 0) print $1" jishu" ;else print $1" ou"}' num
1 jishu
2 ou
3 jishu
4 ou
5 jishu
6 ou
7 jishu
8 ou
9 jishu
10 ou
# -v 指定变量和默认值
[root@localhost shell]# awk -v sum=0 '{sum+=$1}END{print sum}' num
55
[root@localhost shell]# cat num2
1 2 3
4 5 6
7 8 9
# 每行累加并输出 for
[root@localhost shell]# awk '{sum=0;for(i=1;i<4;i++)sum+=$i;print sum}' num2
6
15
24
# 第一行累加并输出
[root@localhost shell]# awk 'NR==1{sum=0;for(i=1;i<4;i++)sum+=$i;print sum}' num2
6
# while do
[root@localhost shell]# awk '{sum=0;i=1;while(i<4){sum+=$i;i++}print sum}' num2
6
15
24
# do while
[root@localhost shell]# awk '{sum=0;i=1;do{sum+=$i;i++}while(i<4);print sum}' num2
6
15
24
常用:
# 统计行数
[root@localhost shell]# awk 'END{print NR}' awk_test.txt
5
# 打印最后一行
[root@localhost shell]# awk 'END{print $0}' awk_test.txt
5. There is a dog at my doorstep dog
# 打印列数
[root@localhost shell]# awk 'END{print NF}' awk_test.txt
9
# 求出每一列的和
[root@localhost shell]# awk -v 'sum=0' '{sum+=$1}END{print sum}' num2
12
[root@localhost shell]# awk -v 'sum=0' '{sum+=$2}END{print sum}' num2
15
[root@localhost shell]# awk -v 'sum=0' '{sum+=$3}END{print sum}' num2
18
11. 常用shell脚本案例
11.1 检测一个主机状态
#!/bin/bash
# 报警阈值 3次全部失败,报警机器 down
# ping 频率设置为 1秒一次
# ping -c1 表示发送一个包,ping 使用 ICMP 协议
for((i=1;i<4;i++))
do
if ping -c1 $1 &> /dev/null
then
# 定义为全局变量
export ping_count"$i"=1
else
export ping_count"$i"=0
fi
# ping 频率
sleep 1
done
# 判断是否报警
if [ $ping_count1 -eq $ping_count2 ] && [ $ping_count2 -eq $ping_count3 ] && [ $ping_count1 -eq 1 ]
then
echo "$1 is Up"
else
echo "$1 is Down"
fi
# 撤销变量
unset ping_count1
unset ping_count2
unset ping_count3
11.2 监控一个端口状态
等价于监控系统服务
# 监控方法
# 1) 通过 systemctl 查看服务状态
# 2) 通过 lsof 查看端口是否存在
# 3) 通过查看进程是否存在 ps
####### 以上3中方法在压力过大的时候无法相应,有的时候可能出现假死状态,比如服务down掉了,但是上述的进程状态仍然可以检测到
# 4) 测试端口是否有响应,使用 telnet 协议
# 使用 telnet
#[root@localhost shell]# telnet 127.0.0.1 22
#Trying 127.0.0.1...
#Connected to 127.0.0.1.
#Escape character is '^]'. ^] 这个符号表示端口响应成功
#SSH-2.0-OpenSSH_7.4
#quit;
#Connection closed by foreign host.
#[root@localhost shell]# telnet 127.0.0.1 23
#Trying 127.0.0.1...
#telnet: connect to address 127.0.0.1: Connection refused 端口响应不成功
# 代码
#!/bin/bash
# 定义函数
port_status_fun (){
# . 后面的 x x x 表示随机生成一个文件名
temp_file=`mktemp port_status.XXX`
# 1. 判断是否有 telnet 这个命令
[ ! -x /usr/bin/telnet ] && echo "telnet: note found command" && exit 1
# 2. 测试 ip 和端口
( telnet $1 $2 <<EOF
quit
EOF
) &> $temp_file
# 3. 分析文本中的内容,判断结果
if grep -E "^]" $temp_file &>/dev/null
then
echo "$1 $2 is Open"
else
echo "$1 $2 is Close"
fi
rm -f $temp_file
}
# 调用函数
port_status_fun $1 $2
11.3 内存使用率
内存使用顺序:
- free
- buff/cache
- Swap
#!/bin/bash
# 内存申请顺序 free-cache-buffer-swap
memory_use() {
memory_used=`head -2 /proc/meminfo | awk 'NR==1{t=$2}NR==2{f=$2;print (t-f)*100/t}'`
memory_cache=`head -5 /proc/meminfo | awk 'NR==1{t=$2}NR==5{c=$2;print c*100/t}'`
memory_buffer=`head -4 /proc/meminfo | awk 'NR==1{t=$2}NR==4{b=$2;print b*100/t}'`
echo -e "memory_used:$memory_used\tmemory_cache:$memory_cache\tmemory_buffer:$memory_buffer"
}
memory_use
[root@localhost shell]# head -5 /proc/meminfo
MemTotal: 2046816 kB
MemFree: 115500 kB
MemAvailable: 1265080 kB
Buffers: 102492 kB
Cached: 1140640 kB
11.4 内存使用前10进程
-
top -b -n 1
-b 表示所有进程
-n 1 表示只需要一次的信息
-
使用 ps aux 显示所有进程信息也可以
#!/bin/bash
# 统计系统中前十名使用内存最多的进程
memory() {
#1、收集任务管理器进程信息
temp_file=`mktemp memory.XXX`
top -b -n 1 > $temp_file
#2、按照进程统计内存大小
tail -n +8 $temp_file | awk '{array[$NF]+=$6}END{for (i in array) print array[i],i}' | sort -k 1 -n -r | head -10
#3、删除临时文件
rm -f $temp_file
}
# 统计系统中前十名使用CPU最多的进程
CPU() {
#1、收集任务管理器进程信息
temp_file=`mktemp memory.XXX`
top -b -n 1 > $temp_file
#2、按照进程统计内存大小
tail -n +8 $temp_file | awk '{array[$NF]+=$9}END{for (i in array) print array[i],i}' | sort -k 1 -n -r | head -10
#3、删除临时文件
rm -f $temp_file
}
echo "memory top10"
memory
echo "cpu top10"
CPU
11.5 io 队列长度监控
监控目的: IO队列长度、IOPS(每秒操作次数)、磁盘吞吐量
监控方法: iostat (sysstat包提供)
语法:iostat [选项][<时间间隔>[<次数>]]
选项说明:
-c:只显示系统 CPU 统计信息,即单独输出 avg-cpu 结果,不包括 device 结果
-d:单独输出 Device 结果,不包括 cpu 结果
-k/-m:输出结果以 kB/mB 为单位,而不是以扇区数为单位
-x:输出更详细的 io 设备统计信息
interval/count:每次输出间隔时间,count 表示输出次数,不带 count 表示循环输出
[root@localhost ~]# iostat
Linux 3.10.0-1160.71.1.el7.x86_64 (localhost) 04/30/2023 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
0.60 0.00 0.56 0.23 0.00 98.61
Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
vda 7.03 1.00 58.44 6635778 387751088
scd0 0.00 0.00 0.00 7248 0
# iostat 结果从系统开机到当前执行时刻的统计信息
avg-cpu:总体 cpu 使用情况统计信息,对于多核 cpu,这里为所有 cpu 的平均值。重点关注 iowait 值,表示 cpu 用于等待 io 请求的完成时间
%user:CPU处在用户模式下的时间百分比
%nice:CPU处在带 NICE 值的用户模式下的时间百分比
%system:CPU处在系统模式下的时间百分比
%iowait:CPU等待输入输出完成时间的百分比
%steal:管理程序维护另一个虚拟处理器时,虚拟 CPU的无意识等待时间百分比
%idle:CPU空闲时间百分比
Device:以 sdX 形式显示的设备名称
tps:每秒进程下发的 IO读、写请求数量
KB_read/s:每秒从驱动器读入的数据量,单位为 K
KB_wrtn/s:每秒从驱动器写入的数据量,单位为 K
KB_read:读入数据总量,单位为 K
KB_wrtn:写入数据总量,单位为 K
[root@localhost ~]# iostat -x
Linux 3.10.0-1160.71.1.el7.x86_64 (localhost) 04/30/2023 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
0.60 0.00 0.57 0.23 0.00 98.61
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 6.25 0.03 6.99 1.00 58.44 16.92 0.02 3.10 5.70 3.09 0.80 0.56
scd0 0.00 0.00 0.00 0.00 0.00 0.00 102.08 0.00 0.30 0.30 0.00 0.24 0.00
rrqm/s:每秒对设备的读请求被合并次数,文件系统对读取同块(block)的请求进行合并
wrqm/s:每秒对设备的写请求被合并次数
r/s:每秒完成的读次数
w/s:每秒完成的写次数
rkB/s:每秒读数据量(kB为单位)
wkB/s:每秒写数据量(kB为单位)
avgrq-sz:平均每次IO操作的数据量(扇区数为单位)
avgqu-sz:平均等待处理的IO请求队列长度
await:平均每次IO请求等待时间(包括等待时间和处理时间,毫秒为单位)
svctm:平均每次IO请求的处理时间(毫秒为单位)
%until:采用周期内用于IO操作的时间比率,即IO队列非空的时间比率
# 重点关注参数
1、iowait% 表示CPU等待IO时间占整个CPU周期的百分比,如果iowait值超过50%,或者明显大于%system、%user以及%idle,表示IO可能存在问题
2、avgqu-sz 表示磁盘IO队列长度,即IO等待个数
3、await 表示每次IO请求等待时间,包括等待时间和处理时间
4、svctm 表示每次IO请求处理的时间
5、%util 表示磁盘忙碌情况,一般该值超过80%表示该磁盘可能处于繁忙状态
如果%util 接近100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘可能存在瓶颈。如果 svctm 比较接近 await,说明 I/O 几乎没有等待时间;如果 await 远大于 svctm,说明 i/o 队列太长,IO 响应太慢,则需要进行必要优化。如果avgqu-sz比较大,也表示有当量IO在等待。
dm-0、dm-1、dm-2 的主设备号是 253(是linux内核留给本地使用的设备号),次设备号分别是 0、1、2,这类设备在 /dev/mapper 中
#!/bin/bash
io() {
device_num=`iostat -x | grep -E "^sd[a-z]" | wc -l`
iostat -x 1 3 | grep -E "^sd[a-z]" | tail -n +$((device_num+1)) | awk '{io_long[$1]+=$9}END{for( i in io_long) print io_long[i],i}'
}
while true
do
io
sleep 5
done