☰
从零开始学 Linux(二):Shell 脚本入门与文本处理实战
2026/10/19 5:02:30 网站建设 项目流程

前言

前篇都是手动敲单条命令,当要重复执行几十上百条操作的时候,一条条敲效率太低了。这时候就可以把命令按照逻辑写成 Shell 脚本,一次性批量执行,实现自动化。Shell 脚本不需要编译,直接解释运行,是运维自动化最常用的工具。

一、脚本入门:怎么写和执行脚本

Shell 脚本本质就是一个文本文件,第一行必须用#!/bin/bash指定用哪个解释器来执行它,后面一行一条 Linux 命令就行。 比如最简单的脚本:

#!/bin/bash echo "Hello, Shell!"

执行脚本有两种方式,原理不一样:

  1. 直接用解释器调用:bash ./test.sh,这种方式不需要脚本有执行权限,本质是 bash 程序帮你读这个脚本执行
  2. 给脚本加执行权限之后直接跑: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 # 输出 b

Shell 原生一般只支持整数运算。浮点计算可用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 # 目录不存在则创建 backup

3. 流程控制

  • 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 # 结束 if
  • case多分支:适合一个变量有多个固定取值的场景,比堆 if 简洁,每个分支用;;结束,相当于 break
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 列表遍历集合
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,排序,去重计数,按次数倒序,取前 10
  1. awk提取:把我要找的东西(比如 IP)抠出来。

  2. sort排队:按字母顺序排队,让同一个 IP 站在一起。

  3. uniq -c报数:让挨在一起的 IP 报个数,记录每个 IP 出现了几次。

  4. sort -nr排名:按照“报数”的数值大小,从高到低排好队。

  5. 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 基本功全部过了一遍。这些内容不需要死记硬背,多在实际环境里敲几次、写两个小脚本(比如自动清理旧日志、批量备份文件)自然就熟练了,这也是我自己学习过程中的一点沉淀。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询