序章:那个被日志逼疯的夜晚
30天前,我在一家互联网公司做后端开发。某天深夜,运维同事丢给我一个任务:"服务器日志炸了,赶紧把最近7天的错误日志筛出来,按模块分类打包,明早9点前发我。"我盯着终端里密密麻麻的日志,手忙脚乱地用grep "ERROR"筛了两页,突然发现——我不会用Shell。
那晚我熬到凌晨三点,用最笨的方法:手动复制粘贴、用Excel筛选、甚至用Word做统计。第二天交差时,运维大哥扫了眼文件:"你这格式乱得,下次学学Shell吧,半小时能搞定。"这句话像根刺扎在我心里——原来,我离"高效"只差一个Shell的距离。
这30天,我从对着终端发抖的新手,到能写复杂脚本的"自动化狂魔"。今天,我想把这些血泪经验整理成一份"保姆级指南",帮你少走弯路,真正把Shell变成你的技术武器。
第一关:打破恐惧——Shell到底是个啥?
1.1 你和计算机的"方言"对话
想象你在教一个完全不懂中文的外国人做事:"把桌上的红色杯子拿到厨房"。他会问:"红色?杯子?厨房?"这时候你需要用他的语言(比如英语)解释:"Take the red cup on the table to the kitchen."
Shell就是你和计算机之间的"翻译官"。你用ls -l告诉它:"列出当前目录的所有文件,详细点";它翻译成计算机能听懂的系统调用(比如getdents),再把结果翻译回你能看懂的文字。
1.2 为什么必须学Shell?3个真实场景
场景1:上线后服务器CPU飙高,你需要快速定位哪个进程在搞鬼。用top -c看进程,ps -ef | grep 进程名找启动命令,kill -9 PID终止进程——这些操作,不用Shell你得翻遍图形化工具。
场景2:每周五要备份10个项目代码到云盘。手动cd、git pull、tar、scp?用Shell脚本写个循环,一键搞定10个项目。
场景3:日志里有10万条记录,你要找出所有包含"Timeout"且状态码504的条目。用grep "Timeout" access.log | grep "504",10秒出结果;手动翻?得翻到眼瞎。
结论:Shell不是"运维专属",而是每个开发者的"效率外挂"。它能让你和计算机对话更高效,把重复劳动变成"一键执行"。
第二关:基础语法——先搞定这些,你就能写90%的脚本
2.1 第一个脚本:从"hello world"到"我能解决实际问题"
很多人学Shell的第一步是写hello.sh,但我建议你直接挑战一个有实际价值的小任务:"输出当前目录下所有文件的大小(MB为单位),并按从大到小排序"。
试试看,你能写出这样的脚本吗?
代码:
#!/bin/bash
# 文件大小统计脚本(进阶版hello world)
echo "当前目录文件大小统计(MB):"
du -sm * | sort -hr | awk '{print $2 ": " $1 "MB"}'
du -sm *:统计每个文件/目录的大小(-s汇总,-m以MB为单位);
sort -hr:按数值逆序排序(-h兼容人类可读单位,-r降序);
awk:格式化输出,只保留文件名和大小。
运行结果:
代码:
当前目录文件大小统计(MB):
logs/: 1234MB
data.csv: 567MB
backup.tar.gz: 456MB
成就感爆棚! 这就是Shell的魅力——用几行代码解决原本需要手动操作半小时的问题。
2.2 变量:给数据起个"名字"
变量是Shell的"储物盒",用来存你需要的数据。比如,你想把日志路径存起来,方便后续修改:
代码:
LOG_DIR="/var/log/myapp" # 定义变量(等号两边不能有空格!)
echo "日志路径:$LOG_DIR" # 使用变量($符号是关键)
常见坑点:
变量赋值时,等号两边不能有空格(LOG_DIR = /var/log会报错);
变量名区分大小写(log_dir和LOG_DIR是两个不同的变量);
用${}明确变量边界(echo "文件:${LOG_DIR}/app.log"比echo "文件:$LOG_DIR/app.log"更安全,避免路径中有特殊字符时出错)。
2.3 输入输出:和用户"聊天"
想让脚本更灵活?试试让用户输入参数。比如,写一个"根据输入的数字计算平方"的脚本:
代码:
#!/bin/bash
echo "请输入一个数字:"
read num # 读取用户输入到变量num
square=$((num * num)) # 计算平方($(( ))是算术运算)
echo "$num 的平方是:$square"
运行效果:
代码:
请输入一个数字:
5
5 的平方是:25
进阶玩法:用read一次性读取多个变量:
代码:
echo "请输入姓名和年龄(用空格分隔):"
read name age
echo "你好,$name!你今年$age岁了。"
2.4 控制流:让脚本"做决定"
条件判断:if-elif-else
比如,写一个"判断成绩等级"的脚本:
代码:
#!/bin/bash
score=85
if [ $score -ge 90 ]; then
grade="优秀"
elif [ $score -ge 80 ]; then
grade="良好"
elif [ $score -ge 60 ]; then
grade="及格"
else
grade="不及格"
fi
echo "分数:$score,等级:$grade"
关键语法:
[ ]是test命令的别名,里面每个条件都要用空格隔开(比如$score -ge 90);
-ge表示"大于等于"(其他常用运算符:-eq等于,-lt小于,-n字符串非空)。
循环:for/while
场景:批量重命名当前目录下所有.txt文件为.md,并加note_前缀。
用for循环轻松搞定:
代码:
for file in *.txt; do
new_name="note_${file%.txt}.md" # ${file%.txt}去掉.txt后缀
mv "$file" "$new_name"
done
echo "重命名完成!共处理$(ls note_*.md | wc -l)个文件"
关键技巧:
*.txt会匹配所有以.txt结尾的文件;
${file%.txt}是变量替换,去掉变量值的.txt后缀(类似Python的str.rstrip(".txt"));
$(...)是命令替换,把括号里的命令结果插入到当前位置(比如ls note_*.md | wc -l统计文件数量)。
第三关:实战进阶——用Shell解决真实痛点
3.1 痛点1:日志分析——快速定位问题
场景:线上服务器日志app.log有10GB,你需要找出最近1小时内所有"ERROR"级别的日志,并按模块分类统计数量。
解决方案:
代码:
#!/bin/bash
LOG_FILE="/var/log/app.log"
ONE_HOUR_AGO=$(date -d "1 hour ago" +"%Y-%m-%d %H:%M:%S") # 计算1小时前的时间
# 筛选最近1小时的ERROR日志,并按模块(假设模块名在日志的第三列)统计
grep "$ONE_HOUR_AGO" "$LOG_FILE" | grep "ERROR" | awk '{print $3}' | sort | uniq -c
效果:
代码:
45 payment # payment模块出现45次ERROR
32 order # order模块出现32次ERROR
8 user # user模块出现8次ERROR
关键技术:
date -d:动态计算时间(比硬编码时间更灵活);
grep:文本搜索(-E参数可启用正则表达式,比如grep -E "ERROR|WARN"同时搜索ERROR和WARN);
awk:按列处理($3表示第三列,sort | uniq -c统计频率)。
3.2 痛点2:文件批量处理——解放双手
场景:你下载了100张图片,命名混乱(img1.jpg、pic2.png、2023_photo.jpg),需要统一重命名为photo_001.jpg、photo_002.png等格式。
解决方案:
代码:
#!/bin/bash
COUNT=1 # 计数器
for file in *.{jpg,png,gif}; do # 匹配所有jpg/png/gif文件
ext="${file##*.}" # 提取文件后缀(${var##*.}表示取最后一个点后的内容)
new_name="photo_$(printf "%03d" $COUNT).$ext" # 格式化计数器(03d表示3位,前面补0)
mv -- "$file" "$new_name" # 重命名(--防止文件名以-开头时报错)
((COUNT++)) # 计数器+1(等价于COUNT=$((COUNT+1)))
done
echo "重命名完成!共处理$((COUNT-1))个文件"
关键技术:
*.{jpg,png,gif}:通配符扩展(匹配所有指定后缀的文件);
${file##*.}:变量替换(提取文件后缀);
printf "%03d":格式化数字(补零,比如1→001);
((COUNT++)):算术运算(简化COUNT=$((COUNT+1)))。
3.3 痛点3:自动化运维——解放重复劳动
场景:你负责维护3台服务器,需要每天凌晨2点备份它们的/var/www目录到本地/backup,并删除7天前的旧备份。
解决方案:用Shell脚本+crontab实现定时任务。
步骤1:写备份脚本backup_servers.sh:
代码:
#!/bin/bash
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d) # 当前日期(格式:20231025)
# 备份3台服务器
for ip in 192.168.1.101 192.168.1.102 192.168.1.103; do
scp -r root@$ip:/var/www "$BACKUP_DIR/www_$DATE" # 远程拷贝目录
tar -czf "$BACKUP_DIR/www_$DATE.tar.gz" -C "$BACKUP_DIR" "www_$DATE" # 打包压缩
rm -rf "$BACKUP_DIR/www_$DATE" # 删除临时目录
done
# 删除7天前的备份
find "$BACKUP_DIR" -name "www_*.tar.gz" -mtime +7 -delete # -mtime +7表示7天前
步骤2:设置定时任务(每天2点执行):
crontab -e # 打开crontab编辑器
# 添加以下内容(保存退出后生效):
0 2 * * * /bin/bash /path/to/backup_servers.sh >> /backup/backup.log 2>&1
关键技术:
scp:远程文件传输(root@$ip:/path表示远程服务器的路径);
tar:打包压缩(-czf创建gzip压缩包,-C切换目录);
find:查找文件并删除(-mtime +7匹配7天前的文件,-delete直接删除);
crontab:定时任务(0 2 * * *表示每天2:00)。
第四关:避坑指南——这些错误90%的新手会犯
4.1 常见错误1:路径问题——"文件找不到"
现象:脚本运行时报错No such file or directory,但文件明明存在。
原因:Shell脚本的工作目录(pwd)可能和你预期不同。比如,你在/home/user目录下运行./script.sh,但脚本里的/var/log/app.log是绝对路径,没问题;但如果脚本里用了logs/app.log(相对路径),实际路径会是/home/user/logs/app.log,如果不存在就会报错。
解决方案:
尽量用绝对路径(如/var/log/app.log);
如果必须用相对路径,在脚本开头打印当前工作目录:echo "当前工作目录:$(pwd)";
用realpath命令获取文件的绝对路径:FILE_PATH=$(realpath "logs/app.log")。
4.2 常见错误2:变量未定义——"结果不对"
现象:脚本输出的变量值为空,但你确定已经赋值了。
原因:Bash默认允许变量未定义(视为空),如果脚本依赖变量的值,可能导致逻辑错误。
解决方案:
用set -u开启"未定义变量报错"(脚本开头加set -u,遇到未定义变量直接终止);
初始化变量(即使赋默认值,如LOG_DIR=${LOG_DIR:-"/var/log"},表示如果LOG_DIR未定义,默认用/var/log)。
4.3 常见错误3:权限问题——"没有那个权限"
现象:脚本运行时报错Permission denied。
原因:
脚本本身没有执行权限(需要chmod +x script.sh);
脚本调用了没有权限的命令(比如往/root目录写文件,但当前用户不是root)。
解决方案:
给脚本添加执行权限:chmod +x script.sh;
用sudo运行需要权限的命令(如sudo mv file /root),或在脚本里检查权限(if [ ! -w "/root" ]; then echo "无写入权限"; exit 1; fi)。
终章:Shell的未来——从"脚本小子"到"自动化专家"
30天前,我对着终端发抖;今天,我能用Shell写复杂脚本,用cron定时执行,用ssh远程管理服务器。更重要的是,我学会了用Shell思维解决问题:遇到重复劳动,先想"能不能用一行命令搞定?"
Shell不是终点,而是起点。掌握它后,你可以:
学习sed/awk高级文本处理(比如用awk生成报表);
掌握ssh密钥登录(告别重复输入密码);
结合Python/Go写更强大的工具(Shell负责调用,Python负责复杂逻辑);
甚至学习Ansible/Puppet(它们底层也是Shell命令)。
最后送一句话:Shell的终极目标不是"写脚本",而是"解决问题"。现在就打开终端,输入echo "Hello, Shell!",迈出你的第一步吧!
延伸资源:
经典书籍:《Shell脚本学习指南(第3版)》《Linux命令行与shell脚本编程大全》;
在线工具:ShellCheck(语法检查)、ExplainShell(命令解析);
社区:Stack Overflow(搜Shell问题)、GitHub(找优秀的Shell脚本示例)。