第1章:什么是 Shell 与 Shell 脚本?
一、Shell 是什么?
- Shell 是一个命令解释器,是你在 Linux 里敲命令的地方。
- 你平时用的命令如
cd
、ls
、echo
,其实都由 Shell 来解析执行。 - 最常见的 Shell 是 Bash,绝大多数 Linux 系统默认都用它。
二、什么是 Shell 脚本?
- Shell 脚本就是一系列 Shell 命令的集合,保存成一个
.sh
文件。 - 本质上:你平时一条一条敲的命令,写成脚本就能一次性自动运行。
- 示例场景:
- 每天定时备份数据库
- 启动一堆服务
- 自动上传文件、发通知等
三、第一个 Shell 脚本实战
步骤1:创建一个脚本文件
vim hello.sh
步骤2:写入下面内容
#!/bin/bash
# 这是我的第一个Shell脚本echo "Hello, Shell!"
第一行
#!/bin/bash
是固定写法,告诉系统用 Bash 来执行脚本。
步骤3:保存退出并赋予执行权限
chmod +x hello.sh
步骤4:运行脚本
./hello.sh
输出应该是:
Hello, Shell!
四、脚本中的几个细节
- 所有行首以
#
开头的是注释,系统会忽略。 - 每一行相当于你在终端执行的一条命令。
- 脚本默认按顺序逐行执行。
五、本节练习
自己创建一个脚本 myinfo.sh
,内容如下:
#!/bin/bashecho "欢迎来到 Shell 脚本世界!"
echo "当前用户:$USER"
echo "当前时间:$(date)"
echo "当前目录:$(pwd)"
运行后会打印你的用户名、当前时间和所在目录。
第2章:变量与运算符
一、变量的定义与使用
1. 定义变量(= 两边不能有空格)
name="张三"
age=25
2. 使用变量:$变量名
或 ${变量名}
echo "姓名:$name"
echo "年龄:${age}"
二、变量赋值的注意事项
a=10 # 正确
a = 10 # 错误,不能有空格
三、特殊变量
变量 | 说明 |
---|---|
$0 | 当前脚本名称 |
$1 ~$9 | 传入脚本的第1~9个参数 |
$@ | 传入的所有参数(可用于循环) |
$# | 参数个数 |
$$ | 当前脚本的进程ID |
$? | 上一条命令的返回值(0成功,非0失败) |
示例:
#!/bin/bash
echo "脚本名:$0"
echo "第一个参数:$1"
echo "参数个数:$#"
运行:
./myscript.sh hello
输出:
脚本名:./myscript.sh
第一个参数:hello
参数个数:1
四、字符串操作
拼接字符串:
first="张"
last="三"
full=$first$last
echo "姓名:$full"
获取字符串长度:
str="abc123"
echo "长度:${#str}" # 输出 6
五、数值运算(整数)
Bash 默认只支持整数,用三种方式:
1. 使用 let
:
let a=5+3
echo $a # 输出 8
2. 使用 expr
:
a=`expr 5 + 3`
echo $a
注意:运算符前后必须有空格。
3. 使用 $((...))
(推荐)
a=$((5 + 3))
echo $a
六、本节小练习
请写一个脚本:userinfo.sh
,功能如下:
#!/bin/bashname="李四"
age=30
year=$((2025 - age))echo "姓名:$name"
echo "年龄:$age"
echo "出生年份:$year"
运行后输出:
姓名:李四
年龄:30
出生年份:1995
第3章:流程控制语句(if / case / for / while)
一、if 判断语句
基本语法:
if [ 条件 ]
then命令1命令2
fi
示例:
age=20if [ $age -ge 18 ]; thenecho "成年人"
elseecho "未成年人"
fi
常用条件判断符号:
条件 | 含义 |
---|---|
-eq | 等于 |
-ne | 不等于 |
-gt | 大于 |
-lt | 小于 |
-ge | 大于等于 |
-le | 小于等于 |
== / != | 字符串比较 |
二、文件判断(常用于运维脚本)
if [ -f "a.txt" ]; thenecho "a.txt 是一个文件"
fiif [ -d "/home" ]; thenecho "/home 是一个目录"
fi
表达式 | 含义 |
---|---|
-f FILE | 是否是普通文件 |
-d DIR | 是否是目录 |
-e FILE | 是否存在 |
-r/-w/-x FILE | 是否可读 / 可写 / 可执行 |
三、case 多分支选择(类似 switch)
read -p "请输入操作命令(start|stop|restart):" cmdcase $cmd instart)echo "正在启动服务...";;stop)echo "正在停止服务...";;restart)echo "正在重启服务...";;*)echo "未知命令";;
esac
四、for 循环
遍历列表:
for name in 张三 李四 王五
doecho "你好,$name"
done
遍历数字:
for i in {1..5}
doecho "第 $i 次"
done
五、while 循环
i=1
while [ $i -le 5 ]
doecho "当前是第 $i 次"i=$((i + 1))
done
六、本节练习题
写一个脚本 score.sh
:
#!/bin/bashread -p "请输入成绩:" scoreif [ $score -ge 90 ]; thenecho "优秀"
elif [ $score -ge 70 ]; thenecho "良好"
elif [ $score -ge 60 ]; thenecho "及格"
elseecho "不及格"
fi
第4章:函数与脚本模块化
在脚本越来越复杂时,把逻辑封装成函数可以让脚本更清晰、复用性更强。
一、函数的定义与调用
语法1:带 function
关键字
function say_hello() {echo "Hello!"
}
语法2:省略 function
say_hello() {echo "Hello!"
}
调用函数:
say_hello
二、函数传参
函数可以像脚本一样,通过 $1
、$2
来接收参数:
greet() {echo "你好,$1!今天是 $2。"
}greet "张三" "星期四"
输出:
你好,张三!今天是 星期四。
三、函数返回值
return
只能返回 整数(状态码),不适合传数据- 可以通过 echo 输出 + 命令替换的方式返回字符串
示例1:返回状态码
check_even() {if [ $(( $1 % 2 )) -eq 0 ]; thenreturn 0elsereturn 1fi
}check_even 4
if [ $? -eq 0 ]; thenecho "偶数"
elseecho "奇数"
fi
示例2:返回字符串
get_name() {echo "张三"
}name=$(get_name)
echo "姓名是:$name"
四、脚本模块化(多个脚本协作)
你可以将常用函数写在一个文件里,用其他脚本通过 source
引入使用。
step1:创建函数库 utils.sh
say_hi() {echo "你好,我是工具函数"
}
step2:主脚本中引入使用
#!/bin/bash
source ./utils.shsay_hi
五、本章练习
写一个函数 calc_sum
,输入两个参数,输出它们的和:
calc_sum() {sum=$(( $1 + $2 ))echo "$1 + $2 = $sum"
}calc_sum 4 6
第5章:文件判断、重定向与文本处理
一、文件存在性与属性判断
1. 判断文件是否存在
if [ -e "file.txt" ]; thenecho "文件存在"
fi
2. 常用文件判断符号
表达式 | 含义 |
---|---|
-e file | 是否存在 |
-f file | 是否是普通文件 |
-d dir | 是否是目录 |
-s file | 文件非空 |
-r file | 是否可读 |
-w file | 是否可写 |
-x file | 是否可执行 |
示例:判断一个目录是否存在,不存在则创建
dir="/data/backup"
if [ ! -d "$dir" ]; thenmkdir -p "$dir"echo "目录已创建:$dir"
elseecho "目录已存在:$dir"
fi
二、输入输出重定向
1. 重定向输出(覆盖 / 追加)
echo "一行内容" > file.txt # 覆盖写入
echo "再来一行" >> file.txt # 追加写入
2. 重定向错误输出
command 2> error.log # 仅错误输出到 error.log
command > out.log 2>&1 # 正常和错误都写入 out.log
三、文本内容处理:核心工具
1. cat
:查看文件内容
cat file.txt
2. head
/ tail
:查看前几行/后几行
head -n 5 file.txt
tail -n 10 file.txt
3. cut
:按列提取文本内容
cat users.csv | cut -d ',' -f 2 # 提取第2列(用,分隔)
4. grep
:关键词搜索
grep "ERROR" /var/log/syslog
5. awk
:按列处理文本,超强工具
awk -F ',' '{print $1, $3}' users.csv
6. sed
:流编辑器,可做替换、删除行等
sed 's/错误/成功/g' log.txt # 全文替换
sed -n '5,10p' file.txt # 显示第5到10行
四、本章综合示例
日志扫描:提取最近系统错误日志
#!/bin/bash
log="/var/log/syslog"grep "ERROR" "$log" | tail -n 20 > recent_errors.log
echo "最近错误已提取到 recent_errors.log"
CSV 提取:获取手机号列表(第3列)
#!/bin/bash
cut -d ',' -f 3 users.csv > phones.txt
echo "手机号已提取完毕"
五、本章练习题
- 判断
/etc/passwd
文件是否存在,如果存在则统计其行数。 - 提取
backup.log
文件中最近的包含关键字fail
的前 10 条记录。 - 将
data.txt
中的所有 “null” 替换为 “空值”。
第6章:Shell 工具组合实战(find、xargs、tar、scp、curl 等)
一、find:查找文件
基本用法
find 路径 [条件] [操作]
常见示例
# 查找所有 .log 文件
find /var/log -name "*.log"# 查找3天前修改的文件
find /data -mtime +3# 查找大于100M的文件
find / -type f -size +100M# 查找并删除7天前的备份文件
find /backup -name "*.tar.gz" -mtime +7 -exec rm -f {} \;
二、xargs:批量传参执行命令
cat files.txt | xargs rm -f
或结合 find
使用:
find . -name "*.tmp" | xargs rm -f
xargs
是 Shell 脚本中实现批处理的“神器”。
三、tar:打包压缩备份
# 打包当前目录
tar -czvf backup.tar.gz ./# 解压
tar -xzvf backup.tar.gz -C /tmp
四、scp:远程复制文件
# 上传文件
scp localfile.txt user@remote:/data/# 下载文件
scp user@remote:/data/backup.tar.gz ./
配合 SSH 免密登录使用更方便。
五、rsync:增量同步工具(强烈推荐用于部署)
rsync -avz ./dist/ user@remote:/data/web/
-a
:归档模式(保留时间、权限等)-v
:显示详细过程-z
:压缩传输
六、curl:调用接口(之前已经讲过)
常用于脚本中调用 webhook、发送通知、上报状态等。
curl -X POST http://api.example.com/report \-H "Content-Type: application/json" \-d '{"status":"ok","time":"2025-04-10 12:00:00"}'
七、本章综合实战:自动打包 + 清理 + 上报
#!/bin/bash# 定义变量
BAK_DIR="/data/backup"
NOW=$(date +"%Y%m%d%H%M%S")
FILENAME="backup_$NOW.tar.gz"# 打包
tar -czf "$BAK_DIR/$FILENAME" /data/mingdao# 删除7天前的旧备份
find $BAK_DIR -name "*.tar.gz" -mtime +7 -exec rm -f {} \;# 上报结果
curl -X POST http://api.example.com/backup/report \-H "Content-Type: application/json" \-d "{\"filename\":\"$FILENAME\", \"time\":\"$NOW\"}"
八、本章练习题
- 使用
find
查找/tmp
目录下所有.log
文件并删除。 - 编写一个脚本:打包
/etc
目录,保存到/data/backup
,并保留最近3份。 - 编写一个自动化部署脚本,使用
rsync
将本地/dist
同步到远程服务器指定目录。
第7章:Shell 实战项目篇
实战1:数据库定时备份 + 接口上报
场景说明:
你想每天定时备份 MySQL 和 MongoDB,并在完成后通过 curl
报告备份信息。
脚本:backup_and_report.sh
#!/bin/bash# 1. 定义开始时间
start_time=$(date +"%Y-%m-%d %H:%M:%S")# 2. 执行备份命令(这里用 docker 容器)
container_id=$(docker ps | grep community | awk '{print $1}')
docker exec -it "$container_id" bash -c 'source /entrypoint.sh && backup mysql mongodb file'# 3. 定义结束时间
end_time=$(date +"%Y-%m-%d %H:%M:%S")# 4. 找到最新备份路径
backup_dir="/data/mingdao/script/volume/data/backup"
latest=$(ls -td ${backup_dir}/*/ | head -n 1)# 5. 上报接口
curl -X POST http://api.example.com/backup/report \-H "Content-Type: application/json" \-d '{"startTime": "'"$start_time"'","endTime": "'"$end_time"'","backupPath": "'"$latest"'"}'
配合 crontab 每天凌晨1点执行:
0 1 * * * /bin/bash /opt/scripts/backup_and_report.sh >> /var/log/backup.log 2>&1
实战2:自动部署前端代码到远程服务器
脚本:deploy_frontend.sh
#!/bin/bashdist_dir="./dist"
remote_user="deploy"
remote_host="192.168.10.100"
remote_path="/data/www/"rsync -avz --delete "$dist_dir/" "$remote_user@$remote_host:$remote_path"
echo "部署完成:$(date)"
实战3:系统监控报警脚本
脚本:check_cpu.sh
#!/bin/bashthreshold=80
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}') # $8为空闲if [ $(echo "$cpu_usage > $threshold" | bc) -eq 1 ]; thencurl -X POST http://api.example.com/alert \-H "Content-Type: application/json" \-d '{"message": "CPU使用率过高:'"$cpu_usage"'%"}'
fi
实战4:交互式菜单工具脚本
脚本:menu.sh
#!/bin/bashwhile true; doecho "====== 系统工具菜单 ======"echo "1. 查看磁盘使用"echo "2. 查看内存使用"echo "3. 查看登录用户"echo "4. 退出"read -p "请输入选项[1-4]:" choicecase $choice in1) df -h ;;2) free -h ;;3) who ;;4) echo "Bye!"; exit 0 ;;*) echo "无效输入" ;;esac
done
实战5:每日自动压缩日志
#!/bin/bashlog_dir="/var/log/myapp"
backup_dir="/data/log_backup"
today=$(date +%F)mkdir -p "$backup_dir"
tar -czf "$backup_dir/logs_$today.tar.gz" "$log_dir"/*.log
rm -f "$log_dir"/*.log
本章练习题
- 写一个脚本定时将
/etc
目录打包上传到远程服务器。 - 写一个脚本每天查找
/tmp
下.log
文件并清理超过3天的文件。 - 写一个自动监测服务器磁盘占用超过 90% 就上报告警的脚本。
第8章 · 个人实战项目总结:自动化备份 + 压缩 + 清理 + 上报
脚本目标
每天凌晨 1 点自动执行以下操作:
- 判断磁盘空间是否足够
- 执行容器内数据库+文件备份命令
- 将备份目录压缩为
.tar.gz
- 删除原始目录
- 保留最近 3 个压缩文件
- 上报接口通知备份结果
- 生成详细执行日志
- 防止多次重复执行
脚本路径
/data/backup_and_notify.sh
最终正式版脚本
#!/bin/bash
set -euo pipefail # 脚本遇错立即退出,禁止未定义变量,管道中出错也终止
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin# ========== 配置区域 ==========
LOG_FILE="/tmp/cron_exec.log" # 日志文件路径
LOCK_FILE="/tmp/backup.lock" # 锁文件路径(防止重复执行)
BACKUP_BASE="/backup_base" # 备份目录(需确保存在)
API_URL="http://your-api-host/your/endpoint" # 上报接口地址(请替换)
DEBUG_MODE=false # 调试开关,true 为跳过 docker 执行并模拟
# =================================log() {echo "[$(date +"%F %T")] $1" >> "$LOG_FILE" # log 函数:统一日志格式写入
}# ========== 防重入锁机制 ==========
if [ -f "$LOCK_FILE" ]; thenold_pid=$(cat "$LOCK_FILE")if ps -p "$old_pid" > /dev/null 2>&1; thenlog "已有任务执行中(PID=$old_pid),跳过本次"exit 0elselog "锁文件存在但进程不存在,清理锁文件"rm -f "$LOCK_FILE"fi
fiecho $$ > "$LOCK_FILE" # 写入当前脚本 PID 为锁文件# ========== 主流程开始 ==========
log "=== 备份任务开始 ==="
start_time=$(date +"%Y-%m-%d %H:%M:%S")# 1. 检查磁盘剩余空间是否大于当前已用空间(防爆盘)
disk_info=$(df -k "$BACKUP_BASE" | tail -1)
used_kb=$(echo "$disk_info" | awk '{print $3}')
avail_kb=$(echo "$disk_info" | awk '{print $4}')
log "磁盘空间 - 已用: ${used_kb}KB, 可用: ${avail_kb}KB"if [ "$avail_kb" -lt $((used_kb * 1)) ]; thenlog "剩余空间不足,终止备份"rm -f "$LOCK_FILE"exit 1
fi# 2. 查找名为 community 的 docker 容器
container_id=$(docker ps | grep community | awk '{print $1}')
if [ -z "$container_id" ]; thenlog "未找到包含 community 的容器,退出"rm -f "$LOCK_FILE"exit 1
fi
log "容器 ID: $container_id"# 3. 执行备份命令或生成模拟目录
if [ "$DEBUG_MODE" == "true" ]; thenlog "[调试模式] 跳过 docker 备份,生成模拟目录"mock_dir_name=$(date +"%Y%m%d%H%M%S")mock_dir_path="$BACKUP_BASE/$mock_dir_name"mkdir -p "$mock_dir_path"echo "模拟数据文件内容" > "$mock_dir_path/mock.txt"latest_backup="$mock_dir_path"
elselog "执行容器备份命令..."docker exec "$container_id" bash -c 'source /entrypoint.sh && backup mysql mongodb file'log "容器内备份完成"latest_backup=$(ls -td "$BACKUP_BASE"/*/ 2>/dev/null | head -n 1)
fiend_time=$(date +"%Y-%m-%d %H:%M:%S")# 4. 确认最新备份目录
if [ -z "$latest_backup" ]; thenlog "未找到最新备份目录,退出"rm -f "$LOCK_FILE"exit 1
filatest_dir_name=$(basename "$latest_backup")
log "最新备份目录:$latest_backup"# 5. 压缩目录为 .tar.gz
tar_file="$BACKUP_BASE/$latest_dir_name.tar.gz"
log "压缩目录为:$tar_file"
tar -zcvf "$tar_file" -C "$BACKUP_BASE" "$latest_dir_name" >> "$LOG_FILE" 2>&1if [ -f "$tar_file" ]; thenlog "压缩成功,删除原始目录"rm -rf "$latest_backup"
elselog "压缩失败:文件未生成"
fi# 6. 上报接口,上传 tar 包路径 + 起止时间
log "开始上报接口..."
post_result=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$API_URL" \-H "Content-Type: application/json" \-d '{"startTime": "'"$start_time"'","endTime": "'"$end_time"'","backupPath": "'"$tar_file"'"}')if [ "$post_result" == "200" ]; thenlog "上报成功"
elselog "上报失败,状态码:$post_result"
fi# 7. 清理旧备份,仅保留最新 3 个 tar.gz 文件
log "开始清理旧备份(保留3份)"
old_tars=$(ls -1t "$BACKUP_BASE"/*.tar.gz 2>/dev/null | tail -n +4)
for f in $old_tars; dolog "删除旧文件:$f"rm -f "$f"
donelog "=== 备份任务结束 ==="# 8. 删除锁文件
rm -f "$LOCK_FILE"
定时任务 crontab 设置
crontab -e
添加:
0 1 * * * /bin/bash /data/backup_and_notify.sh >> /var/log/backup.log 2>&1
测试方式
- 设置
DEBUG_MODE=true
快速走完整流程 - 查看日志输出:
/var/log/backup.log
/tmp/cron_exec.log
- 检查是否压缩
.tar.gz
成功 - 检查是否自动清理旧文件
- 检查是否触发上报接口
第9章:Shell 脚本高阶技巧与调试优化
一、调试脚本的方法
1. 手动打印变量值(最常用)
echo "当前值是:$var"
2. 使用 set -x
开启调试模式
#!/bin/bash
set -x # 开始调试
...你的代码...
set +x # 关闭调试
会打印出每一行执行的命令和变量展开后的值,便于追踪问题。
3. 整体调试脚本执行
bash -x yourscript.sh
二、安全与健壮性写法
1. 脚本头部加入防御设置
#!/bin/bash
set -euo pipefail
-e
: 一旦出错立即退出-u
: 使用未定义变量时立即退出-o pipefail
: 管道中任何一个命令出错就认为整个失败
2. 变量默认值
echo "用户名:${USERNAME:-guest}"
如果 USERNAME
未定义,则默认值为 guest
3. 防止空变量导致误删
# 错误写法(rm -rf $dir,如果 $dir 是空,会删当前目录)
rm -rf "$dir"# 安全写法
if [ -n "$dir" ]; thenrm -rf "$dir"
fi
三、优化脚本结构
1. 使用函数模块化脚本
backup_mysql() { ... }
backup_mongo() { ... }
send_report() { ... }main() {backup_mysqlbackup_mongosend_report
}main
2. 拆分配置文件
将常量配置提取到 config.sh
,主脚本中用 source ./config.sh
引入,方便统一维护。
四、日志管理与错误输出
记录日志
log() {echo "[$(date +"%F %T")] $1" >> /var/log/myscript.log
}
捕获错误信息
some_command 2>> error.log
五、制作可执行命令脚本
chmod +x mytool.sh
mv mytool.sh /usr/local/bin/mytool
之后你就可以在任何地方输入 mytool
直接调用脚本了。
六、实践建议与面试亮点
- 编写脚本时,错误处理永远优先;
- 函数 + 配置分离是结构清晰的关键;
- 使用
set -euo pipefail
保证健壮性; - 利用
cron + curl + shell
可构建一整套自动化任务、监控、备份系统; - 如果要参加面试,可主动讲你写过哪些“脚本工具”、怎么处理日志、怎么保障安全 —— 是加分项!
七、本章练习与挑战
- 为你最常用的命令写一个简化脚本,如批量清理日志、上传打包文件。
- 尝试把项目中某个手工流程自动化为脚本,比如打包部署/数据库备份。
- 写一个脚本框架,支持日志输出、错误捕获、配置参数、分函数执行。