别把它想得太玄乎。Linux Shell 脚本编程,本质上就是你把平时在终端里一条条敲的命令,提前写进一个文件里,让机器按顺序、按条件、按循环去自动执行。我见过太多人学这个,要么卡在语法细节里出不来,要么觉得“反正能敲命令就行”一直没跨过写脚本这道坎。这篇文章我尽量讲实在的,从最基础的思路到能落地的实战写法,按我自己的学习路径和带人经验来拆一遍,希望能帮你把这条学习曲线拉直一点。
1. 项目概述与整体学习思路拆解
1.1 学Shell脚本到底在学什么
Shell脚本的学习,表面看是学语法,实际上是在学三件事:第一件,把Linux常用命令用熟;第二件,理解进程、文件描述符、环境变量这些操作系统概念;第三件,建立“用程序逻辑组织命令”的思维方式。三件事缺一不可。
很多人买了一大本《Linux命令行与Shell脚本编程大全》,翻到第三十章还在看命令参数,真正自己写脚本的时候还是无从下手,原因就是他把这三件事孤立着学了。命令背了一堆,不知道什么时候组合;概念看了一堆,不知道和命令行有什么关系;逻辑学了一堆,一遇到真实文件名带空格、路径带中文就懵。
我建议换一个思路:不要按命令字典的顺序学,而是按“我要完成什么任务”的顺序学。比如“我要把某个目录下所有log文件按日期归档”,这个任务会迫使你去用find、grep、date、tar、循环、变量,学完这个任务,命令和逻辑就自然长在一起了。
1.2 这份学习指南适合谁、能解决什么问题
如果你是运维工程师、后端开发、测试工程师,或者正在准备Linux相关岗位面试,这份指南适合你。如果你只是偶尔用一下Linux桌面,平时不碰命令行,可以先收藏,等真需要批量处理文件时再回来看。
这篇文章对应的是一个“从入门到实战”的完整路径:先建立认知框架,再掌握高频命令的深度用法,然后通过真实案例把脚本的骨架搭起来,最后讲清楚常见问题和进阶方向,同时也整理了一些面试和岗位技能相关的内容。你不需要一次性读完,建议按章节来,每一章动手敲一遍,效果比通读三遍好得多。
提示:学Shell最忌讳的就是“只看不敲”。你哪怕照着文章把命令原封不动打一遍,收获都比读十遍大。一定要自己建目录、自己造垃圾文件去练。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频命令与核心概念:先解决“工具不熟”的问题
2.1 20个高频命令,按场景分组比按字母背高效十倍
我不建议拿命令大全从头背。按场景分组最快。我整理了一份高频命令清单,你可以把它当成“最小可用集”,先用熟,再扩展。
| 场景 | 命令家族 | 核心用途 |
|---|---|---|
| 文件操作 | cd、ls、cp、mv、rm、ln | 移动、复制、删除、链接,最基础 |
| 内容查看 | cat、less、head、tail | 查看文件内容,tail -f 几乎是日志排查标配 |
| 文本处理 | grep、sed、awk、cut、sort、uniq | 搜索、替换、切片、排序去重 |
| 查找定位 | find、locate、which、whereis | 按条件找文件、找命令位置 |
| 权限与用户 | chmod、chown、useradd、usermod | 权限控制、用户管理 |
| 进程与系统 | ps、top、kill、df、du、free | 看进程、看资源、清进程 |
| 传输与网络 | scp、rsync、curl、wget | 文件传输、HTTP请求 |
| 打包压缩 | tar、zip、gzip | 归档与压缩 |
初学阶段,你先别碰sed和awk的复杂写法,能把grep、cut、sort、uniq用顺,就已经能解决很多实际问题了。sed和awk等遇到批量文本修改的需求再学,效率更高,因为有了需求驱动,你记参数会快很多。
2.2 环境变量、通配符、引号:这三个是脚本的隐形地基
命令本身好背,但脚本里最容易出错的反而是环境变量、通配符和引号的用法。
环境变量是脚本里传递信息的通道。你执行一个脚本时,shell会先启动一个子进程,这个子进程带着一份环境变量副本。你在脚本里export一个变量,它会影响这个脚本再启动的子进程,但不会影响你当前的终端。这个理解很重要,很多人写脚本时发现变量设置了但外部拿不到,就是因为没搞清楚父子进程的环境变量隔离。查看变量用echo $HOME,列出全部环境变量用env。
通配符和正则不是一回事,这个观念要尽早建立。通配符是shell在解析命令时做的路径扩展,比如*.txt只匹配文件名;而正则是文本匹配规则,比如grep '^abc'匹配行首。Shell里给find或grep传正则时加不加引号,结果完全不一样,这一点在命令行里踩坑极多。
引号更关键。双引号会展开变量,但不会通配;单引号完全字面量;反引号会被当成命令替换。你写rm -rf "$dir"和rm -rf $dir,前者安全,后者当dir为空时直接把当前目录给删了。不要觉得这是小事,这是脚本面试里最经典的坑。
2.3 命令找不到或权限被拒绝:先分清shell层的三类报错
实操中我经常收到这种问题:“明明装了软件,执行时却提示command not found”,“脚本文件写好了,执行时提示Permission denied”。
这两种报错要分开看。command not found是shell在PATH路径下找不到这个可执行文件,解决方向是检查软件是否真的安装了、安装路径是否在PATH里。Permission denied是可执行权限不够,解决方向是chmod +x。还有一种报错是“bad interpreter”或者“/bin/bash^M: bad interpreter”,这是脚本在Windows下编辑过,行尾带了回车符,用sed -i 's/\r$//' script.sh清洗一下就好。
这三类问题我后面会在常见问题速查表里给全。
3. 脚本语法核心细节:变量、条件、循环与函数
3.1 变量:命名、默认值、字符串处理
脚本里变量就是个名字加值,但有几个细节值得认真对待。
命名规则就三条:字母数字下划线,不能数字开头,赋值等号两边不能有空格。很多人卡在第三个,因为在别的语言里写a = 1没问题,在Shell里shell会把a当命令去执行,把=和1当参数传过去,然后报command not found。
变量默认值的处理是很实用的技巧。比如你写一个部署脚本,想支持外部传入环境名,不传就默认用prod。这里我会用两种方式:
bash复制# 方式一:${VAR:-default},变量为空时用默认值
env="${1:-prod}"
# 方式二:判断变量是否为空,再赋值
if [ -z "$env" ]; then
env="prod"
fi
第一种写法更简洁,而且可以嵌套在字符串里,比如echo "当前环境:${env}-cluster"。
字符串处理也有几个高频操作:${#var}拿到字符串长度,${var#prefix}去掉前缀,${var%suffix}去掉后缀,${var//old/new}全局替换。这些写法在批量处理文件名时极其好用,比如把.jpg改成.png:
bash复制for file in *.jpg; do
mv "$file" "${file%.jpg}.png"
done
这一小段代码其实是热词里“Linux用shell重命名文件”的最经典解法,后面我会展开讲更完整的版本。
3.2 条件判断:test、[ ]和[[ ]]的区别
Shell的条件判断大概是新手最容易混淆的部分。if后面跟的是一个命令的退出状态码,而不是布尔表达式。这是Shell和高级语言最大的不同。0代表成功,非0代表失败。所以if grep -q "error" logfile; then这种写法才能成立,因为grep有0和非0的退出码。
[ ]和[[ ]]是两个语法糖。单括号[ ]是test命令的别名,里面的变量最好加双引号,否则变量为空时会语法错误。双括号[[ ]]是bash的扩展语法,支持模式匹配、正则、逻辑运算,更安全也更强大。我推荐新写的脚本统一用[[ ]]:
bash复制name=""
if [[ "$name" == "dev" || "$name" == "prod" ]]; then
echo "合法环境:$name"
fi
双括号里字符串比较用==,数值比较用-eq、-ne、-gt、-lt。文件判断用-f(文件)、-d(目录)、-x(可执行)、-e(存在)。这些记法不需要硬背,用到的时候查一下,一个周后就自然记住了。
注意:用[ ]判断时,[、条件和]之间必须留空格,否则会直接语法错误。比如if [-f file]是错的,if [ -f file ]才是对的。这个细节几乎每个新手都踩过。
3.3 循环:for、while与真正的实战场景
for循环是脚本里出现频率最高的结构。最常用的三种写法:
bash复制# 遍历列表
for env in dev test prod; do
echo "部署到: $env"
done
# C风格循环,控制次数
for ((i=1; i<=10; i++)); do
echo "第 $i 次尝试"
done
# 搭配命令替换,遍历命令输出
for file in $(ls /var/log/*.log); do
echo "日志文件: $file"
done
第三种写法有个坑:如果文件名里有空格,$(ls)会把一个文件名拆成多个,导致循环出错。更稳妥的做法是用find配合while读:
bash复制find /var/log -name "*.log" -type f | while read -r file; do
echo "日志文件: $file"
done
while循环的常用场景是读文件、轮询等待结果、死循环保活。比如等待一个服务端口起来:
bash复制while ! nc -z localhost 8080; do
sleep 1
done
echo "服务已就绪"
这个写法在部署脚本里太常用了,比sleep固定的时间要优雅得多。
3.4 函数与shift:脚本参数处理的进阶玩法
函数本质上是把一段逻辑收拢起来,起个名字,方便复用和模块化。Shell函数的定义很简单:
bash复制log_info() {
echo "[INFO] $(date '+%F %T') $1"
}
log_info "开始部署"
$1就是传给函数的第一个参数,和脚本参数一样,$0是脚本名,$1到$9是位置参数,$#是参数个数,$@是所有参数。函数里return只能返回数字,用来表示退出状态码,不要用它返回字符串。
shift命令是用来“吃掉”位置参数的。执行一次shift,$2变$1,$3变$2。它在解析命令行参数时特别好用。比如你想要一个支持-h、-n参数的命令行选项解析器:
bash复制# 简单参数解析
while [[ $# -gt 0 ]]; do
case "$1" in
-h)
echo "用法: $0 [-n name] [-v]"
exit 0
;;
-n)
name="$2"
shift 2
;;
-v)
verbose=1
shift
;;
*)
echo "未知参数: $1"
exit 1
;;
esac
done
echo "name=$name"
shift 2是把参数往前挪两位,因为-n后面那个值也被消费掉了。这个模式是很多运维脚本参数解析的基本盘,不过如果你的参数特别多,还是建议用getopts或argparse,后面我讲进阶再提。
4. 实战案例拆解:从零写一个能落地的脚本
4.1 实战一:日志归档清理脚本的完整演进
日志清理是Shell脚本最典型的应用,没有之一。我们来设计一个需求:Nginx日志目录下每天生成access.log,要求保留最近30天,超过30天的压缩并删除30天前的压缩包。
第一版,直接用find实现最核心的逻辑:
bash复制#!/bin/bash
log_dir="/var/log/nginx"
keep_days=30
# 找到超过1天的日志文件,压缩成.gz
find "$log_dir" -name "access.log.*" -type f -mtime +1 -exec gzip {} \;
# 找到超过30天的.gz文件,直接删除
find "$log_dir" -name "access.log.*.gz" -type f -mtime +30 -delete
这一版能跑了,但有两个问题:没有日志记录,不知道到底清理了多少;如果压缩失败,后续的删除还是会执行,可能把未压缩的文件删掉。
第二版,加入日志和错误处理:
bash复制#!/bin/bash
log_dir="/var/log/nginx"
keep_days=30
log_file="/var/log/script/clean_nginx.log"
mkdir -p "$(dirname "$log_file")"
echo "$(date '+%F %T') 开始清理 $log_dir" >> "$log_file"
# 压缩前先统计
before_count=$(find "$log_dir" -name "access.log.*" -type f | wc -l)
# gzip超过1天未压缩的日志
find "$log_dir" -name "access.log.*" -type f -mtime +1 -exec gzip {} \;
# 删除30天前的压缩包
find "$log_dir" -name "access.log.*.gz" -type f -mtime +30 -delete
after_count=$(find "$log_dir" -name "access.log.*" -type f | wc -l)
echo "$(date '+%F %T') 清理完成, 剩余未压缩文件 $after_count 个" >> "$log_file"
这一版已经具备生产可用性了:有目录预检、有日志输出、有统计信息。再往后可以加判断:如果before_count为0就直接退出,避免无谓的输出;可以把keep_days做成外部参数,配合cron定期执行。
4.2 实战二:用脚本批量重命名文件
重命名文件这个需求我在网上看到太多人问了,正好热词里也有。最常遇到的场景是:有一堆文件,名字带统一前缀或后缀,要批量处理。
场景A:把所有.jpg文件改成.jpg.bak:
bash复制for file in *.jpg; do
mv "$file" "$file.bak"
done
场景B:剥离所有.bak后缀:
bash复制for file in *.bak; do
mv "$file" "${file%.bak}"
done
场景C:把IMG_20230101_123456.jpg这种命名改成20230101_123456.jpg,去掉IMG_前缀:
bash复制for file in IMG_*.jpg; do
new_name="${file#IMG_}"
mv "$file" "$new_name"
done
这个案例里${file#IMG_}就是字符串去前缀的语法,前面提到过。实战中这个写法比用sed去改文件名更安全,因为mv本身只处理文件名,不需要经过文本流的转义。
注意:批量重命名前,强烈建议先做一次dry run,也就是只打印“将把A改成B”,确认无误后再真正mv。你可以把脚本里的mv改成echo,跑一遍看输出,这个习惯能帮你避免大面积误操作。
4.3 实战三:一个可复用的命令行参数解析框架
参数解析是脚本从“自己用”到“交付给别人用”的分水岭。我平时习惯先把参数解析写好,再接业务逻辑,这样脚本到了别人手里也能用明白。
这里给一个稍微完整一点的示例,支持短参数和长参数:
bash复制#!/bin/bash
# 用法: deploy.sh -e prod -v 1.2.3 -f --verbose
env=""
version=""
force=0
verbose=0
while [[ $# -gt 0 ]]; do
case "$1" in
-e|--env)
env="$2"
shift 2
;;
-v|--version)
version="$2"
shift 2
;;
-f|--force)
force=1
shift
;;
--verbose)
verbose=1
shift
;;
-h|--help)
echo "用法: $0 -e <env> -v <version> [-f]"
exit 0
;;
*)
echo "未知参数: $1, 试 -h 查看帮助"
exit 1
;;
esac
done
if [[ -z "$env" || -z "$version" ]]; then
echo "错误: -e 和 -v 是必填参数"
exit 1
fi
echo "部署环境: $env"
echo "版本号: $version"
如果你参数更多、规则更复杂,我建议直接用bash内置的getopts,它的好处是支持选项合并和错误信息自动输出。但对于大多数脚本,手写case循环已经足够了,而且可读性更好。
5. 常见问题与排查技巧实录
5.1 Shell脚本报错排查速查表
写了这么多年代码和脚本,我把日常遇到最频繁的问题整理成一张表,方便你快速定位。
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| command not found | PATH里没有这个命令,或命令没装 | 用which确认路径,检查export PATH |
| Permission denied | 文件没有执行权限 | chmod +x script.sh |
| bad interpreter: /bin/bash^M | 脚本在Windows编辑过,行尾有CR | sed -i 's/\r$//' script.sh |
| Syntax error: unexpected end of file | if/for等结构没正常闭合 | 检查fi/done是否匹配,或bash -n检查语法 |
| [: too many arguments | 变量没加引号,被当成多个词 | 统一用[[ ]]并加引号 |
| 变量取值为空 | 变量名拼错或未export | 用set -u暴露未定义变量 |
| 循环只执行一次 | for循环里用了$(ls),或管道新建了子shell | 改用find + while read 或进程替换 |
| cp: cannot stat | 文件名带空格或特殊字符 | 所有变量加双引号 |
你可以在写完脚本后,用bash -n script.sh先做语法检查,用bash -x script.sh跟踪每一行执行过程。这两个参数就是排查问题的左膀右臂。
5.2 关于“忽略错误继续执行”的经验
热词里有人搜“shell忽略错误继续执行”,如果你搜的是让脚本在某个命令失败后不中断,那分两种情况。
一种情况是脚本开头写了set -e,它的意思是“任何命令返回非0,脚本立即退出”。如果这时候你希望某个命令失败不影响后续,可以在命令末尾加|| true,等于把退出码强行改成0:
bash复制set -e
rm -rf /tmp/cache || true
echo "继续执行"
另一种情况是脚本没写set -e,但你在管道之间想忽略错误,可以在管道最后一个命令后加|| true,或者把set -e改成set +e临时关闭,执行完再set -e恢复。
我在实际脚本里有个习惯:对于关键步骤(比如数据库备份、配置文件校验)用set -e保证失败即停止;对于非关键步骤(比如清理临时文件、发通知)用|| true处理。这样既不会因为小错误中断流程,也不会让致命错误静默跳过。等脚本写多了之后,这个度你会拿捏得更准。
热词里还出现一个“codex执行是shell权限被拒绝”,这个大概率是你在某个工具里执行Shell命令时,当前用户对目标文件或目录没有执行权限。排查方向:先看当前用户是谁,再看文件属主和权限位,用ls -l确认。
5.3 系统启动进入emergency mode的排查
热词里有“entering emergency mode. exit the shell to continue”,这不是脚本问题,但它和Shell操作息息相关:系统在启动阶段挂载文件系统失败时,会掉进救援模式。常见原因是/etc/fstab里写入了错误的挂载项,或者磁盘UUID变了。
排查思路:在emergency mode输入root密码进入Shell,执行mount -a,看哪一行报错。确认是fstab里哪项的问题后,用vim或sed把错误行注释掉,然后reboot。如果你不熟悉vim,在救援Shell里直接执行cp /etc/fstab /etc/fstab.bak,再执行sed -i 's/^UUID=xxx/#UUID=xxx/' /etc/fstab,把对应行注释掉。这个案例说明一个道理:哪怕不是为了写脚本而学Shell,系统救援时你至少得能看懂错误提示,能改配置文件。
6. 进阶方向:从“会写”到“写得好”
6.1 脚本的性能与健壮性优化
脚本写得多了,你会发现它和写程序一样有性能问题。最典型的几个:循环里频繁调用外部命令、无脑用grep和cat处理大文件、管道链过长导致难以排错。
bash复制# 慢: 循环里一次次启动awk和date
for file in *.log; do
size=$(awk '{s+=$1} END{print s}' "$file")
echo "$(date +%s) $file $size"
done
# 快: 尽量在循环外处理
for file in *.log; do
echo "$file $(stat -c%s "$file")"
done
Shell脚本的性能优化诀窍其实就一条:尽量用shell内置语法和少启动外部命令。因为每执行一次外部命令,都要fork一个子进程,在大规模循环里这是很昂贵的开销。
健壮性方面,建议每个脚本开头都加上这些:
bash复制#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
set -e是有错误就退出;set -u是变量未定义就报错;set -o pipefail是管道中任何一个命令失败都算失败。这三件套能挡住大部分“脚本跑着跑着不知道错在哪”的问题。不过要注意,set -e对条件判断里的命令是豁免的,所以不用怕if判断失败导致退出。
IFS=$'\n\t'是安全处理空格路径的关键。默认的IFS包含空格,会导致for循环时文件名按空格切分,改成只按换行和tab切分后,就能安全遍历带空格的文件名了。
6.2 从脚本到自动化:cron任务调度的正确姿势
脚本写好了,最终要交给系统自动执行。cron是最常用的调度工具。
crontab -e编辑当前用户的任务,格式是“分 时 日 月 周 命令”。比如每天凌晨3点执行清理脚本:
code复制0 3 * * * /opt/scripts/clean_logs.sh >> /var/log/clean_cron.log 2>&1
这里有几个细节值得注意:cron环境变量很少,PATH不一定包含/usr/local/bin,所以脚本里最好用绝对路径或先export PATH;脚本输出如果不重定向,会以邮件形式发送,容易塞爆邮箱;建议每条cron任务都把日志写到固定位置,方便排错。
我还会在脚本里给cron任务加一个简单的锁,避免上一次没跑完,下一次又启动了:
bash复制# 加锁防止重复执行
lock_file="/tmp/clean_logs.lock"
if [[ -f "$lock_file" ]]; then
echo "另一个实例还在运行,退出"
exit 1
fi
touch "$lock_file"
trap 'rm -f "$lock_file"' EXIT
trap的EXIT关键字表示脚本无论正常退出还是异常退出都会执行清理,这个写法是防止锁残留的标准做法。
6.3 关于岗位需求和面试的实话
最后聊聊求职相关的话题。很多人在网上搜“linux shell编程的岗位需求以及知识技能需求”,实际上纯粹写Shell脚本的岗位很少,Shell往往是运维、开发、测试、大数据、云计算等岗位的必备技能之一,而不是独立岗位。
面试常考的Shell知识点也就是那几个:变量和引号、条件判断、循环、函数、find和grep的组合用法、文本处理三剑客的常见场景、cron与文件权限。面试题里经常出现“统计日志中ERROR出现的次数”“找出10天前的日志文件并删除”“用一个脚本完成Nginx日志切割”这类实战题目。
我给个建议:与其背面试题,不如在平时工作中真的写十来个脚本解决实际问题。你能讲清楚一个脚本为什么这么写、踩过什么坑、怎么优化的,比背一百道题有用得多。
6.4 持续学习:从100例到自己的脚本库
热词里有人搜“shell脚本编程100例”,网上确实有很多这类合集。我觉得可以看,但不要沉迷于“刷题”。看例子的正确方式是带着问题看:这个例子解决的是什么问题?用了什么语法?如果我来写,我会怎么写?有没有更好的方式?
我自己的做法是建一个自己的脚本库,按功能分类放:备份类、清理类、部署类、监控类、工具类。每个脚本头部注释清晰,参数说明完整。一段时间后,你会发现很多脚本可以复用到新项目里,写起来越来越快。
另外,热词里还提到“polyworks脚本编程全解”和“lcaros shell extensions”,这些是特定软件或环境里的脚本扩展,底层逻辑和Shell脚本是相通的。能把Shell学扎实,学任何脚本语言都会容易很多。
写在最后
从入门到实战,这条学习路径其实没有捷径,但也不用把战线拉得太长。我个人在带人的时候,通常按照“两周入坑、一个月可写生产脚本、三个月能独立负责部署和巡检”的节奏来推进,其实核心就是多写、多用、多记坑。你现在看这篇文章,不管是在准备面试,还是刚接手一个需要写脚本的临时任务,都不要被各种语法细节吓退。Shell脚本有一个好处是容错率高,写错了大不了跑不来,改就是了,不会损坏系统,也不会造成什么严重后果,放心动手就对了。
