前言
前篇都是手动敲单条命令,当要重复执行几十上百条操作的时候,一条条敲效率太低了。这时候就可以把命令按照逻辑写成 Shell 脚本,一次性批量执行,实现自动化。Shell 脚本不需要编译,直接解释运行,是运维自动化最常用的工具。
一、脚本入门:怎么写和执行脚本
Shell 脚本本质就是一个文本文件,第一行必须用#!/bin/bash指定用哪个解释器来执行它,后面一行一条 Linux 命令就行。 比如最简单的脚本:
bash
#!/bin/bash
echo "Hello, Shell!"
执行脚本有两种方式,原理不一样:
- 直接用解释器调用:
bash ./test.sh,这种方式不需要脚本有执行权限,本质是 bash 程序帮你读这个脚本执行 - 给脚本加执行权限之后直接跑:
chmod +x test.sh,然后./test.sh,这种方式脚本自己就是可执行程序,系统会根据第一行的解释器路径来执行
保存为 hello.sh,运行方式:
bash
bash hello.sh # 用 bash 解释器执行脚本
sh hello.sh # 用 sh 解释器执行脚本
chmod +x hello.sh # 给脚本增加可执行权限
./hello.sh # 以可执行文件方式运行脚本
小技巧:还有一种
source ./test.sh的执行方式,不会开子进程,直接在当前终端环境执行,一般用来加载配置文件。
二、变量体系
1. 变量基础
定义变量的时候等号两边不能有空格,这是新手最容易踩的坑:name=张三是对的,name = 张三会直接报错。 Shell 里的变量默认都是字符串类型,不能直接做数学运算。变量值如果有空格,要用引号包起来:desc="我是一个字符串"。
unset 变量名:删除变量readonly 变量名:声明只读变量,不能改也不能删- 用
export 变量名可以把变量升级成全局环境变量,这样子脚本也能读到这个变量,注意是父进程传子进程,反过来不行。
系统变量:
bash
echo $PATH # 输出 PATH 环境变量
echo $HOME # 输出当前用户家目录
echo $USER # 输出当前用户名
echo $SHELL # 输出当前 Shell 路径
自定义变量:
bash
app_name="blog-service" # 定义变量 app_name
version="1.0" # 定义变量 version
注意:
-
等号两边不能有空格
-
值有空格时用引号
-
变量默认按字符串处理
-
推荐用
${变量名}引用
只读变量:
bash
readonly env="prod" # 定义只读变量 env,不可修改
撤销变量:
bash
unset version # 删除变量 version
导出环境变量:
bash
export APP_ENV="production" # 将 APP_ENV 导出为环境变量
2. 特殊位置变量
这些变量是脚本运行时自动填充的,写通用脚本的时候特别好用:
| 变量 | 作用 |
|---|---|
$0 |
脚本自己的文件名 |
$1~$9 |
执行脚本时传的第 1 到第 9 个参数 |
$# |
一共传了多少个参数 |
$* |
所有参数,当成一个整体字符串 |
$@ |
所有参数,每个都是独立的 |
$? |
上一条命令的退出状态码,0 是成功,非 0 是失败 |
这里$*和$@的区别在循环里特别明显:加双引号的时候,"$*"是把所有参数拼成一个字符串,循环只跑一次;"$@"是每个参数独立,有几个参数跑几次循环。
示例:
bash
#!/usr/bin/env bash # 指定 bash 解释器
echo "脚本名:$0" # 输出脚本名称
echo "第一个参数:$1" # 输出第一个参数
echo "参数个数:$#" # 输出参数个数
echo "全部参数:$@" # 输出所有参数
$* 和 $@ 在加双引号时区别明显:
bash
for arg in "$@"; do # 遍历每个独立参数
echo "arg: $arg" # 输出当前参数
done # 循环结束
三、运算、判断与流程控制
1. 算术运算
因为变量默认是字符串,做数学计算要用专门的语法:$((表达式))或者$[表达式],比如sum=$((1+2*3)),注意 Shell 默认只支持整数运算。
bash
a=$((3 + 5)) # 计算 3+5,结果赋给 a
b=$[10 * 2] # 计算 10*2,结果赋给 b
echo $a # 输出 a
echo $b # 输出 b
Shell 原生一般只支持整数运算。浮点计算可用 bc 或 awk:
bash
echo "3.14 * 2" | bc # 用 bc 计算浮点乘法
awk 'BEGIN {print 3.14 * 2}' # 用 awk 计算浮点乘法
2. 条件判断
判断用[ 条件 ],括号两边必须有空格,常用的判断:
- 整数比较:不能直接用大于小于,要用
-eq等于、-ne不等于、-gt大于、-lt小于 - 文件判断:
-e文件存在、-f是普通文件、-d是目录、-r有读权限、-x有执行权限
示例:
bash
if [ -f "/data/app/app.conf" ]; then # 判断文件是否存在且为普通文件
echo "配置文件存在" # 条件成立时输出
fi # 结束 if
if [ "$1" -gt 10 ]; then # 判断第一个参数是否大于 10
echo "参数大于 10" # 条件成立时输出
fi # 结束 if
逻辑组合:
bash
[ -f "app.log" ] && echo "日志存在" # 文件存在则输出提示
[ -d "backup" ] || mkdir backup # 目录不存在则创建 backup
3. 流程控制
if分支:if [ 条件 ]; then ... elif [ 条件 ]; then ... else ... fi
bash
if [ "$1" = "start" ]; then # 如果第一个参数是 start
echo "启动服务" # 输出启动服务
elif [ "$1" = "stop" ]; then # 否则如果第一个参数是 stop
echo "停止服务" # 输出停止服务
else # 其他情况
echo "用法:$0 {start|stop}" # 输出用法提示
fi # 结束 if
case多分支:适合一个变量有多个固定取值的场景,比堆 if 简洁,每个分支用;;结束,相当于 break
bash
case "$1" in # 根据第一个参数匹配
start) # 匹配 start
echo "启动服务" # 输出启动服务
;; # 分支结束
stop) # 匹配 stop
echo "停止服务" # 输出停止服务
;; # 分支结束
restart) # 匹配 restart
echo "重启服务" # 输出重启服务
;; # 分支结束
*) # 默认分支
echo "用法:$0 {start|stop|restart}" # 输出用法
;; # 分支结束
esac # 结束 case
for循环:支持 C 语言风格for((i=1;i<=100;i++)),也支持for i in 列表遍历集合
bash
for name in tom jerry lucy; do # 遍历列表中的每个名字
echo "hello $name" # 输出问候语
done # 循环结束
sum=0 # 初始化 sum 为 0
for ((i = 1; i <= 100; i++)); do # 从 1 循环到 100
sum=$(($sum + $i)) # 累加 i 到 sum
done # 循环结束
echo "$sum" # 输出累加结果
while循环:while [ 条件 ]; do ... done,条件成立就一直循环
bash
i=1 # 初始化 i 为 1
sum=0 # 初始化 sum 为 0
while [ "$i" -le 100 ]; do # 当 i 小于等于 100 时循环
sum=$(($sum + $i)) # 累加 i 到 sum
i=$((i + 1)) # i 自增 1
done # 循环结束
echo "$sum" # 输出累加结果
read -p "提示文字" 变量名:脚本运行的时候接收用户从终端输入的内容
bash
while read -r line; do # 逐行读取文件内容
echo "读取到:$line" # 输出当前行
done < app.log # 从 app.log 读取
4. 自定义函数
把重复的逻辑封装成函数,提升复用性:
bash
# 函数定义
function add() {
echo $(( $1 + $2 ))
}
# 调用函数,参数直接跟在后面
result=$(add 1 2)
bash
log_info() { # 定义 log_info 函数
echo "[INFO] $(date '+%F %T') $*" # 输出带时间戳的 INFO 日志
} # 函数结束
log_error() { # 定义 log_error 函数
echo "[ERROR] $(date '+%F %T') $*" >&2 # 输出带时间戳的 ERROR 日志到标准错误
} # 函数结束
add() { # 定义 add 函数
local a=$1 # 第一个参数赋给局部变量 a
local b=$2 # 第二个参数赋给局部变量 b
echo $(($a + $b)) # 输出两数之和
} # 函数结束
log_info "开始执行" # 调用 log_info
log_error "出现异常" # 调用 log_error
add 3 5 # 调用 add,传入 3 和 5
说明:
-
函数必须先定义后调用
-
local定义局部变量 -
return只能返回 0~255 的整数 -
复杂返回值可以用
echo输出,再由调用方捕获
四、文本处理两大利器
1. cut:简单列切割
cut 就是按分隔符切列,适合格式规整的文本:cut -d "分隔符" -f 列号,比如echo $PATH | cut -d ":" -f 3-就是取 PATH 里从第 3 个冒号开始的所有路径。缺点是分隔符只能是单个字符,多个连续空格就处理不好。
按列提取:
bash
cut -d ":" -f 1,7 /etc/passwd # 以冒号分隔,提取第 1 和第 7 列
echo "$PATH" | cut -d ":" -f 3- # 以冒号分隔,提取第 3 列及之后所有列
2. awk:强大的文本分析工具
awk 比 cut 灵活得多,它逐行读文本,自动按空格切分多列,$0是整行,$1是第一列,$2是第二列,还支持正则匹配、统计计算。 比如提取 passwd 文件里 root 用户的 Shell:
bash
awk -F : '/^root/{print $7}' /etc/passwd
这里-F :指定用冒号当分隔符,/^root/是正则匹配以 root 开头的行,{print $7}打印第 7 列。 还支持BEGIN块(处理文本前先执行,比如打印表头)和END块(所有行处理完执行,比如统计总数),是处理日志、统计数据的神器。
基本结构:
bash
awk 'pattern {action}' file # awk 基本语法:匹配模式后执行动作
示例:
bash
awk -F ":" '{print $1, $7}' /etc/passwd # 以冒号分隔,打印第 1 和第 7 列
awk -F ":" '$3 >= 1000 {print $1, $3}' /etc/passwd # 打印 UID 大于等于 1000 的用户名和 UID
awk '{sum += $2} END {print sum}' sales.txt # 对第二列求和并输出
内置变量:
| 变量 | 含义 |
|---|---|
NR |
行号 |
NF |
字段数 |
FILENAME |
文件名 |
$0 |
整行 |
$1 |
第一列 |
添加表头表尾:
bash
awk -F ":" ' # 以冒号分隔
BEGIN {print "user,shell"} # 处理前打印表头
{print $1 "," $7} # 每行打印第 1 和第 7 列,逗号分隔
END {print "done"} # 处理后打印 done
' /etc/passwd # 输入文件
五、正则表达式入门
grep、awk 这些工具都支持正则表达式来匹配文本,记住几个基础元字符就够日常用了:
^匹配行开头,$匹配行结尾.匹配任意一个字符*匹配前面的字符 0 次或多次,.*就是匹配任意内容[]匹配括号里的任意一个字符,比如[0-9]匹配数字,[a-z]匹配小写字母
日常最常用的就是配合 grep 过滤日志,比如grep "ERROR" app.log找错误日志。
常用元字符:
| 符号 | 含义 |
|---|---|
^ |
行首 |
$ |
行尾 |
. |
任意单字符 |
* |
前一个字符 0 次或多次 |
[] |
字符集合 |
[^] |
排除 |
\ |
转义 |
+ |
1 次或多次,扩展正则 |
? |
0 次或 1 次,扩展正则 |
{n,m} |
重复次数范围 |
示例:
bash
grep -E "^root" /etc/passwd # 匹配以 root 开头的行
grep -E "bash$" /etc/passwd # 匹配以 bash 结尾的行
grep -E "r.t" /etc/passwd # 匹配 r 和 t 之间有一个任意字符的行
grep -E "ro*t" /etc/passwd # 匹配 r 后跟 0 个或多个 o,再跟 t
grep -E "[0-9]" /etc/passwd # 匹配包含任意数字的行
注意: 上面几个例子使用的是基本正则表达式,可以不加
-E。但如果使用了+、?、{}、|等扩展元字符,则必须使用grep -E,或者加上反斜杠进行转义(如\+、\|、\{\})
简单邮箱匹配:
bash
grep -E "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" emails.txt # 匹配邮箱格式
简单手机号匹配:
bash
grep -E "^1[3-9][0-9]{9}$" phones.txt # 匹配 11 位手机号
六、综合案例:日志统计与服务巡检
1. 统计访问日志 Top 10 IP
bash
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -n 10
# 提取第一列 IP,排序,去重计数,按次数倒序,取前 10
-
awk提取:把我要找的东西(比如 IP)抠出来。 -
sort排队:按字母顺序排队,让同一个 IP 站在一起。 -
uniq -c报数:让挨在一起的 IP 报个数,记录每个 IP 出现了几次。 -
sort -nr排名:按照"报数"的数值大小,从高到低排好队。 -
head颁奖:取排在最前面的前几名。
2. 统计状态码
bash
awk '{print $9}' access.log | sort | uniq -c | sort -nr
# 提取第九列状态码,排序,去重计数,按次数倒序
3. 服务健康检查
bash
#!/usr/bin/env bash # 指定 bash 解释器
check_url() { # 定义检查 URL 的函数
local url=$1 # 第一个参数为 URL
if curl -fsS --max-time 5 "$url" >/dev/null; then # 静默请求 URL,超时 5 秒
echo "OK $url" # 成功则输出 OK
else # 否则
echo "FAIL $url" # 输出 FAIL
fi # 结束 if
} # 函数结束
for url in \ # 遍历 URL 列表
"http://127.0.0.1:8080/health" \ # 第一个健康检查地址
"http://127.0.0.1:9090/health" # 第二个健康检查地址
do # 循环开始
check_url "$url" # 调用检查函数
done # 循环结束
在实际写脚本做健康检查时,我们只关心接口是返回 200 还是 500,根本不关心它吐出来的是什么数据。把
curl的结果重定向到/dev/null,就是给命令装个消音器,保持终端整洁。配合-f参数,只要接口异常,curl就会自动报错退出,这样if就能精准捕捉到故障并触发告警了。
-f(fail) :当 HTTP 状态码是 4xx 或 5xx 时,curl会直接报错退出(返回非0状态码),而不是把错误页面的 HTML 打印出来。(这在健康检查中极其关键,如果没有-f,即使服务器返回了 500 错误,curl也会认为请求成功并返回 0)。
-s(silent):静默模式,不显示进度条和错误信息。
-S(show-error) :配合-s使用,表示在静默模式下,如果发生错误,仍然把错误信息显示出来。
--max-time 5:设置超时时间为 5 秒。防止目标服务卡死,导致curl一直挂在那里不返回,拖垮整个巡检脚本。
4. 磁盘巡检
bash
#!/usr/bin/env bash # 指定 bash 解释器
usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%') # 获取根分区使用率并去掉 %
if [ "$usage" -ge 90 ]; then # 如果使用率大于等于 90
echo "警告:根分区使用率 ${usage}%" # 输出警告
elif [ "$usage" -ge 70 ]; then # 否则如果大于等于 70
echo "注意:根分区使用率 ${usage}%" # 输出注意
else # 否则
echo "正常:根分区使用率 ${usage}%" # 输出正常
fi # 结束 if
查出根目录
/的磁盘使用率,并去掉百分号%,只保留纯数字(比如把40%变成40)。之所以要去掉
%,是因为在 Shell 脚本里,if只能对纯整数 做大小比较(比如[ "$usage" -ge 90 ])。如果带着%,Shell 会报错(类似于"期待整数,却得到了字符串")。我们把这一行拆解开来,一步步看它的数据是怎么变化的:
1.
df /(查磁盘)
作用 :查看根目录
/的磁盘使用情况。执行结果:它会输出一个带表头的文本表,像这样:
text
Filesystem 1K-blocks Used Available Use% Mounted on /dev/sda1 51200000 20000000 31200000 40% /2.
| awk 'NR==2 {print $5}'(抓取第五列)
管道符
|:把df /的输出结果,像流水线一样递给awk。
NR==2:NR是 awk 的内置变量,代表行号(Number of Record)。NR==2意思是只处理第二行(因为第一行是表头,第二行才是真实数据)。
$5:代表当前行的第五列(默认按空格分隔)。执行结果 :它精准地提取出了第二行的第五列,即
40%。3.
| tr -d '%'(删掉百分号)
tr:字符转换或删除工具(translate)。
-d:代表删除(delete)。
'%':指定要删除的字符。执行结果 :把
40%里面的%强行删掉,输出纯数字40。4.
usage=$(...)(变量赋值)
$(...):这是 Shell 的命令替换 语法。它的作用是:把括号里面那条长长的流水线跑完,然后把最终的输出结果(比如40)赋值给前面的变量usage。执行结果 :此时,变量
$usage的值就是40。
七、小结
Shell 脚本的核心是:变量、条件、循环、函数,再加上文本处理三件套 grep、cut、awk。写脚本时建议:
-
用
bash -x script.sh调试 -
用
shellcheck检查语法和常见坑 -
变量加双引号,避免空格问题
-
脚本中尽量使用绝对路径
-
重要操作前先
echo确认,再执行
从 Linux 目录结构、基础命令、系统管理,一直到 Shell 脚本自动化,把整个大模型开发环境需要的 Linux 基本功全部过了一遍。这些内容不需要死记硬背,多在实际环境里敲几次、写两个小脚本(比如自动清理旧日志、批量备份文件)自然就熟练了,这也是我自己学习过程中的一点沉淀。