从零开始学 Linux(二):Shell 脚本入门与文本处理实战

前言

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

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

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

bash 复制代码
#!/bin/bash
echo "Hello, Shell!"

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

  1. 直接用解释器调用:bash ./test.sh,这种方式不需要脚本有执行权限,本质是 bash 程序帮你读这个脚本执行
  2. 给脚本加执行权限之后直接跑: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
  1. awk 提取:把我要找的东西(比如 IP)抠出来。

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

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

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

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

相关推荐
.道阻且长.1 小时前
Linux软件包管理器
linux·运维·ubuntu·职场和发展·centos
想做小南娘,发现自己是女生喵2 小时前
第二周学习文档 Linux C 文件操作
linux·c语言·学习
元岳数字人小元2 小时前
一台终端兼顾接待与咨询,AI数字人对话赋能线下多场景服务
运维·人工智能·人机交互·交互·语音识别·源代码管理
djjjx.2 小时前
Linux进程篇(三):深度硬核!全面起底 Linux 进程状态变化与内核链表动态解绑
linux·运维·服务器
乔旭伟2 小时前
Linux 服务器环境搭建实战笔记:最小化安装、Docker Compose 编排与上线检查全流程
linux·服务器·笔记
燕落南枝2 小时前
HTML 学习:input 单选、复选与文件域详解
服务器·学习·html
Chenyu_31010 小时前
DPDK、TC 和 XDP:Linux 高性能网络到底在优化什么
linux·服务器·网络·缓存
qqxiaoxingxing10 小时前
HR重复性工作自动化落地指南:先梳理任务边界,再选工具
运维·自动化