前言

Shell 脚本是将一系列 Linux 命令组合在一起自动执行的程序。它能帮助你自动化备份、部署、日志处理等日常运维任务,是每个后端开发者和运维工程师的必备技能。

本文基于 bash,这是 Linux 上最通用的 Shell。


一、第一个 Shell 脚本

1
2
3
4
#!/bin/bash
# 这是我的第一个脚本
echo "Hello, 七月小站!"
echo "当前时间: $(date)"

解释:

代码 含义
#!/bin/bash Shebang,指定解释器
# 注释 # 开头的行为注释
echo 输出文本
$(command) 命令替换,执行命令并获取输出

运行脚本:

1
2
chmod +x hello.sh    # 赋予执行权限
./hello.sh # 运行

二、变量

2.1 定义与使用

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
name="July"
age=25
readonly PI=3.14159 # 只读变量

echo "我是${name},年龄${age}"
echo "π ≈ $PI"

# 删除变量
unset age

⚠️ 注意= 两边不能有空格

2.2 特殊变量

1
2
3
4
5
6
7
8
#!/bin/bash
echo "脚本名: $0"
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "参数个数: $#"
echo "所有参数: $@"
echo "上一条命令的退出状态: $?"
echo "当前进程 PID: $$"
1
2
3
4
5
6
./test.sh hello world
# 脚本名: ./test.sh
# 第1个参数: hello
# 第2个参数: world
# 参数个数: 2
# 所有参数: hello world

2.3 读取用户输入

1
2
3
4
5
6
7
8
9
#!/bin/bash
echo -n "请输入名字: "
read name
echo "你好, $name!"

# 带提示的一行写法
read -p "年龄: " age
read -sp "密码: " password # -s 隐藏输入
echo # 换行

三、字符串操作

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
str="Hello, Bash!"

echo "长度: ${#str}" # 12
echo "子串: ${str:0:5}" # Hello
echo "替换: ${str/Bash/World}" # Hello, World!
echo "是否包含: $(echo $str | grep -q 'Bash' && echo yes || echo no)"

# 拼接
a="Hello"
b="World"
c="$a, $b!"
echo $c # Hello, World!

四、数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# 定义数组
fruits=("苹果" "香蕉" "橘子" "草莓")

# 访问
echo ${fruits[0]} # 苹果
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度

# 遍历
for fruit in "${fruits[@]}"; do
echo "$fruit"
done

# 增删
fruits+=("葡萄") # 追加
unset fruits[1] # 删除索引 1

五、条件判断

5.1 if 语句

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
score=85

if [ $score -ge 90 ]; then
echo "优秀"
elif [ $score -ge 80 ]; then
echo "良好"
elif [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi

5.2 常用判断条件

数值比较:

运算符 含义
-eq 等于
-ne 不等于
-gt 大于
-lt 小于
-ge 大于等于
-le 小于等于

文件判断:

条件 含义
-f file 是普通文件
-d dir 是目录
-e path 路径存在
-r file 可读
-w file 可写
-x file 可执行
-s file 非空
-z "$str" 字符串为空
-n "$str" 字符串非空
"$a" = "$b" 字符串相等
"$a" != "$b" 字符串不等

5.3 逻辑运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# AND
if [ $age -ge 18 ] && [ $age -le 60 ]; then
echo "成年人"
fi

# 等价写法
if [ $age -ge 18 -a $age -le 60 ]; then
echo "成年人"
fi

# OR
if [ "$role" = "admin" ] || [ "$role" = "superadmin" ]; then
echo "管理员"
fi

# NOT
if [ ! -f "config.yml" ]; then
echo "配置文件不存在"
fi

5.4 现代写法 [[ ]](推荐)

1
2
3
4
5
6
7
8
# 支持正则、逻辑运算符更自然
if [[ $name =~ ^J.*y$ ]]; then
echo "名称以 J 开头,y 结尾"
fi

if [[ -f config.yml && -r config.yml ]]; then
echo "配置文件可读"
fi

5.5 case 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
read -p "输入 (start/stop/restart): " action

case $action in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart)
echo "重启服务..."
;;
*)
echo "用法: $0 {start|stop|restart}"
exit 1
;;
esac

六、循环

6.1 for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 遍历列表
for color in red green blue; do
echo "$color"
done

# 遍历数组
files=(*.log)
for file in "${files[@]}"; do
echo "处理: $file"
done

# C 风格
for ((i=0; i<5; i++)); do
echo "第 $i 次"
done

# 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
echo "用户: $user"
done

6.2 while 循环

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
count=0
while [ $count -lt 5 ]; do
echo $count
((count++))
done

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

6.3 break / continue

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in {1..10}; do
if [ $i -eq 5 ]; then
break # 退出循环
fi
echo $i
done

for i in {1..10}; do
if [ $i -eq 5 ]; then
continue # 跳过本次
fi
echo $i
done

七、函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

# 定义函数
greet() {
local name=$1 # local 限定局部变量
echo "你好, $name!"
}

# 带返回值的函数
add() {
return $(($1 + $2)) # return 只能返回 0-255
}

# 通过 echo 返回结果(常用)
multiply() {
echo $(($1 * $2))
}

# 调用
greet "July"
add 3 5
echo "结果: $?" # $? 获取 return 值

result=$(multiply 3 5)
echo "3 × 5 = $result"

八、文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 写入文件
echo "Hello" > output.txt # 覆盖
echo "World" >> output.txt # 追加

# 读取文件
content=$(<output.txt) # 读取全部内容
while IFS= read -r line; do
echo "$line"
done < output.txt

# 检查文件
if [ -f "output.txt" ]; then
echo "文件大小: $(stat -c%s output.txt) 字节"
fi

# 临时文件
tempfile=$(mktemp)
echo "临时数据" > "$tempfile"
# ... 使用 ...
rm "$tempfile"

九、错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
set -euo pipefail # 严格模式(推荐在每个脚本开头添加)

# set -e: 任何命令失败立即退出
# set -u: 使用未定义变量时报错
# set -o pipefail: 管道中任一命令失败都算失败

# 错误处理函数
error_exit() {
echo "错误: $1" >&2
exit 1
}

# 使用
if ! command -v curl &> /dev/null; then
error_exit "curl 未安装"
fi

# 清理函数(脚本退出时执行)
cleanup() {
echo "清理临时文件..."
rm -f /tmp/myapp-*.tmp
}
trap cleanup EXIT

# 跳过某个命令的错误
command_that_might_fail || true

十、实用脚本实战

10.1 自动备份脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
set -euo pipefail

SOURCE_DIR="/var/www/myapp"
BACKUP_DIR="/backup"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/backup_${DATE}.tar.gz"

mkdir -p "$BACKUP_DIR"

echo "[$(date)] 开始备份 $SOURCE_DIR"
tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

echo "备份完成: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"

# 清理旧备份
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "已清理 ${RETENTION_DAYS} 天前的备份"

# 保留最近 5 个备份(备用策略)
# ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +6 | xargs rm -f

10.2 服务健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# 检查服务是否运行,不运行则尝试重启

SERVICE="nginx"
LOG_FILE="/var/log/health_check.log"

log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

if ! systemctl is-active --quiet "$SERVICE"; then
log "⚠ $SERVICE 未运行,尝试重启..."
systemctl restart "$SERVICE"

sleep 3
if systemctl is-active --quiet "$SERVICE"; then
log "✅ $SERVICE 重启成功"
else
log "❌ $SERVICE 重启失败!请检查"
# 可以在这里添加告警通知
fi
else
log "✅ $SERVICE 运行正常"
fi

10.3 批量重命名文件

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# 将当前目录下所有 .jpg 改为 序号.jpg

count=1
for file in *.jpg; do
[ -f "$file" ] || continue # 跳过不存在的文件
mv "$file" "$(printf '%03d' $count).jpg"
echo "$file$(printf '%03d' $count).jpg"
((count++))
done

10.4 日志分析脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# 分析 Nginx 访问日志,统计 Top 10 IP

LOG_FILE="${1:-/var/log/nginx/access.log}"

if [ ! -f "$LOG_FILE" ]; then
echo "日志文件不存在: $LOG_FILE"
exit 1
fi

echo "===== Nginx 访问日志分析 ====="
echo "日志文件: $LOG_FILE"
echo "总请求数: $(wc -l < "$LOG_FILE")"
echo ""

echo "===== Top 10 访问 IP ====="
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10

echo ""
echo "===== 状态码分布 ====="
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn

10.5 项目部署脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
set -euo pipefail

APP_DIR="/var/www/myapp"
GIT_REPO="https://gitee.com/time2017/myapp.git"
BRANCH="master"

echo "===== 开始部署 ====="

cd "$APP_DIR"

# 拉取最新代码
git checkout "$BRANCH"
git pull origin "$BRANCH"

# 安装依赖
npm ci --production

# 重新构建
npm run build

# 重启服务
pm2 restart myapp

echo "===== 部署完成 ====="

十一、定时任务(Cron)

1
2
3
4
5
6
7
8
# 编辑当前用户的定时任务
crontab -e

# 查看定时任务
crontab -l

# 删除所有定时任务
crontab -r

Cron 表达式格式:

1
2
3
4
5
6
7
* * * * * command_to_run
│ │ │ │ │
│ │ │ │ └── 星期 (0-7, 0和7都代表周日)
│ │ │ └──── 月份 (1-12)
│ │ └────── 日期 (1-31)
│ └──────── 小时 (0-23)
└────────── 分钟 (0-59)

示例:

1
2
3
4
5
6
7
8
9
10
11
# 每天凌晨 2 点执行备份
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

# 每小时执行健康检查
0 * * * * /home/user/health_check.sh

# 每周一早上 9 点
0 9 * * 1 /home/user/weekly_report.sh

# 每 5 分钟
*/5 * * * * /home/user/monitor.sh

十二、常用技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 批量创建目录
mkdir -p project/{src/{js,css,img},dist,docs}

# 命令行参数解析
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
echo "帮助信息"
exit 0
;;
-n|--name)
NAME="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
*)
echo "未知参数: $1"
exit 1
;;
esac
done

# 彩色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}成功${NC}: 操作完成"
echo -e "${RED}错误${NC}: 操作失败"
echo -e "${YELLOW}警告${NC}: 请检查配置"

# 显示进度条
for i in {1..100}; do
printf "\r进度: [%-50s] %d%%" $(printf '=%.0s' $(seq 1 $((i/2)))) $i
sleep 0.02
done
echo ""

调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
# 方式1:运行时开启调试
bash -x script.sh

# 方式2:脚本内开启
#!/bin/bash -x
# 或
set -x # 开启调试
set +x # 关闭调试

# 方式3:输出到文件
exec 2>debug.log
set -x

结语

Shell 脚本的核心价值在于自动化重复劳动。学习路径建议:

  1. 先写好单个命令,确认正确
  2. 把命令放进脚本,添加变量
  3. 加上条件判断和错误处理
  4. 加入定时任务实现自动运行

不需要一次记住所有语法——把本文当作速查手册,需要时翻开即可。

把重复的事情交给脚本,把时间留给自己。🐚