1. 为什么2026年还要啃Shell这门手艺
1.1 所有自动化都跑在Shell的肩膀上
做了十来年运维和脚本开发,近几年越来越多朋友问我同一个问题:现在都有了各种自动化平台、可视化运维系统,甚至AI都能写代码了,还有必要花时间学Linux Shell编程吗?
我的回答一直很干脆:有必要,而且越早学越值。2026年了,云原生、容器、CI/CD满天飞,但只要你把任何一个自动化流程解剖到底,最后真正在服务器上干活的,依然是那一行行Shell命令和Shell脚本。部署平台点一下按钮,底层执行的是Shell;容器启动要跑初始化逻辑,入口多半是Shell脚本;定时备份、日志切割、故障巡检,这些活儿在绝大多数公司里到现在还是Shell脚本在扛。Shell不是被淘汰的老技术,它恰恰是所有现代自动化体系最底层的那个"地基"。
对想进入运维、后端、嵌入式甚至是数据分析领域的人来说,Shell编程是那种"看起来不显眼,但一遇到实际问题就要靠它救命"的能力。它不需要装额外环境,几乎每一台Linux服务器都自带,学完就能用,性价比极高。这篇文章是我基于自己这些年实际踩坑和日常使用整理出来的快速入门路线,针对的是2026年还在用Linux、还在和命令行打交道的朋友,尤其是零基础想系统入门的初学者。
1.2 从敲命令到写脚本,转变的是编程思维
很多新手会有个困惑:我会敲几十条Linux命令了,为什么还是写不出脚本?
这是非常正常的阶段。敲命令本质是"一次性的操作",就像你拿起刀切了一根黄瓜;写脚本则是"把整个做菜流程编排成工序",你要考虑先放油还是先放菜、火候怎么控制、什么时候出锅。两者差的不是命令数量,而是编程思维。
Shell脚本的核心思维其实只有三件事:把重复的操作参数化、把判断和循环加进去、把错误处理补上。举个很常见的场景:你有100个文件要统一重命名,如果一个个敲mv命令,手疼不说还容易出错;写成脚本后,一个for循环全部搞定。同一个需求,从"手工敲命令"到"拿脚本跑",效率提升是数量级的,这就是学Shell编程真正的收益所在。
1.3 我能用Shell做什么:典型应用场景
很多初学者不知道该往哪用力,我先给你列几个Shell编程的高频实际场景,你心里有个数:
- 批量操作:批量重命名文件、批量修改配置、批量打包日志。
- 系统巡检:定时检查CPU、内存、磁盘占用率,超阈值就告警。
- 数据备份:把数据库导出、压缩、按日期命名、上传到远端存储。
- 日志处理:从几十GB的日志里grep出关键错误,再统计出现次数。
- 一键部署:把拉代码、装依赖、重启服务、健康检查这些步骤串成一个脚本。
- 自动化测试:在CI流程里做接口冒烟测试、环境准备、结果清理。
这些需求几乎每个用Linux的团队都会碰到。你不需要等自己"学完了"再动手,完全可以一边学一边写,遇到什么需求就写什么脚本,这才是最快的进步方式。
2. Shell脚本的语法骨架:变量、参数与控制流
2.1 变量与作用域:别小看export那点事
Shell编程的入门第一课永远是变量。它的语法看起来简单,但坑也不少。
定义变量和取值的标准写法如下:
# 定义变量:等号两边绝对不能有空格 name="zhangsan" version="2026" echo "Hello, $name, version is ${version}"这里必须强调一个新手最容易犯的错:等号两边不能留空格。name = "zhangsan"这种写法在别的语言里是赋值,到了Shell里就会被拆成三个词,系统会试图把name当成命令去执行,直接报错或者产生诡异行为。我当年就因为这个卡了很久。
关于引号,规则是:
- 双引号里的变量会展开,比如
"Hello, $name"输出的是Hello, zhangsan。 - 单引号里的内容原样输出,
'Hello, $name'会原封不动打出Hello, $name。 - 不加引号时,遇到空格或特殊字符容易被拆词,所以字符串尽量加双引号是稳妥习惯。
再说说export。很多初学者一开始不理解,为什么我在脚本里定义了一个变量,脚本跑完了再看终端里还是空的。
# test.sh export MY_VAR="hello" # 在终端执行 ./test.sh echo $MY_VAR # 输出空原因是:每执行一个脚本,系统都会启动一个子Shell进程,脚本里定义的变量只在子进程里有效,脚本一结束进程销毁,变量也就没了。export的作用不是让变量"跑到父进程去",而是把变量标记为环境变量,这样它可以在当前进程的所有子进程里被继承。
这个知识点特别重要。比如你要在脚本里调用另一个脚本,希望对方也能读到你的变量,就必须export。但如果只是脚本内部临时计算用的变量,不导出也没有问题。用生活类比来说:export相当于你给高铁票办了个"行李直挂",儿孙后代(子进程)都能拿到;不export就像是手提行李,自己拎着走,孩子上车前你还得亲手递给他。
2.2 参数处理:shift与位置参数的灵活应用
写脚本和敲命令最大的不同之一,就是脚本要接收参数。Shell里用位置参数来处理:
$0:脚本名本身$1、$2、$3:第1、2、3个参数$#:参数总个数$@:所有参数列表,每个参数独立$*:所有参数,当成一个整体字符串
举个实际例子。假设我要写一个脚本,它可以接收任意多个文件名,逐个显示类型信息:
#!/bin/bash echo "脚本名: $0" echo "参数个数: $#" for file in "$@" do echo "检查文件: $file" file "$file" done这里有个关键点:遍历参数时务必写"$@",不要裸写$@或$*。加双引号能确保参数里哪怕含有空格、特殊字符,也能被当成一个整体,不会意外拆分。如果你还没理解,可以想想一个文件名叫my documents.txt,不加引号它就会裂成两个词,处理逻辑直接就崩了。
再讲shift。这个命令是让你把参数往左"挪"一位:原来$2变成$1,$3变成$2,同时$#减1。它特别适合用while循环一个一个消费参数,尤其是处理"选项+值"的场景。
#!/bin/bash while [ "$#" -gt 0 ] do case "$1" in -f|--file) shift file="$1" ;; -d|--dir) shift dir="$1" ;; *) echo "未知参数: $1" exit 1 ;; esac shift done echo "文件: $file" echo "目录: $dir"用shift的好处是,无论参数顺序怎么变,都能逐个处理,不用写一大串复杂的索引逻辑。我在写通用脚本时几乎天天用,属于那种"会了之后离不开"的技能。
2.3 for循环与流程控制:让脚本真正自动化起来
没有循环和判断的脚本,顶多算"多条命令的合集"。加上流程控制,它才配叫"编程"。
for循环是Shell里使用频率最高的循环,常见三种写法:
# 写法1:基于列表 for name in nginx redis mysql do echo "服务: $name" done # 写法2:基于范围(Bash支持数字序列) for i in {1..10} do echo "第 $i 次" done # 写法3:C语言风格 for ((i=1; i<=10; i++)) do echo "数值: $i" done第一种最常用,特别适合遍历文件列表。之前说的批量重命名,核心就是它:
for f in *.txt do mv "$f" "${f%.txt}.bak" done第二种的{1..10}很方便,但如果数字很大或者需要更复杂的步长,就建议用第三种C风格写法,性能更好、可控性更强。
while循环常用于需要"持续读入"的场景。比如逐行读取文件:
while IFS= read -r line do echo "内容: $line" done < /etc/hosts注意这里的IFS= read -r是固定搭配,-r防止反斜杠被转义,IFS=保证行首行尾的空格不被吃掉。这行代码本身有点抽象,你直接当成"安全逐行读文件"的模板背下来就行。
if判断则是所有逻辑的地基:
if [ -f "$1" ] then echo "$1 是一个文件" elif [ -d "$1" ] then echo "$1 是一个目录" else echo "$1 不知道是什么" fi判断文件类型的-f、-d、-e这些选项要重点记,日常判断文件、目录、是否存在全靠它们。另外,用单中括号[ ]时,两边必须有空格,[ "$1" -f ]这种顺序错、空格错,脚本就会报奇怪的错。推荐直接改用双中括号[[ ]],它支持更多的逻辑操作符(比如&&、||、正则匹配=~),而且对空格宽容度高:
if [[ -f "$1" && "$1" == *.log ]] then echo "这是一个log文件" fi我个人的经验是:所有新脚本一律用[[ ]],只有需要兼容老旧的sh环境时才用[ ]。
3. 常用命令与脚本实战:从重命名到巡检
3.1 绕不开的高频命令清单
写Shell脚本离不开系统命令。我按使用频率给你挑一份"入门必会清单",每一类挑最核心的讲:
文件与目录操作:ls、cd、cp、mv、rm、mkdir、touch。这些是基本功,重点注意rm -rf的危险性,脚本里使用需要格外小心,最好先加判断。
文本处理三剑客:grep、sed、awk。这才是Shell脚本里的重头戏。
grep负责筛选行:grep "ERROR" app.log能找出所有包含ERROR的行;加-c统计行数,加-v反向匹配,加-E启用扩展正则。sed负责替换和编辑:sed -i 's/old/new/g' file.conf是原地替换;sed -n '20,30p' file.log打印指定行范围。awk负责列处理和统计:awk '{print $1, $NF}' access.log打印第一列和最后一列;awk -F: '{sum+=$3} END{print sum}' /etc/passwd还能做求和统计。
这三者配合起来,能完成90%的文本处理需求。举一个综合场景:统计一个日志文件里每个IP出现的次数:
awk '{print $1}' access.log | sort | uniq -c | sort -rn这条命令在真实排障中我用过无数次。它的思路是:先取第一列IP,排序,去重并统计次数,再从大到小排序。一跑出来后,哪个IP访问最频繁,一眼就知道。
查询类:find、xargs、du、df。find按时间、名称、大小找文件;du -sh *看每个目录体积;df -h看磁盘空间。它们结合起来可以实现很多自动化运维需求。
进程与服务类:ps、kill、systemctl。ps -ef | grep nginx查进程,配合grep筛选;systemctl status/restart/stop管理系统服务。写部署脚本时,这些命令几乎必用。
需要多说一句的是,很多人在手机上可能见过adb shell这种写法——安卓调试里也有一个Shell环境,但它的底层思想上和Linux Shell同源,命令风格也很接近。如果你以后要写安卓自动化脚本,现有Linux Shell基础同样能平移过去。不过我们这里先聚焦服务器场景,毕竟那是Shell的主战场。
3.2 脚本实战:批量重命名文件
讲再多理论知识,不如完整写一个脚本。我选一个最常见的需求:给一批文件统一加前缀或改后缀。
假设目录里有photo1.jpg、photo2.jpg……一直到photo20.jpg,我想把所有.jpg重命名为2026_photoN.jpg,前面加上年份前缀。
#!/bin/bash # rename.sh - 批量给jpg文件加前缀 for f in *.jpg do # 提取不带后缀的文件名 base="${f%.jpg}" # 新文件名 new_name="2026_${base}.jpg" # 如果已经改过名则跳过 if [[ "$f" == 2026_* ]] then echo "跳过 $f(已处理)" continue fi mv "$f" "$new_name" echo "已重命名: $f -> $new_name" done脚本里几个点值得琢磨:
${f%.jpg}是Shell的变量模式删除语法,%表示从右侧删除最短匹配,这样可以把后缀剥掉。[[ "$f" == 2026_* ]]用于判断是否已经是新名字,避免重复跑脚本时二次加前缀。- 所有变量都加双引号,防止文件名带空格导致执行出错。
这个脚本看似简单,但它的结构就是很多"批量处理脚本"的原型。你在它的基础上换成.png、.log、.conf,或者改成删除特定前缀,就是能直接用的工具。我自己通常还会再包一层if [ -f "$f" ]判断,保证目录里没有匹配文件时不会直接报错。
3.3 脚本实战:5分钟写一个系统巡检脚本
另一个高频需求是服务器巡检。每次登录服务器都要敲free看内存、敲df -h看磁盘,太累了。写一个脚本把信息一次性打出来:
#!/bin/bash # check.sh - 系统健康快速巡检 echo "========== 系统负载 ==========" uptime echo "========== 内存使用 ==========" free -h echo "========== 磁盘使用 ==========" df -h | grep -vE 'tmpfs|udev' echo "========== CPU占用TOP5 ==========" ps aux --sort=-%cpu | head -6 echo "========== 当前监听端口 ==========" ss -tlnp 2>/dev/null | head -10这个脚本里的命令都是我在服务器上手敲频率最高的几类,把它们合并到一个脚本之后,登录机器只需要执行一次./check.sh,所有关键信息一览无余,省时又省心。
你还可以搭配crontab实现定时巡检和告警。比如每天凌晨2点执行一次,异常时把信息发到自己的邮箱或机器人。第一步很简单,写一个凡是有问题就echo警告的脚本,第二步再考虑接通知系统。在这些真实小项目上不断叠加功能,你的Shell水平会比闷头看书涨得快得多。
4. 踩坑与排查:Shell编程的隐形陷阱
4.1 空格与引号的战争
Shell脚本报错里,我见的第一个高频问题就是空格位置不对。它不是语法错误,而是逻辑错误,而且经常不报错,只是结果不对。
最经典的三个位置:
- 赋值等号两边不能有空格:
name = "zhangsan"会尝试执行name命令。 [ ]判断时括号内部要有空格:[$1 = "a"]会报错,正确是[ "$1" = "a" ]。- 命令替换中的引号:
filename=$(ls -la)里的$()和反引号都可以做命令替换,但推荐用$(),它支持嵌套、可读性也好。
再强调一次引号。很多人看到mv $f $new_name不报错就放松了,真遇到带空格的文件名才明白什么叫"当场裂开"。一句话:在脚本里引用变量,默认都加上双引号,除非你明确知道要分词。这是全篇最值得记住的一条经验。
4.2 退出码与条件判断的坑
Shell里每个命令执行完都会留下一个退出码,0表示成功,非0表示失败。这个机制直接影响if的判断逻辑,但新手常常被坑。
坑点一:if判断的是命令的退出码,不是表达式的真假。if grep -q "ERROR" log.txt意思是"如果grep成功找到,就进入then分支"。理解这一点,你就能明白为什么if [ ... ]里的括号本质是一个"有退出码的命令"。
坑点二:脚本中最后一条命令的退出码决定脚本的退出状态。如果你在脚本最后执行了一条失败命令,整个脚本的退出码就是非0,这在CI流水线里会被判定为"失败"。解决办法之一是在脚本开头加set -e,让任意命令失败时立刻退出。
但set -e也有坑。如果你写cmd1 | cmd2,管道中某一环失败时set -e不一定能抓住。想更严格,可以配合set -o pipefail。但set -e会让某些"预期内失败"直接中断脚本,比如grep没找到内容时返回非0,如果不希望脚本退出,需要加|| true。我的习惯是:脚本前期调试用set -ex(x会打印每条执行的命令),跑稳定后再把x去掉。
坑点三:$?只能取上一次命令的退出码。如果你先执行echo再判断$?,取到的已经是echo的退出码了。要保存就得立刻赋值:
grep -q "ERROR" log.txt result=$? # 后续可以放心使用 $result4.3 Shellcheck与调试技巧推荐
排查Shell脚本问题,光靠肉眼盯实在效率低。我强烈建议使用ShellCheck工具。它可以静态分析你的脚本,找出变量引用问题、可能的引号错误、权限问题等。很多发行版的软件源里直接有,装上后一条命令跑完就能看到"你脚本里可能有哪几个坑",对新手极其友好。
执行方式很简单:
shellcheck myscript.sh它会输出类似"SC2086: Double quote to prevent globbing and word splitting"这样的提示,新手可能看不懂英文说明,但只要看到"Double quote",就知道该给变量加引号了。
除了工具,还有一个我自己经常用的笨办法:用bash -x运行脚本。它会逐行打印脚本实际执行的命令和展开后的变量值,一眼就能看出变量是不是为空、判断是不是走了错误分支。执行方式:
bash -x check.sh如果脚本很长,输出太多,你可以只在可疑的区域前后加set -x和set +x,把调试信息集中在局部。这个方法救了我不下几十次。
5. 面试题与学习路线建议:让别人看出你懂Shell
5.1 面试常问的Shell考点解析
很多做运维、后端、测试的朋友都会遇到Linux面试题,Shell编程部分是面试官非常喜欢深挖的区域。我根据近几年被反复问的问题,挑几个典型的说一说。
问题1:如何把一个目录下所有.log文件按大小排序后,各取前几行内容?
典型回答:
find /path/logs -name "*.log" -exec ls -s {} \; | sort -rn | head -5 | awk '{print $2}' | xargs head这道题考的是命令组合能力。find负责定位,sort负责排大小,head取前5,awk取文件名,xargs将文件名传给head。整条命令一气呵成,很能体现熟练度。
问题2:如何判断某个服务进程是否存在?
一种常见写法:
if pgrep -x nginx > /dev/null then echo "nginx正在运行" else echo "nginx未运行" fi考的是你能不能跳出"用ps硬grep然后用awk解析"的野路子,用pgrep这种专为场景设计的命令。同时/dev/null把不必要的输出丢了,这也是一种好习惯。
问题3:写脚本统计access.log里各URL的访问次数并取前10。
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10第七列通常是URL路径(Nginx默认格式),剩下的模式跟之前统计IP一模一样。这个思路在面试时摆出来,面试官基本能确认你是随手能写脚本的人。
面试题不用背,本质是检查你是否真的用Shell解决过问题。你日常写的脚本越多,这类问题越答得来。
5.2 从入门到进阶:推荐的学习路径
如果你是完全零基础,我建议按照这个路径走,节奏相对合理:
- 第一阶段:Linux基础命令(1-2周)。把
ls、cd、cp、mv、rm、grep、chmod这些高频命令练熟,达到"你想让电脑干什么,能想到对应命令"的水平。 - 第二阶段:Shell脚本语法(2-3周)。掌握变量、引号、
$@和$#、if、for、while、case。每天写一个10行动的小脚本,比如批量创建用户、批量改扩展名。 - 第三阶段:文本处理三剑客(3-4周)。重点学
grep、sed、awk,配合正则表达式。建议拿真实日志练手,比如统计访问最多的IP、筛选慢查询日志。 - 第四阶段:实战项目(持续)。找一个真实痛点写完整工具,比如日志备份脚本、服务异常自动重启脚本。把之前学的东西串起来。
不要急着看高深技巧,Shell编程里真正决定水平的是"你处理真实问题时的思路",而思路只能从实际写脚本里获得。
5.3 一点忠告:别背命令,背思路
我在不少交流群里看到有人晒"背过的命令清单",说实话,我不太建议那样子学Shell。命令是背不完的,Linux里的命令成千上万,每个命令参数又有一堆。真正有用的是解决问题的思路。
同样面对"找出日志错误并统计"这个需求,思路熟练的人脑子里会立刻浮现出:先grep筛选,再sort、uniq -c统计,最后sort -rn排序。这个链条一旦形成,用什么命令、带什么参数都是顺手的。如果思路不清楚,就算你把awk所有参数都背下来,看到实际问题还是不知道从哪里下手。
我自己的做法是:每遇到一个问题,先想清楚"我从原始数据走到目标结果中间需要做哪几步转换",然后再逐步落实成命令。Shell脚本的写法不唯一,但分解问题的能力是通用的。学会把大问题拆成几个小步骤,这不仅是Shell编程的康庄大道,也是所有编程领域的通用心法。
最后再分享一个小习惯:我会在个人电脑的笔记目录里放一个"脚本工具箱"文件夹,每写一个能复用的脚本就丢进去,并配上一句话说明注释。几个月下来,你会拥有一套真正属于自己的Shell工具集,碰到类似问题直接改造复用,省下来的时间远超当初学习投入的时间。