前言
前篇都是手动敲单条命令,当要重复执行几十上百条操作的时候,一条条敲效率太低了。这时候就可以把命令按照逻辑写成 Shell 脚本,一次性批量执行,实现自动化。Shell 脚本不需要编译,直接解释运行,是运维自动化最常用的工具。
一、脚本入门:怎么写和执行脚本
Shell 脚本本质就是一个文本文件,第一行必须用#!/bin/bash指定用哪个解释器来执行它,后面一行一条 Linux 命令就行。 比如最简单的脚本:
#!/bin/bash echo "Hello, Shell!"执行脚本有两种方式,原理不一样:
- 直接用解释器调用:
bash ./test.sh,这种方式不需要脚本有执行权限,本质是 bash 程序帮你读这个脚本执行 - 给脚本加执行权限之后直接跑:
chmod +x test.sh,然后./test.sh,这种方式脚本自己就是可执行程序,系统会根据第一行的解释器路径来执行
保存为hello.sh,运行方式:
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 变量名可以把变量升级成全局环境变量,这样子脚本也能读到这个变量,注意是父进程传子进程,反过来不行。
系统变量:
echo $PATH # 输出 PATH 环境变量 echo $HOME # 输出当前用户家目录 echo $USER # 输出当前用户名 echo $SHELL # 输出当前 Shell 路径自定义变量:
app_name="blog-service" # 定义变量 app_name version="1.0" # 定义变量 version注意:
等号两边不能有空格
值有空格时用引号
变量默认按字符串处理
推荐用
${变量名}引用
只读变量:
readonly env="prod" # 定义只读变量 env,不可修改撤销变量:
unset version # 删除变量 version导出环境变量:
export APP_ENV="production" # 将 APP_ENV 导出为环境变量2. 特殊位置变量
这些变量是脚本运行时自动填充的,写通用脚本的时候特别好用:
| 变量 | 作用 |
|---|---|
$0 | 脚本自己的文件名 |
$1~$9 | 执行脚本时传的第 1 到第 9 个参数 |
$# | 一共传了多少个参数 |
$* | 所有参数,当成一个整体字符串 |
$@ | 所有参数,每个都是独立的 |
$? | 上一条命令的退出状态码,0 是成功,非 0 是失败 |
这里$*和$@的区别在循环里特别明显:加双引号的时候,"$*"是把所有参数拼成一个字符串,循环只跑一次;"$@"是每个参数独立,有几个参数跑几次循环。
示例:
#!/usr/bin/env bash # 指定 bash 解释器 echo "脚本名:$0" # 输出脚本名称 echo "第一个参数:$1" # 输出第一个参数 echo "参数个数:$#" # 输出参数个数 echo "全部参数:$@" # 输出所有参数$*和$@在加双引号时区别明显:
for arg in "$@"; do # 遍历每个独立参数 echo "arg: $arg" # 输出当前参数 done # 循环结束三、运算、判断与流程控制
1. 算术运算
因为变量默认是字符串,做数学计算要用专门的语法:$((表达式))或者$[表达式],比如sum=$((1+2*3)),注意 Shell 默认只支持整数运算。
a=$((3 + 5)) # 计算 3+5,结果赋给 a b=$[10 * 2] # 计算 10*2,结果赋给 b echo $a # 输出 a echo $b # 输出 bShell 原生一般只支持整数运算。浮点计算可用bc或awk:
echo "3.14 * 2" | bc # 用 bc 计算浮点乘法 awk 'BEGIN {print 3.14 * 2}' # 用 awk 计算浮点乘法2. 条件判断
判断用[ 条件 ],括号两边必须有空格,常用的判断:
- 整数比较:不能直接用大于小于,要用
-eq等于、-ne不等于、-gt大于、-lt小于 - 文件判断:
-e文件存在、-f是普通文件、-d是目录、-r有读权限、-x有执行权限
示例:
if [ -f "/data/app/app.conf" ]; then # 判断文件是否存在且为普通文件 echo "配置文件存在" # 条件成立时输出 fi # 结束 if if [ "$1" -gt 10 ]; then # 判断第一个参数是否大于 10 echo "参数大于 10" # 条件成立时输出 fi # 结束 if逻辑组合:
[ -f "app.log" ] && echo "日志存在" # 文件存在则输出提示 [ -d "backup" ] || mkdir backup # 目录不存在则创建 backup3. 流程控制
if分支:if [ 条件 ]; then ... elif [ 条件 ]; then ... else ... fi
if [ "$1" = "start" ]; then # 如果第一个参数是 start echo "启动服务" # 输出启动服务 elif [ "$1" = "stop" ]; then # 否则如果第一个参数是 stop echo "停止服务" # 输出停止服务 else # 其他情况 echo "用法:$0 {start|stop}" # 输出用法提示 fi # 结束 ifcase多分支:适合一个变量有多个固定取值的场景,比堆 if 简洁,每个分支用;;结束,相当于 break
case "$1" in # 根据第一个参数匹配 start) # 匹配 start echo "启动服务" # 输出启动服务 ;; # 分支结束 stop) # 匹配 stop echo "停止服务" # 输出停止服务 ;; # 分支结束 restart) # 匹配 restart echo "重启服务" # 输出重启服务 ;; # 分支结束 *) # 默认分支 echo "用法:$0 {start|stop|restart}" # 输出用法 ;; # 分支结束 esac # 结束 casefor循环:支持 C 语言风格for((i=1;i<=100;i++)),也支持for i in 列表遍历集合
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,条件成立就一直循环
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 "提示文字" 变量名:脚本运行的时候接收用户从终端输入的内容
while read -r line; do # 逐行读取文件内容 echo "读取到:$line" # 输出当前行 done < app.log # 从 app.log 读取4. 自定义函数
把重复的逻辑封装成函数,提升复用性:
# 函数定义 function add() { echo $(( $1 + $2 )) } # 调用函数,参数直接跟在后面 result=$(add 1 2)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 个冒号开始的所有路径。缺点是分隔符只能是单个字符,多个连续空格就处理不好。
按列提取:
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:
awk -F : '/^root/{print $7}' /etc/passwd这里-F :指定用冒号当分隔符,/^root/是正则匹配以 root 开头的行,{print $7}打印第 7 列。 还支持BEGIN块(处理文本前先执行,比如打印表头)和END块(所有行处理完执行,比如统计总数),是处理日志、统计数据的神器。
基本结构:
awk 'pattern {action}' file # awk 基本语法:匹配模式后执行动作示例:
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 | 第一列 |
添加表头表尾:
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} | 重复次数范围 |
示例:
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,或者加上反斜杠进行转义(如\+、\|、\{\})
简单邮箱匹配:
grep -E "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" emails.txt # 匹配邮箱格式简单手机号匹配:
grep -E "^1[3-9][0-9]{9}$" phones.txt # 匹配 11 位手机号六、综合案例:日志统计与服务巡检
1. 统计访问日志 Top 10 IP
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -n 10 # 提取第一列 IP,排序,去重计数,按次数倒序,取前 10awk提取:把我要找的东西(比如 IP)抠出来。sort排队:按字母顺序排队,让同一个 IP 站在一起。uniq -c报数:让挨在一起的 IP 报个数,记录每个 IP 出现了几次。sort -nr排名:按照“报数”的数值大小,从高到低排好队。head颁奖:取排在最前面的前几名。
2. 统计状态码
awk '{print $9}' access.log | sort | uniq -c | sort -nr # 提取第九列状态码,排序,去重计数,按次数倒序3. 服务健康检查
#!/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. 磁盘巡检
#!/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 基本功全部过了一遍。这些内容不需要死记硬背,多在实际环境里敲几次、写两个小脚本(比如自动清理旧日志、批量备份文件)自然就熟练了,这也是我自己学习过程中的一点沉淀。