引言
重复的命令敲三遍就该写脚本了。Shell 是 Linux 运维与开发自动化最快的入口——它能把文件处理、服务管理、日志分析、定时任务编织成可复用的工具。然而「能跑」与「健壮」之间,隔着引号、退出码、变量展开和文本处理这些基础功。
本文从 bash 语法核心讲起,覆盖变量与引号、条件与循环、函数、正则三剑客(grep/sed/awk),再到调试与错误处理,最后用一个真实的运维自动化脚本串联全部知识点。
前置:熟悉基本命令行。与 https://plumephp.com/linux-process-management/、https://plumephp.com/linux-filesystem-disk/ 配合效果最佳。
目录
- 1. 脚本基础与 shebang
- 2. 变量、引号与参数
- 3. 条件判断与分支
- 4. 循环:for/while/until
- 5. 函数与返回值
- 6. 正则表达式基础
- 7. 文本三剑客:grep/sed/awk
- 8. 调试与错误处理
- 9. 实战:日志清理与备份脚本
- 延伸阅读
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 — 脚本静态检查工具
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。