Linux Shell 脚本编程实战:从 bash 语法到运维自动化

系统讲解 Linux Shell 脚本编程:bash 语法基础(变量/引号/参数)、条件与循环控制、函数与作用域、正则与文本处理三剑客(grep/sed/awk)、脚本调试与错误处理,以及一个完整的运维自动化脚本实战。

引言

重复的命令敲三遍就该写脚本了。Shell 是 Linux 运维与开发自动化最快的入口——它能把文件处理、服务管理、日志分析、定时任务编织成可复用的工具。然而「能跑」与「健壮」之间,隔着引号、退出码、变量展开和文本处理这些基础功。

本文从 bash 语法核心讲起,覆盖变量与引号、条件与循环、函数、正则三剑客(grep/sed/awk),再到调试与错误处理,最后用一个真实的运维自动化脚本串联全部知识点。

前置:熟悉基本命令行。与 https://plumephp.com/linux-process-management/、https://plumephp.com/linux-filesystem-disk/ 配合效果最佳。


目录


1. 脚本基础与 shebang

1.1 第一个脚本

#!/usr/bin/env bash
# 上面的 shebang 告诉系统用 bash 解释执行
echo "Hello, Shell"
# 赋予执行权限并运行
chmod +x hello.sh
./hello.sh

1.2 为什么用 env bash

#!/bin/bash          -> 写死路径,某些系统 bash 不在 /bin
#!/usr/bin/env bash  -> 从 PATH 找 bash,更可移植

1.3 set 安全选项(强烈推荐开头加上)

#!/usr/bin/env bash
set -euo pipefail
# -e   遇错误即退出
# -u   使用未定义变量报错
# -o pipefail  管道任一命令失败则整体失败

2. 变量、引号与参数

2.1 变量定义与使用

name="world"
echo "Hello, $name"
echo "长度: ${#name}"

# 命令替换(两种写法)
now=$(date +%Y%m%d)
now2=`date +%Y%m%d`

2.2 引号的区别(最容易踩坑)

var="hello world"

echo $var        # 变量被分词 -> hello world(拆成两个词)
echo "$var"      # 保留为整体 -> hello world
echo '$var'      # 单引号字面量 -> $var

铁律:引用变量几乎总是用双引号 "$var",尤其是文件名可能含空格时。

2.3 位置参数

./script.sh a b c
# $0 = script.sh, $1 = a, $2 = b, $3 = c
# $# = 参数个数, $@ = 所有参数, $? = 上条命令退出码

echo "共 $# 个参数: $@"

3. 条件判断与分支

3.1 test 与 if

if [ -f "/etc/passwd" ]; then
  echo "文件存在"
elif [ -d "/etc" ]; then
  echo "是目录"
else
  echo "都不是"
fi

3.2 常用测试操作符

操作符含义示例
-f是普通文件[ -f file ]
-d是目录[ -d dir ]
-e存在(任意类型)[ -e path ]
-z字符串为空[ -z "$var" ]
-n字符串非空[ -n "$var" ]
-eq数字相等[ "$a" -eq "$b" ]
-lt数字小于[ "$a" -lt "$b" ]
=字符串相等[ "$a" = "$b" ]

3.3 case 分支

case "$1" in
  start)
    echo "启动";;
  stop|restart)
    echo "停止/重启";;
  *)
    echo "未知参数"; exit 1;;
esac

4. 循环:for/while/until

4.1 for 循环

# 遍历列表
for f in /var/log/*.log; do
  echo "处理 $f"
done

# C 风格
for ((i=1; i<=10; i++)); do
  echo $i
done

# 命令输出遍历
for ip in $(cat hosts.txt); do
  ping -c1 "$ip" > /dev/null && echo "$ip OK"
done

4.2 while 循环

# 逐行读取文件
while IFS= read -r line; do
  echo "行: $line"
done < /etc/hosts

# 计数
count=0
while [ "$count" -lt 5 ]; do
  echo $count
  count=$((count + 1))
done

4.3 循环控制

continue   # 跳过本次
break      # 跳出循环

5. 函数与返回值

5.1 定义与调用

log() {
  local level="${1:-INFO}"   # local 局部变量
  echo "[$level] $(date) $2"
}

log INFO "系统启动"
log ERROR "磁盘告警"

5.2 返回值

is_file() {
  [ -f "$1" ] && return 0 || return 1
}

if is_file "/etc/passwd"; then
  echo "是文件"
fi

# 注意:函数的 return 只能返回 0-255 的退出码
# 想返回字符串用 echo + 命令替换

5.3 函数库文件

# lib.sh
say_hi() { echo "Hi, $1"; }

# main.sh
source ./lib.sh   # 或 . ./lib.sh
say_hi "Alice"

6. 正则表达式基础

6.1 基础元字符

元字符含义
.任意单个字符
*前一个字符出现 0 次或多次
+前一个字符出现 1 次或多次
^行首
$行尾
[a-z]字符集
\d / \w数字 / 单词字符
\{n\} / {n}恰好 n 次

6.2 grep 中使用

# 找 IP 地址
grep -Eo '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log

# 找邮箱
grep -E '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' file

# 统计次数
grep -c 'ERROR' app.log

6.3 贪婪 vs 惰性

.* 贪婪:尽量多匹配
.*? 惰性:尽量少匹配(需要 -P 的 Perl 正则)

7. 文本三剑客:grep/sed/awk

7.1 grep:筛选

grep -i 'error' app.log          # 忽略大小写
grep -v 'DEBUG' app.log          # 排除匹配
grep -A3 -B2 'panic' app.log     # 上下文 3 行后/2 行前
grep -r 'TODO' src/ --include='*.py'   # 递归

7.2 sed:流编辑

# 替换(注意:默认不改原文件)
sed 's/foo/bar/g' file           # g=全局替换
sed -i 's/foo/bar/g' file        # 直接改文件

# 删除行
sed '/^#/d' config.conf          # 删除注释行

# 提取行范围
sed -n '10,20p' file             # 打印 10-20 行

7.3 awk:结构化处理

# 列操作(默认按空白分列)
awk '{print $1, $3}' file

# 带条件的列过滤
awk '$3 > 100 {print $1, $3}' stats.log

# 统计求和
awk '{sum += $3} END {print "总和:", sum}' file

# 自定义分隔符
awk -F: '{print $1, $7}' /etc/passwd

# 格式化输出
awk '{printf "%-10s %5d\n", $1, $2}' file

7.4 三剑客分工

工具定位典型场景
grep找行过滤/计数
sed改流替换/删除
awk算列统计/格式化/报表

8. 调试与错误处理

8.1 调试手段

bash -x script.sh      # 跟踪执行,显示每一步
bash -n script.sh      # 语法检查(不执行)
set -x                 # 脚本内开启跟踪
set +x                 # 关闭

8.2 错误处理

# 检查上条命令
if ! command -v docker > /dev/null; then
  echo "docker 未安装"; exit 1
fi

# 关键命令失败即退出
set -e

# 自定义 trap 清理
trap 'echo "异常退出,清理中"; rm -f /tmp/tmpfile' EXIT

8.3 日志与健壮性

log() {
  echo "$(date '+%F %T') [$1] $2" | tee -a /var/log/myapp.log
}

# 使用临时文件加 trap 清理
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

9. 实战:日志清理与备份脚本

9.1 完整脚本

#!/usr/bin/env bash
# 自动清理 N 天前的日志并备份到指定目录
set -euo pipefail

# 配置
LOG_DIR="/var/log/myapp"
BACKUP_DIR="/backup/logs"
RETENTION_DAYS=7

log() {
  echo "$(date '+%F %T') [$1] $2"
}

# 检查目录
[ -d "$LOG_DIR" ] || { log ERROR "日志目录不存在"; exit 1; }

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 备份最近 1 天内的日志(保留存档),清理更旧的
today=$(date +%F)
find "$LOG_DIR" -name "*.log" -mtime -1 -exec cp -p {} "$BACKUP_DIR/''$today''_" \; 2>/dev/null || true

# 清理超过保留期的日志
deleted=$(find "$LOG_DIR" -name "*.log" -mtime +"$RETENTION_DAYS" -delete -print | wc -l)
log INFO "清理了 $deleted 个过期日志文件"

# 备份目录同样只保留 30 天
find "$BACKUP_DIR" -name "*.log" -mtime +30 -delete 2>/dev/null || true

log INFO "清理完成"

9.2 配合 crontab 定时执行

crontab -e
# 每天凌晨 2:30 执行
30 2 * * * /opt/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1

9.3 脚本复用思路

1. 配置变量抽到顶部 -> 改配置不改逻辑
2. 关键操作加日志   -> 出问题可追溯
3. set -euo pipefail -> 失败早暴露
4. 路径加引号        -> 防空格

延伸阅读

  • https://plumephp.com/linux-process-management/ — 服务管理脚本化
  • https://plumephp.com/linux-filesystem-disk/ — 磁盘清理脚本实战
  • https://plumephp.com/linux-user-permissions/ — 脚本权限与 sudo 配合
  • Bash 官方参考手册 — 语法完整参考
  • ShellCheck — 脚本静态检查工具

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. Linux 高级文件系统:XFS、Btrfs、ZFS 与存储进阶
  2. Linux 高可用与负载均衡:HAProxy、Keepalived 与集群方案
  3. Linux 防火墙与 nftables:规则集、链、NAT 与网络安全防护