Linux Shell 脚本入门:从第一个脚本到能用的备份/监控小程序
2026-08-06 · DevCraft Studio
想让服务器自动干活,shell 脚本是绕不开的基本功。这篇文章从 shebang、变量、条件、循环、函数讲起,带你写出一个带严格模式、能监测磁盘使用率的实用脚本,并介绍 shellcheck 这个防坑神器。
前面学的命令都是"手动一条条敲",但真正的效率来自把命令串成脚本,让服务器自己跑。Shell 脚本是 Linux 自动化的基本功:备份、监控、批量处理,全靠它。很多人觉得脚本高深,其实半个下午就能上手。这篇文章从第一行 shebang 讲起,覆盖变量、条件、循环、函数,最后带你写出一个能监测磁盘使用率并告警的实用脚本,并安利 shellcheck 这个"防坑神器",让你写的脚本少出低级错误。
一、shebang 与严格模式
每个脚本第一行是 #!/usr/bin/env bash,告诉系统用哪个解释器——用 env bash 比写死路径更可移植。第二行强烈建议 set -euo pipefail:遇到未定义变量报错(-u)、命令失败立即退出(-e)、管道中任一段失败也算失败(-o pipefail)。这行"严格模式"能在出错时立刻停下,而不是带着错误继续执行、酿成更大事故。养成开头就写这两行的习惯,脚本质量直接上台阶。
#!/usr/bin/env bash
set -euo pipefail二、变量与输入
赋值写成 VAR=value(等号两侧不能有空格,否则被当成命令),引用用 $VAR 或 "${VAR}"。读用户输入用 read -p。位置参数里 $1 是第一个参数、$@ 是所有参数、$# 是参数个数、$0 是脚本名、$? 是上条命令的退出码(0 成功,非 0 失败)。记住:引用变量一定要加双引号,比如 "$(date +%F)",否则路径带空格会拆词出错。这是脚本里最高频的坑。
NAME="world"
echo "Hello, ${NAME}"
read -r -p "输入文件名: " FNAME
echo "参数个数: $#, 第一个: $1"三、条件判断
用 if 配合 [[ ]](bash 增强版,支持 &&、正则、字符串比较不用转义)。字符串相等用 ==,数字比较用 -gt/-lt/-eq,文件存在用 -f,目录可读用 -d && -r。多分支用 case。新手常错:用 == 比数字(应 -eq)、在 [ ] 里直接用 < >(被当重定向,应换 [[ ]])。条件写得对,脚本逻辑才不会跑偏。
if [[ "$NAME" == "root" ]]; then echo "是 root"; fi
if [[ -f "/etc/passwd" ]]; then echo "文件存在"; fi
case "$1" in
start) echo "启动" ;;
stop) echo "停止" ;;
*) echo "用法: $0 start|stop" ;;
esac四、循环
for 遍历文件:for f in *.log; do ... done。C 风格计数:for ((i=1; i<=5; i++))。按行读文件千万别用 for line in $(cat file)(遇空格换行会拆词),正确是用 while IFS= read -r line; do ... done < file.txt。循环是批处理的核心,写错遍历方式会漏数据或重复处理。多写几遍,肌肉记忆就有了。
for f in *.log; do echo "处理 $f"; done
while IFS= read -r line; do echo "$line"; done < file.txt五、函数
把一段逻辑包成函数:myfunc() { ... },用 local 声明局部变量避免污染全局,return 返回退出码。函数让脚本模块化、可复用,也更好读。比如把"打一个带日期的压缩包"封装成 backup() 函数,主流程里一行调用。脚本一长,函数就是你的秩序感。
backup() {
local src="$1"
tar zcf "/tmp/$(date +%F).tar.gz" "$src"
}
backup /etc六、参数与退出码
脚本通过 exit N 返回状态,0 表示成功、非 0 表示各类失败。调用方用 $? 判断。常见写法:command_that_may_fail || { echo "失败"; exit 1; }——失败就报错并退出。把"可能失败的命令 + 错误处理"成对写,脚本才健壮。也支持默认参数:SRC="${1:-/etc}" 表示没传参时用 /etc。
command_that_may_fail || { echo "失败"; exit 1; }
SRC="${1:-/etc}"七、实例:磁盘使用率监控脚本
把前面知识合起来,写一个监控脚本:遍历所有 /dev 挂载点,取出使用率,超过阈值(如 80%)就打印告警。它用 while read 逐行读 df 输出、awk 取百分比、tr 去百分号、[[ ]] 做数字比较。写完 chmod +x 加执行权限,./script.sh 运行。这就是一个能放进 cron 定时跑的实用工具——脚本的价值,在于把"手动巡检"变成"自动守护"。
补充与延伸:写脚本是在训练你的工程思维
Shell 脚本看似只是"把命令排起来",但写得久了你会发现自己变了一个人:你开始用"可重复、可自动化、出错可恢复"的方式思考任何重复劳动。今天手动改十个配置文件,明天就想着写个 for 循环一次搞定;发现某操作会忘,就封装成脚本放进定时任务。这种思维迁移,比学会具体语法更有长期价值——它让你从"操作工"变成"系统设计者",把时间花在更值得的地方。
写好脚本有几个值得刻进骨子里的习惯。其一是 set -euo pipefail 几乎必开:让脚本遇到未定义变量、命令失败、管道中断就立刻停,而不是带着错误状态继续往下跑出更离谱的结果。其二是所有外部输入(文件名、参数、命令输出)都用双引号包住,否则名字里带空格就会把你的逻辑撕碎。其三是别重复造轮子,复杂文本处理该用 awk/sed 就用,但逻辑一复杂就考虑换 Python——shell 擅长粘合,不擅长复杂算法。知道每种工具的边界,比死磕一种更聪明。
最后,把脚本当代码对待:写注释说明"为什么"而非"做什么",用 shellcheck 在提交前扫一遍常见坑,重要的脚本放进 git 版本管理。当你的脚本库越来越厚,它们就成了一座个人运维工具箱——新机器到手,clone 下来跑一遍,环境、监控、备份全就位。到这一步,你写的已经不是脚本,而是你自己的生产力杠杆。脚本能力越强,你在服务器面前就越从容。
还有一个心态:别怕脚本短。很多人觉得"这么几行也算脚本?"就懒得写,结果每次都手工敲一串命令,既容易敲错又没法复用。恰恰相反,越短越常用的操作,越值得固化成脚本——三行的 deploy.sh 可能救你十次部署。脚本的价值不在长度,而在它把"一次性动作"变成了"可随时重放的确定流程"。从今天起,遇到第二次重复的手工操作,就把它写成脚本。
再补一个实用习惯:给脚本加"用法说明"和参数校验。脚本开头用 getopts 解析 -h/--help,参数缺失时打印用法并 exit 1,比让用户对着报错猜要友好得多。重要的脚本还可以用 mktemp 创建临时文件、用 trap 'rm -f "$TMP"' EXIT 确保退出时清理,避免满地临时文件。这些"边角料"细节,恰恰是脚本从"能跑"到"可靠"的分水岭。
最后一个进阶方向:当脚本逻辑变复杂,考虑用 bash 严格模式 + 函数库,把常用功能(发告警、写日志、锁文件)抽成可复用的 lib.sh,各个脚本 source 它。这样你的工具箱不是一堆孤岛,而是一套有共同底座的装备。脚本写到这份上,它已经是你个人运维体系的骨架,而不是临时凑出来的几行命令了。
回到开头的那句话:脚本是杠杆。它放大的不是某一行命令的威力,而是你重复做事的效率。当你积累起十几个、几十个顺手的小脚本,你会发现很多以前觉得"麻烦、懒得做"的运维动作,现在一行命令就搞定,机器也变得更可控。别小看这些积累,一年后再看,它们就是你和"手忙脚乱版自己"之间最大的差距。
收个尾:脚本的世界没有终点,今天你觉得完美的脚本,半年后回头看可能满眼可改之处——这恰恰说明你在进步。保持"写完就想着怎么让它更稳、更易读"的习惯,你的工具箱会越长越结实。这就是把工程思维装进日常的最朴素方式。
#!/usr/bin/env bash
set -euo pipefail
THRESHOLD=80
while IFS= read -r line; do
usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
mp=$(echo "$line" | awk '{print $6}')
if [[ "$usage" -ge "$THRESHOLD" ]]; then
echo "警告: $mp 使用率 ${usage}%"
fi
done < <(df -h | grep '^/dev/')八、shellcheck:上线前的防坑神器
shellcheck 是静态检查工具,能揪出引号缺失、测试写错、未定义变量等常见陷阱,很多 CI 都集成它。写完脚本跑一句 shellcheck script.sh,按提示改,能避免大量"本地能跑、上线就崩"的尴尬。把它当脚本的拼写检查,养成习惯,你的脚本会越来越稳。学脚本没有捷径,唯手熟尔——从今天这个监控脚本开始改改看吧。
sudo apt install shellcheck
shellcheck script.sh