Linux命令与Shell脚本实战

第一阶段:基础准备—— 先搞定 “Linux 环境” 和 “命令行习惯”

Shell 脚本和工具(grep/sed/awk)都依赖 Linux 命令行,先回顾核心 Linux 基础,避免后续 “卡壳”。

1.1 必须掌握的 Linux 基础(回顾 + 实验)

知识模块 核心内容 实战实验(跟着做)
Linux 文件系统 目录结构(/home、/etc、/var、/tmp)、绝对路径 vs 相对路径、文件权限(rwx) 实验 1:创建多层目录并操作1. 执行 
mkdir -p /test/shell/learn
(创建多层目录)2. 执行 
cd /test/shell/learn
(绝对路径切换)3. 执行 
touch demo.txt
(创建空文件)4. 执行 
chmod 755 demo.txt
(修改权限为 “所有者可读可写可执行,其他只读可执行”)5. 执行 
ls -l
(查看权限是否生效,结果应显示 
-rwxr-xr-x
常用命令
cd
(切换目录)、
ls
(列文件)、
touch
(创文件)、
cat
(读文件)、
cp
(复制)、
mv
(移动 / 改名)、
rm
(删除)、
tail
(看文件末尾)、
head
(看文件开头)
实验 2:文件操作实战1. 执行 
cat > test.log << EOF
(创建日志文件,输入以下内容)
2024-11-20 10:00:00 [INFO] 服务器启动成功

2024-11-20 10:05:00 [ERROR] 数据库连接失败

2024-11-20 10:10:00 [INFO] 重启数据库成功

EOF
(按回车结束输入)2. 执行 
head -1 test.log
(查看第一行,应显示 “服务器启动成功”)3. 执行 
tail -2 test.log
(查看最后两行,应包含 “数据库连接失败” 和 “重启成功”)4. 执行 
cp test.log test.log.bak
(备份日志)5. 执行 
mv test.log.bak /tmp/
(把备份移到 /tmp 目录)
Shell 环境 默认 Shell(
echo $SHELL
 查看,企业常用 bash)、环境变量(
~/.bashrc
 配置个人环境)、命令帮助(
man 命令
 或 
命令 --help
实验 3:配置 bash 环境1. 执行 
echo $SHELL
(确认是否为 
/bin/bash
)2. 执行 
vim ~/.bashrc
(编辑 bash 配置文件,添加一行)
PS1='[u@h W]$ '
(修改命令提示符为 “[用户名 @主机名 当前目录]$ ”)3. 执行 
source ~/.bashrc
(让配置生效,此时命令提示符会立即变化)4. 执行 
man grep
(查看 grep 的帮助文档,按 
q
 退出)

1.2 学习工具准备

终端工具:Windows 用 Xshell、FinalShell;Mac 用自带终端或 iTerm2(方便复制命令、多窗口操作)。实验环境:推荐用虚拟机(VMware Workstation)装 CentOS 7,或直接买阿里云 / 腾讯云学生机(1 核 2G 足够,成本低,贴近真实服务器)。文本编辑器
vim
(必须会基础操作:
i
 插入、
ESC
 退出插入、
:wq
 保存退出、
:q!
 不保存退出)—— 后续写脚本全靠它。

第二阶段:正则表达式—— 所有工具的 “灵魂”,先吃透

grep/sed/awk 的核心是 “用正则匹配文本”,先学正则,再用工具时会 “一通百通”。

2.1 正则表达式基础(核心元字符)

正则是 “描述文本规则的符号”,比如 “匹配以数字开头的行”“匹配包含邮箱的字符串”,先记基础元字符:

元字符 作用说明 示例(匹配目标)

^
匹配行的开头
^ERROR
 → 匹配以 “ERROR” 开头的行

$
匹配行的结尾
success$
 → 匹配以 “success” 结尾的行

.
匹配任意 1 个字符(除了换行符)
a.b
 → 匹配 “aab”“acb”“a1b”(中间任意 1 个)

*
匹配前面的字符 “0 次或多次”
a*b
 → 匹配 “b”“ab”“aab”“aaab”

[]
匹配 “中括号内的任意 1 个字符”
[0-9]
 → 匹配任意 1 个数字;
[a-zA-Z]
 → 任意字母

[^]
匹配 “不在中括号内的任意 1 个字符”(取反)
[^0-9]
 → 匹配非数字字符

?
匹配前面的字符 “0 次或 1 次”(扩展正则,需加 
-E
 选项)

a?b
 → 匹配 “b” 或 “ab”

+
匹配前面的字符 “1 次或多次”(扩展正则,需加 
-E
 选项)

a+b
 → 匹配 “ab”“aab”(至少 1 个 a)

{n,m}
匹配前面的字符 “n 到 m 次”(扩展正则,需加 
-E
 选项)

a{2,3}b
 → 匹配 “aab”“aaab”

|
匹配 “左边或右边的表达式”(扩展正则,需加 
-E
 选项)

ERROR|WARN
 → 匹配 “ERROR” 或 “WARN”

()
分组(把多个字符当一个整体,扩展正则,需加 
-E
 选项)

(ab)+
 → 匹配 “ab”“abab”“ababab”

d
匹配数字(等价于 
[0-9]
,部分工具支持,如 grep)

d{4}
 → 匹配 4 个连续数字(如 “2024”)

[:alnum:]
匹配字母 + 数字(等价于 
[a-zA-Z0-9]
,通用字符类,避免中英文问题)

[:alnum:]+
 → 匹配连续的字母或数字

2.2 正则表达式实验(分基础→进阶)

先创建一个测试文件 
regex_test.txt
,后续所有正则实验都基于它:

bash

运行



# 执行以下命令创建文件
cat > regex_test.txt << EOF
1. 运维工程师的邮箱是 ops@example.com
2. 服务器IP:192.168.1.100,网关:192.168.1.1
3. 错误日志:ERROR: 数据库连接超时(2024-11-20 10:05)
4. 警告日志:WARN: 磁盘空间不足(剩余20%)
5. 正常日志:INFO: 服务启动成功(耗时123秒)
6. 无效行:@#$%^&*()_+
7. 多行测试:
   第一行结尾
   第二行中间有空格
EOF

实验 1:基础元字符练习(用 grep 验证,grep 默认支持基础正则)

匹配以 “ERROR” 开头的行:命令:
grep '^ERROR' regex_test.txt
预期结果:只显示第 3 行(
3. 错误日志:ERROR: 数据库连接超时(2024-11-20 10:05)
)解释:
^
 锚定行首,只找开头是 ERROR 的行。

匹配以 “com” 结尾的行:命令:
grep 'com$' regex_test.txt
预期结果:只显示第 1 行(
1. 运维工程师的邮箱是 ops@example.com
)解释:
$
 锚定行尾,只找结尾是 com 的行。

匹配包含 “IP:任意字符” 的行(用 
.
 匹配任意字符):命令:
grep 'IP:.' regex_test.txt
预期结果:显示第 2 行(
2. 服务器IP:192.168.1.100,网关:192.168.1.1
)解释:
.
 匹配 “IP:” 后面的任意 1 个字符(这里是 “1”),所以能命中该行。

匹配包含 “数字 +%” 的行(用 
[]
 匹配数字):命令:
grep '[0-9]%' regex_test.txt
预期结果:显示第 4 行(
4. 警告日志:WARN: 磁盘空间不足(剩余20%)
)解释:
[0-9]
 匹配任意数字,后面跟 “%”,所以命中 “20%”。

实验 2:扩展正则练习(用 
grep -E
,支持 
?
 
+
 
|
 
()

匹配包含 “ERROR” 或 “WARN” 的行(用 
|
 表示 “或”):命令:
grep -E 'ERROR|WARN' regex_test.txt
预期结果:显示第 3 行(ERROR)和第 4 行(WARN)解释:
|
 分隔两个关键词,只要包含其中一个就匹配。

匹配包含 “1-3 个数字 + 秒” 的行(用 
{n,m}
 控制次数):命令:
grep -E '[0-9]{1,3}秒' regex_test.txt
预期结果:显示第 5 行(
5. 正常日志:INFO: 服务启动成功(耗时123秒)
)解释:
[0-9]{1,3}
 匹配 1-3 个连续数字,后面跟 “秒”,所以命中 “123 秒”。

匹配包含 “邮箱格式(xxx@xxx.xxx)” 的行(用分组 
()
):命令:
grep -E '[a-z]+@[a-z]+.[a-z]+' regex_test.txt
预期结果:显示第 1 行(
1. 运维工程师的邮箱是 ops@example.com
)解释:


[a-z]+
:匹配 1 个以上小写字母(如 “ops”“example”“com”);
@
:匹配 @符号;
.
:匹配小数点(
.
 是元字符,需要用 
 转义,否则会匹配任意字符)。

第三阶段:核心工具实战—— grep→sed→awk,逐个突破

这三个工具是运维 “三剑客”,用途不同:

grep:专注 “文本搜索”(找符合条件的行);sed:专注 “文本编辑”(改、删、插行);awk:专注 “结构化数据处理”(按列分割、统计、计算)。

3.1 grep 工具(文本搜索之王)

3.1.1 grep 核心用法(选项 + 场景)

选项 作用说明 常用场景

-i
忽略大小写(匹配时不区分大写小写) 搜索日志时,同时匹配 “Error”“ERROR”

-v
反向匹配(只显示 “不满足条件” 的行) 过滤日志中的空行(
grep -v '^$'

-n
显示匹配行的 “行号” 定位日志中错误行的位置

-c
只统计 “匹配行的数量”(不显示具体内容) 统计错误日志有多少条

-r
递归搜索(遍历指定目录下的所有文件) 搜索项目目录中包含 “密码” 的文件

-A n
显示匹配行及其 “后面 n 行”(After) 看错误行后面的上下文

-B n
显示匹配行及其 “前面 n 行”(Before) 看错误行前面的上下文

-C n
显示匹配行及其 “前后 n 行”(Context) 看错误行前后的上下文

-E
支持扩展正则(等价于 
egrep
用复杂正则匹配

-F
把模式当作 “固定字符串”(不解析正则,等价于 
fgrep
搜索包含 “.”“*” 等元字符的字符串

3.1.2 grep 实战实验(结合运维场景)

先准备实验环境:创建 
/var/log/ops/
 目录,生成两个日志文件 
app.log
 和 
db.log

bash

运行



# 1. 创建目录
mkdir -p /var/log/ops/
 
# 2. 生成 app.log(应用日志)
cat > /var/log/ops/app.log << EOF
2024-11-20 09:00:00 [INFO] App started (PID: 1234)
2024-11-20 09:05:00 [ERROR] Connection failed: timeout (IP: 10.0.0.5)
2024-11-20 09:10:00 [INFO] User 'admin' logged in
2024-11-20 09:15:00 [ERROR] File not found: /data/config.ini
2024-11-20 09:20:00 [WARN] Memory usage > 80%
EOF
 
# 3. 生成 db.log(数据库日志)
cat > /var/log/ops/db.log << EOF
2024-11-20 09:00:00 [INFO] MySQL started (Port: 3306)
2024-11-20 09:06:00 [ERROR] MySQL connection refused (User: root)
2024-11-20 09:10:00 [INFO] MySQL query success (Time: 0.2s)
EOF
实验 1:搜索单个文件中的错误日志(基础搜索)

需求:从 
app.log
 中找所有包含 “ERROR” 的行,并显示行号。命令:
grep -n 'ERROR' /var/log/ops/app.log
预期结果:

plaintext



2:2024-11-20 09:05:00 [ERROR] Connection failed: timeout (IP: 10.0.0.5)
4:2024-11-20 09:15:00 [ERROR] File not found: /data/config.ini

解释:
-n
 显示行号,能快速定位错误在文件中的位置(第 2 行和第 4 行)。

实验 2:统计所有日志中的错误数量(递归 + 统计)

需求:递归搜索 
/var/log/ops/
 目录下所有文件,统计包含 “ERROR” 的总行数。命令:
grep -rc 'ERROR' /var/log/ops/
预期结果:

plaintext



/var/log/ops/app.log:2
/var/log/ops/db.log:1

解释:
-r
 递归遍历目录,
-c
 统计每个文件的匹配行数,结果显示 app.log 有 2 个 ERROR,db.log 有 1 个。

实验 3:过滤空行并查看错误上下文(反向匹配 + 上下文)

需求:从 
app.log
 中过滤掉空行(反向匹配),并查看 “ERROR” 行的前后 1 行上下文。命令:
grep -v '^$' /var/log/ops/app.log | grep -C 1 'ERROR'
预期结果(以第一个 ERROR 为例):

plaintext



2024-11-20 09:00:00 [INFO] App started (PID: 1234)
2024-11-20 09:05:00 [ERROR] Connection failed: timeout (IP: 10.0.0.5)
2024-11-20 09:10:00 [INFO] User 'admin' logged in
--
2024-11-20 09:10:00 [INFO] User 'admin' logged in
2024-11-20 09:15:00 [ERROR] File not found: /data/config.ini
2024-11-20 09:20:00 [WARN] Memory usage > 80%

解释:


grep -v '^$'
:过滤空行(
^$
 匹配空行,
-v
 反向匹配,即保留非空行);
|
:管道符,把前一个命令的输出作为后一个命令的输入;
grep -C 1 'ERROR'
:显示 ERROR 行的前后 1 行(
-C 1
 表示上下文 1 行),方便分析错误原因。

实验 4:搜索固定字符串(避免正则解析)

需求:搜索包含 “[INFO]” 的行(
[
 和 
]
 是正则元字符,需当作普通字符处理)。命令:
grep -F '[INFO]' /var/log/ops/app.log
预期结果:显示所有包含 “[INFO]” 的行(第 1、3 行)解释:
-F
 表示 “固定字符串匹配”,不解析正则元字符(如 
[
 
]
),直接按字符串 “[INFO]” 搜索,避免匹配错误。

3.2 sed 工具(流编辑器,文本编辑之王)

sed 的核心是 “按行处理”:读取一行→按规则处理(改 / 删 / 插)→输出一行,不默认修改原文件(需加 
-i
 才改)。

3.2.1 sed 核心语法与命令

基本语法:
sed [选项] '操作命令' 文件名
常用选项:


-n
:抑制默认输出(只显示处理后的行,否则会输出所有行);
-i
:直接修改原文件(危险!建议先不加 
-i
 测试,没问题再加
);
-e
:执行多个操作命令(多个命令用 
-e
 分隔);
-f
:从脚本文件读取操作命令(命令多时用)。

核心操作命令(重点记前 5 个):

命令 作用说明 示例(修改文件 test.txt)

s/旧/新/
替换(substitute):把 “旧字符串” 换成 “新字符串”(默认只换每行第一个匹配)
sed 's/ERROR/WARN/' test.txt
 → 把 ERROR 换成 WARN

s/旧/新/g
全局替换(g=global):换每行所有匹配的 “旧字符串”
sed 's/ERROR/WARN/g' test.txt
 → 每行所有 ERROR 都换

d
删除(delete):删除符合条件的行
sed '/ERROR/d' test.txt
 → 删除包含 ERROR 的行

p
打印(print):打印符合条件的行(常和 
-n
 一起用)

sed -n '/INFO/p' test.txt
 → 只打印包含 INFO 的行

a内容
在符合条件的行 “后面” 插入(append)一行 “内容”
sed '/INFO/a---这是插入的行---' test.txt
 → 在 INFO 行后插行

i内容
在符合条件的行 “前面” 插入(insert)一行 “内容”
sed '/INFO/i---这是插入的行---' test.txt
 → 在 INFO 行前插行

c内容
用 “内容” 替换符合条件的 “整行”(change)
sed '/ERROR/c---该行已替换---' test.txt
 → 替换 ERROR 行

3.2.2 sed 实战实验(基于前面的 
/var/log/ops/app.log

实验 1:替换字符串(单行替换 + 全局替换)

需求 1:把 
app.log
 中 “ERROR” 换成 “ERR”(只换每行第一个 ERROR)。命令:
sed 's/ERROR/ERR/' /var/log/ops/app.log
预期结果(关键行):

plaintext



2024-11-20 09:05:00 [ERR] Connection failed: timeout (IP: 10.0.0.5)  # ERROR→ERR
2024-11-20 09:15:00 [ERR] File not found: /data/config.ini          # ERROR→ERR

解释:
s/ERROR/ERR/
 只替换每行第一个匹配的 ERROR(这里每行只有一个,效果和全局一样)。

需求 2:若文件中有 “ERROR ERROR”,需替换所有 ERROR 为 ERR(全局替换)。先构造测试文件:
echo "ERROR ERROR" > test.txt
命令:
sed 's/ERROR/ERR/g' test.txt
预期结果:
ERR ERR
(两个 ERROR 都被替换)解释:
g
 表示全局替换,每行所有匹配都换。

实验 2:删除行(删除空行、删除指定行)

需求 1:删除 
app.log
 中的空行(若有的话)。命令:
sed '/^$/d' /var/log/ops/app.log
预期结果:所有空行被删除,只显示非空行解释:
/^$/
 匹配空行,
d
 命令删除该行。

需求 2:删除 
app.log
 中包含 “WARN” 的行。命令:
sed '/WARN/d' /var/log/ops/app.log
预期结果:原文件第 5 行(WARN 行)被删除,其他行保留解释:
/WARN/
 匹配包含 WARN 的行,
d
 命令删除该行。

实验 3:插入行(在指定行前后插内容)

需求:在 
app.log
 中 “包含 INFO 的行” 后面插入一行 “——INFO 日志结束 ——”。命令:
sed '/INFO/a——INFO日志结束——' /var/log/ops/app.log
预期结果(关键行):

plaintext



2024-11-20 09:00:00 [INFO] App started (PID: 1234)
——INFO日志结束——
2024-11-20 09:05:00 [ERROR] Connection failed: timeout (IP: 10.0.0.5)
2024-11-20 09:10:00 [INFO] User 'admin' logged in
——INFO日志结束——

解释:
/INFO/
 匹配包含 INFO 的行,
a
 命令在该行后面插入指定内容。

实验 4:直接修改原文件(
sed -i
,运维常用)

需求:批量修改 
app.log
 中所有 “2024-11-20” 为 “2024-11-21”(日期更正)。步骤:

先测试修改效果(不加 
-i
,看输出是否正确):命令:
sed 's/2024-11-20/2024-11-21/g' /var/log/ops/app.log
预期:所有日期都变成 2024-11-21。

确认正确后,加 
-i
 修改原文件:命令:
sed -i 's/2024-11-20/2024-11-21/g' /var/log/ops/app.log

验证修改结果:命令:
cat /var/log/ops/app.log
预期:原文件中的日期已全部替换为 2024-11-21。注意
-i
 会直接修改原文件,建议先备份(如 
cp app.log app.log.bak
)再操作!

实验 5:批量修改文件名(结合 
find
 和 
sed

需求:当前目录有 
file1.txt
 
file2.txt
 
file3.txt
,批量把 “file” 改成 “doc”(即 
doc1.txt
 等)。步骤:

先创建测试文件:
touch file1.txt file2.txt file3.txt

用 
find
 找文件,
sed
 改文件名:命令:
find . -name "file*.txt" | sed -n 's/(.*)file(.*)/mv & 1doc2/p' | sh
解释:


find . -name "file*.txt"
:找到当前目录下所有 file 开头的 txt 文件;
sed -n 's/(.*)file(.*)/mv & 1doc2/p'


(.*)file(.*)
:分组匹配,把文件名拆成 “前缀 + file + 后缀”(如 “./file1.txt” 拆成 “./” 和 “1.txt”);
mv & 1doc2
:生成 mv 命令(
&
 表示原文件名,
1
 是第一个分组,
2
 是第二个分组,即 
mv ./file1.txt ./doc1.txt
);
-n
 和 
p
:只打印生成的 mv 命令;

| sh
:执行生成的 mv 命令,完成文件名修改。

验证结果:
ls
 → 显示 
doc1.txt doc2.txt doc3.txt

3.3 awk 工具(结构化数据处理之王)

awk 擅长处理 “按列分割” 的数据(如日志、CSV 文件),核心是 “模式 + 动作”:对符合 “模式” 的行,执行 “动作”。

3.3.1 awk 核心概念与语法

1. 基础语法


awk '模式{动作}' 文件名

模式:可以是 “行号”(如 
NR==1
 表示第 1 行)、“条件”(如 
$3>100
 表示第 3 列大于 100)、“BEGIN/END”(特殊模式,分别在处理文件前 / 后执行);动作:可以是 “打印”(
print
)、“计算”(
sum+=1
)、“判断”(
if($1=="ERROR")
)等。

2. 核心内置变量(必须记)
变量名 作用说明

$0
表示 “整行内容”

$n
表示 “第 n 列内容”(列默认用 “空格或制表符” 分割,可通过 
FS
 修改分隔符)

FS
输入字段分隔符(默认是空格,可在 BEGIN 中设置,如 
BEGIN{FS=","}
 表示用逗号分割)

OFS
输出字段分隔符(默认是空格,如 `BEGIN {OFS=” “}` 表示输出用竖线分隔)

NR
已读取的 “总行数”(遍历多个文件时,行数连续计数)

NF
当前行的 “列数”(如一行有 3 列,
NF==3

FILENAME
当前处理的 “文件名”(遍历多个文件时有用)

3.3.2 awk 实战实验(结合运维场景:日志统计、数据计算)

先准备两个实验文件:

服务器监控日志 
server_monitor.log
(按 “时间 服务器 IP CPU 使用率 内存使用率 磁盘使用率” 格式记录):

bash

运行



cat > server_monitor.log << EOF
2024-11-20 09:00 192.168.1.100 20% 30% 40%
2024-11-20 09:00 192.168.1.101 85% 70% 60%
2024-11-20 10:00 192.168.1.100 25% 35% 42%
2024-11-20 10:00 192.168.1.101 90% 75% 65%
2024-11-20 11:00 192.168.1.100 30% 40% 45%
2024-11-20 11:00 192.168.1.101 95% 80% 70%
EOF

CSV 格式的用户数据 
user.csv
(按 “用户名,UID,GID, 家目录,shell” 格式记录):

bash

运行



cat > user.csv << EOF
root,0,0,/root,/bin/bash
bin,1,1,/bin,/sbin/nologin
daemon,2,2,/sbin,/sbin/nologin
admin,1000,1000,/home/admin,/bin/bash
test,1001,1001,/home/test,/bin/bash
EOF
实验 1:按列提取数据(基础打印)

需求 1:从 
server_monitor.log
 中提取 “服务器 IP” 和 “CPU 使用率”(第 2 列是 IP,第 3 列是 CPU)。命令:
awk '{print $2, $3}' server_monitor.log
预期结果:

plaintext



192.168.1.100 20%
192.168.1.101 85%
192.168.1.100 25%
192.168.1.101 90%
192.168.1.100 30%
192.168.1.101 95%

解释:
$2
 是第 2 列(IP),
$3
 是第 3 列(CPU),
print $2, $3
 打印这两列,默认用空格分隔。

需求 2:从 
user.csv
 中提取 “用户名” 和 “家目录”(CSV 用逗号分隔,需修改 
FS
)。命令:
awk 'BEGIN{FS=","} {print $1, $4}' user.csv
预期结果:

plaintext



root /root
bin /bin
daemon /sbin
admin /home/admin
test /home/test

解释:
BEGIN{FS=","}
 表示在处理文件前,把输入分隔符设为逗号(默认是空格),所以 
$1
 是用户名,
$4
 是家目录。

实验 2:按条件过滤行(模式匹配)

需求 1:从 
server_monitor.log
 中找出 “CPU 使用率> 80%” 的行(第 3 列是 CPU,需去掉 “%” 再比较)。命令:
awk '$3+0 > 80 {print $2, $3}' server_monitor.log
预期结果:

plaintext



192.168.1.101 85%
192.168.1.101 90%
192.168.1.101 95%

解释:


$3+0
:把第 3 列(如 “85%”)转成数字(85),因为 “%” 会让内容变成字符串,无法直接比较;
$3+0 > 80
:条件模式,只处理 CPU>80% 的行;
{print $2, $3}
:打印这些行的 IP 和 CPU。

需求 2:从 
user.csv
 中找出 “shell 是 /bin/bash” 的用户(第 5 列是 shell,用逗号分隔)。命令:
awk 'BEGIN{FS=","} $5=="/bin/bash" {print $1, $5}' user.csv
预期结果:

plaintext



root /bin/bash
admin /bin/bash
test /bin/bash

解释:
$5=="/bin/bash"
 是条件模式,只处理第 5 列(shell)为 /bin/bash 的行,然后打印用户名和 shell。

实验 3:统计数据(计算求和、平均值)

需求:统计 
server_monitor.log
 中 192.168.1.101 的 “平均 CPU 使用率”。命令:
awk '$2=="192.168.1.101" {sum+=$3+0; count++} END{print "平均CPU使用率:", sum/count "%"}' server_monitor.log
预期结果:
平均CPU使用率: 90%
解释:


$2=="192.168.1.101"
:只处理 IP 是 101 的行;
sum+=$3+0
:累加 CPU 使用率(转成数字);
count++
:统计符合条件的行数(共 3 行);
END{...}
:处理完所有行后执行,计算平均值(sum/count= (85+90+95)/3=90)并打印。

实验 4:格式化输出(
printf
,比
print
更灵活)

需求:从 
user.csv
 中提取用户信息,按 “用户名:root,UID:0” 的格式整齐输出。命令:
awk 'BEGIN{FS=","; printf "%-10s %-5s
", "用户名", "UID"} {printf "%-10s %-5d
", $1, $2}' user.csv
预期结果:

plaintext



用户名       UID  
root        0    
bin         1    
daemon      2    
admin       1000 
test        1001 

解释:


printf "%-10s %-5s
"
:格式化输出:


%-10s
:左对齐(
-
 表示左对齐),占 10 个字符位,字符串类型(
s
);
%-5d
:左对齐,占 5 个字符位,数字类型(
d
);

:换行;

BEGIN
 中先打印表头(用户名、UID),再处理每行数据,输出更整齐。

实验 5:处理日志中的 IP 访问次数(运维高频需求)

需求:假设有访问日志 
access.log
(格式:
IP 访问时间 访问路径
),统计每个 IP 的访问次数,按次数降序排列。步骤:

创建 
access.log

bash

运行



cat > access.log << EOF
192.168.1.100 2024-11-20 09:00 /index.html
192.168.1.100 2024-11-20 09:05 /login.html
192.168.1.101 2024-11-20 09:10 /index.html
192.168.1.100 2024-11-20 09:15 /user.html
192.168.1.102 2024-11-20 09:20 /index.html
192.168.1.101 2024-11-20 09:25 /logout.html
EOF

用 awk 统计 IP 次数,sort 排序:命令:
awk '{ip[$1]++} END{for(i in ip) print i, ip[i]}' access.log | sort -k2nr
预期结果:

plaintext



192.168.1.100 3
192.168.1.101 2
192.168.1.102 1

解释:


ip[$1]++
:定义关联数组 
ip
,以 IP(
$1
)为索引,每遇到一个 IP 就加 1(统计次数);
END{for(i in ip) print i, ip[i]}
:遍历数组,打印 IP 和对应的次数;
sort -k2nr
:按第 2 列(次数)降序排列(
-k2
 表示按第 2 列,
n
 表示数字排序,
r
 表示降序)。

第四阶段:Shell 脚本编程—— 把工具串联成 “自动化脚本”

运维工作中,很少单独用 grep/sed/awk,而是用 Shell 脚本把它们串联起来,实现 “自动化任务”(如定时备份、系统监控、批量部署)。

4.1 Shell 脚本基础(语法 + 规范)

4.1.1 脚本开头与执行方式

脚本开头必须加 “解释器路径”:
#!/bin/bash
(告诉系统用 bash 执行脚本);脚本命名:建议以 
.sh
 结尾(如 
backup.sh
),方便识别;执行方式:
赋予执行权限:
chmod +x script.sh
,然后执行 
./script.sh
(绝对 / 相对路径);直接用 bash 执行:
bash script.sh
(无需执行权限)。

示例:创建第一个脚本 
hello.sh

bash

运行



# 1. 用vim创建脚本
vim hello.sh
 
# 2. 输入以下内容
#!/bin/bash
# 这是注释:第一个Shell脚本
echo "Hello, Shell!"  # 打印内容
 
# 3. 赋予执行权限并执行
chmod +x hello.sh
./hello.sh
 
# 预期结果:输出 "Hello, Shell!"

4.1.2 核心语法(变量、流程控制、函数)

1. 变量(无类型,直接定义)
语法 说明 示例
定义变量
变量名=值
(等号前后无空格!)

name="Linux"
 → 定义变量 name
使用变量
$变量名
 或 
${变量名}
(推荐用 
${}
,避免歧义)

echo $name
 → 输出 Linux;
echo ${name}Ops
 → 输出 LinuxOps
位置参数
$1
 表示第一个参数,
$2
 第二个,…,
$n
 第 n 个;
$*
 所有参数;
$#
 参数个数
执行 
./script.sh a b c
 → 
$1=a

$#=3
环境变量 系统预定义变量(大写):
$HOME
(家目录)、
$PATH
(命令路径)、
$?
(上命令退出码,0 = 成功,非 0 = 失败)

echo $HOME
 → 输出 /root(root 用户)

实验:变量使用脚本 
var_demo.sh

bash

运行



#!/bin/bash
# 位置参数示例
echo "脚本名:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "所有参数:$*"
echo "参数个数:$#"
 
# 自定义变量
name="运维工程师"
age=30
echo "姓名:${name},年龄:${age}"
 
# 环境变量
echo "家目录:$HOME"
echo "上命令退出码:$?"  # 上一个echo命令成功,所以输出0

执行:
./var_demo.sh 北京 2024
预期结果:

plaintext



脚本名:./var_demo.sh
第一个参数:北京
第二个参数:2024
所有参数:北京 2024
参数个数:2
姓名:运维工程师,年龄:30
家目录:/root
上命令退出码:0
2. 流程控制(if-else、for、while)
(1)if-else 条件判断

语法:

bash

运行



if [ 条件 ]; then
    执行命令1
elif [ 条件2 ]; then
    执行命令2
else
    执行命令3
fi

条件判断注意


[ 条件 ]
 中,括号前后必须有空格;数值比较:
-eq
(等于)、
-ne
(不等于)、
-gt
(大于)、
-lt
(小于)、
-ge
(大于等于)、
-le
(小于等于);字符串比较:
==
(等于)、
!=
(不等于)、
-z
(空字符串)、
-n
(非空字符串);文件判断:
-f
(是否为文件)、
-d
(是否为目录)、
-r
(是否可读)、
-w
(是否可写)、
-x
(是否可执行)。

实验:系统磁盘监控脚本 
disk_monitor.sh
(当磁盘使用率 > 80% 时报警)

bash

运行



#!/bin/bash
# 监控根目录磁盘使用率
disk_usage=$(df -h / | grep '/' | awk '{print $5}' | sed 's/%//')  # 提取根目录使用率(数字)
 
if [ $disk_usage -gt 80 ]; then
    echo "【报警】根目录磁盘使用率已超过80%,当前使用率:${disk_usage}%"
    # 实际工作中可加邮件/短信报警(如用mail命令)
elif [ $disk_usage -ge 60 ]; then
    echo "【警告】根目录磁盘使用率已超过60%,当前使用率:${disk_usage}%"
else
    echo "【正常】根目录磁盘使用率:${disk_usage}%"
fi

执行:
./disk_monitor.sh
预期结果(根据实际磁盘使用率输出,如使用率 50% 时):
【正常】根目录磁盘使用率:50%

(2)for 循环(遍历列表或范围)

语法 1(遍历列表):

bash

运行



for 变量 in 列表; do
    执行命令
done

语法 2(遍历数字范围):

bash

运行



for ((i=1; i<=10; i++)); do
    执行命令
done

实验 1:批量创建用户脚本 
create_users.sh
(创建 user1-user5,密码 123456)

bash

运行



#!/bin/bash
# 批量创建用户
for user in user1 user2 user3 user4 user5; do
    # 先判断用户是否存在
    if id -u $user &>/dev/null; then
        echo "用户 $user 已存在,跳过创建"
    else
        useradd $user  # 创建用户
        echo "123456" | passwd --stdin $user &>/dev/null  # 设置密码(&>/dev/null 屏蔽输出)
        echo "用户 $user 创建成功,密码:123456"
    fi
done

执行(需 root 权限):
sudo ./create_users.sh
预期结果:

plaintext



用户 user1 创建成功,密码:123456
用户 user2 创建成功,密码:123456
...

实验 2:遍历数字范围,打印 1-5 的平方

bash

运行



#!/bin/bash
for ((i=1; i<=5; i++)); do
    square=$((i*i))  # 计算平方($((...)) 是算术运算)
    echo "${i}的平方是:${square}"
done

执行:
./square.sh
预期结果:

plaintext



1的平方是:1
2的平方是:4
3的平方是:9
4的平方是:16
5的平方是:25
(3)while 循环(条件为真时执行)

语法:

bash

运行



while [ 条件 ]; do
    执行命令
done

实验:倒计时脚本 
countdown.sh
(从 10 秒倒计时到 0)

bash

运行



#!/bin/bash
# 10秒倒计时
sec=10
while [ $sec -ge 0 ]; do
    echo -ne "倒计时:${sec}秒
"  # -ne:不换行;
:光标回到行首(覆盖当前行)
    sleep 1  # 暂停1秒
    sec=$((sec-1))  # 秒数减1
done
echo -e "
倒计时结束!"

执行:
./countdown.sh
预期结果:屏幕显示从 10 到 0 的倒计时,每秒更新一次,最后输出 “倒计时结束!”。

3. 函数(封装重复代码)

语法:

bash

运行



# 定义函数
函数名() {
    执行命令(可接收参数,用$1/$2...获取)
    return 退出码  # 可选,默认返回最后一个命令的退出码
}
 
# 调用函数
函数名 参数1 参数2...

实验:包含函数的备份脚本 
backup_func.sh
(封装备份逻辑,支持备份不同目录)

bash

运行



#!/bin/bash
# 定义备份函数
backup_dir() {
    local src_dir=$1  # 局部变量(只在函数内有效)
    local dest_dir="/backup"  # 备份目标目录
    local backup_name="backup_$(basename $src_dir)_$(date +%Y%m%d_%H%M%S).tar.gz"  # 备份文件名(含日期)
 
    # 检查源目录是否存在
    if [ ! -d $src_dir ]; then
        echo "错误:源目录 $src_dir 不存在!"
        return 1  # 返回错误码1
    fi
 
    # 创建目标目录(不存在则创建)
    [ ! -d $dest_dir ] && mkdir -p $dest_dir
 
    # 执行备份(tar打包压缩)
    tar -zcf ${dest_dir}/${backup_name} $src_dir &>/dev/null
    if [ $? -eq 0 ]; then
        echo "备份成功:${dest_dir}/${backup_name}"
        return 0
    else
        echo "备份失败:$src_dir"
        return 1
    fi
}
 
# 调用函数(备份/etc和/home目录)
backup_dir "/etc"
backup_dir "/home"

执行(需 root 权限,因为备份 /etc 和 /home):
sudo ./backup_func.sh
预期结果:

plaintext



备份成功:/backup/backup_etc_20241120_153000.tar.gz
备份成功:/backup/backup_home_20241120_153005.tar.gz

4.2 Shell 脚本实战(运维高频场景)

实战 1:定时日志备份脚本(结合 crontab)

需求:每天凌晨 2 点,自动备份 
/var/log/nginx/access.log
 到 
/data/log_backup/
,并删除 7 天前的旧备份。

步骤 1:编写脚本 
nginx_log_backup.sh

bash

运行



#!/bin/bash
# Nginx访问日志备份脚本
LOG_SRC="/var/log/nginx/access.log"  # 源日志文件
BACKUP_DIR="/data/log_backup"        # 备份目录
DATE=$(date +%Y%m%d_%H%M%S)          # 当前日期(如20241120_020000)
BACKUP_FILE="nginx_access_${DATE}.tar.gz"  # 备份文件名
 
# 1. 检查源日志是否存在
if [ ! -f $LOG_SRC ]; then
    echo "错误:日志文件 $LOG_SRC 不存在!" >&2  # 错误信息输出到标准错误
    exit 1
fi
 
# 2. 创建备份目录
mkdir -p $BACKUP_DIR
 
# 3. 备份日志(先打包,再清空原日志,避免日志过大)
tar -zcf ${BACKUP_DIR}/${BACKUP_FILE} $LOG_SRC &>/dev/null
if [ $? -eq 0 ]; then
    echo "$(date +'%Y-%m-%d %H:%M:%S') 备份成功:${BACKUP_DIR}/${BACKUP_FILE}" >> /var/log/backup.log  # 记录备份日志
    > $LOG_SRC  # 清空原日志(或用 logrotate,企业中更常用)
else
    echo "$(date +'%Y-%m-%d %H:%M:%S') 备份失败:$LOG_SRC" >> /var/log/backup.log
    exit 1
fi
 
# 4. 删除7天前的旧备份(find查找7天前的文件并删除)
find $BACKUP_DIR -name "nginx_access_*.tar.gz" -mtime +7 -delete
if [ $? -eq 0 ]; then
    echo "$(date +'%Y-%m-%d %H:%M:%S') 删除7天前的旧备份成功" >> /var/log/backup.log
else
    echo "$(date +'%Y-%m-%d %H:%M:%S') 删除旧备份失败" >> /var/log/backup.log
fi

步骤 2:赋予执行权限并测试

bash

运行



chmod +x nginx_log_backup.sh
sudo ./nginx_log_backup.sh  # 测试执行,检查是否生成备份文件

步骤 3:添加定时任务(crontab)

bash

运行



# 编辑crontab配置(root用户,因为要操作/var/log和/data)
sudo crontab -e
 
# 添加以下内容(每天凌晨2点执行)
0 2 * * * /path/to/nginx_log_backup.sh  # 替换/path/to为脚本实际路径
 
# 查看crontab状态(确保crond服务在运行)
sudo systemctl status crond  # CentOS
# 或
sudo systemctl status cron   # Ubuntu

实战 2:服务器批量执行命令脚本(结合 ssh 免密登录)

需求:批量在 10 台服务器上执行 “查看 CPU 使用率”“重启 nginx 服务” 等命令(前提:已配置 ssh 免密登录,即本地公钥已上传到目标服务器)。

步骤 1:准备服务器列表文件 
server_list.txt
(每行一个 IP)

bash

运行



cat > server_list.txt << EOF
192.168.1.100
192.168.1.101
192.168.1.102
# 此处省略其他7台服务器IP
EOF

步骤 2:编写批量执行脚本 
batch_exec.sh

bash

运行



#!/bin/bash
# 服务器批量执行命令脚本
SERVER_LIST="server_list.txt"  # 服务器列表文件
USER="root"                    # 登录用户名(建议用普通用户+sudo,更安全)
COMMAND=$1                     # 要执行的命令(通过位置参数传入)
 
# 检查命令是否传入
if [ $# -eq 0 ]; then
    echo "用法:$0 <要执行的命令>"
    echo "示例:$0 'top -bn1 | grep Cpu' (查看CPU使用率)"
    echo "示例:$0 'systemctl restart nginx' (重启nginx)"
    exit 1
fi
 
# 检查服务器列表文件是否存在
if [ ! -f $SERVER_LIST ]; then
    echo "错误:服务器列表文件 $SERVER_LIST 不存在!"
    exit 1
fi
 
# 遍历服务器列表,执行命令
while read server; do
    # 跳过空行和注释行
    if [ -z "$server" ] || [[ $server == #* ]]; then
        continue
    fi
 
    echo -e "
==================== 服务器 $server 执行结果 ===================="
    # 用ssh登录服务器执行命令
    ssh -o StrictHostKeyChecking=no $USER@$server "$COMMAND"  # -o StrictHostKeyChecking=no:首次登录不提示确认
    if [ $? -eq 0 ]; then
        echo "命令执行成功"
    else
        echo "命令执行失败"
    fi
done < $SERVER_LIST  # 从服务器列表文件读取内容,传给while循环

步骤 3:配置 ssh 免密登录(关键前提)

bash

运行



# 1. 本地生成ssh密钥对(已生成则跳过)
ssh-keygen -t rsa  # 一路按回车,不设置密码
 
# 2. 批量上传公钥到目标服务器(需输入目标服务器密码,只输一次)
while read server; do
    if [ -z "$server" ] || [[ $server == #* ]]; then
        continue
    fi
    ssh-copy-id $USER@$server  # 输入目标服务器密码
done < $SERVER_LIST

步骤 4:执行批量命令

bash

运行



# 赋予脚本执行权限
chmod +x batch_exec.sh
 
# 示例1:批量查看所有服务器的CPU使用率
./batch_exec.sh 'top -bn1 | grep Cpu'
 
# 示例2:批量重启所有服务器的nginx服务
./batch_exec.sh 'systemctl restart nginx'
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容