
干Linux运维这些年我带过不少新人也面试过不少人。每次聊到Shell脚本大家第一反应都是语法我看了命令我也记了可真到了让自己写一个能跑的脚本出来脑子就一片空白。后来我慢慢想明白一件事——Shell这玩意儿跟游泳一样你看再多教学视频不下水永远学不会。所以我带人的时候从来不让他们先啃书而是直接扔一套测试题过去做完讲透再逼着他们把题目改造成自己工作里能用的脚本。这套题就是在这种背景下反复打磨出来的今天整理出来分享给正在入门Shell的朋友。它能帮你在最短时间内检验自己到底会不会写脚本、哪块知识有漏洞非常适合刚接触Linux命令行、准备系统学习shell脚本入门的同学。网上经常看到有人求“Shell脚本编程100例”其实我个人的看法是题目贵精不贵多把一套有梯度的测试题彻底吃透比盲目刷一百道重复题有用得多。下面这套题一共8道覆盖了变量、引号、运算、条件判断、循环、函数、管道和文本统计这些最核心的知识点全是初级到中级的经典场景。每道题我都给了参考答案和思路拆解建议你先自己做一遍再对照答案最后按文末的方法把题目改造成真实脚本。1. 先聊聊为什么用测试题学Shell最有效1.1 刷题为什么比看教程上手快我见过太多人学Shell的方式是打开教程从头看到尾边看边抄命令抄完关掉第二天全忘光。原因其实很简单Shell脚本是“动词驱动”的语言它的核心不是语法而是“用命令拼装出解决问题的流程”。你光记住if、for、while的写法没有用真正值钱的是知道“什么场景下该用哪种结构”。测试题恰好逼着你完成这个思考过程。拿一道“判断文件是否存在不存在就创建”的题目来说你的脑子会被迫走一遍用什么命令判断怎么拿条件判断结果用什么命令创建这几个动作串起来才叫“会写脚本”。这种从输入到输出的完整链路看书是看不出来的只有做题才能练出来。另外测试题还有一个隐藏价值它是低成本试错的最佳方式。你在练习题上踩了坑最多花几分钟debug同样的坑要是在生产环境的脚本里踩到可能就是一次事故。所以我现在带团队新同事入职第一周什么都不用干先把这套题做完做完我逐题review确认基础扎实了才允许碰线上脚本。1.2 这套初级测试题覆盖的知识地图这套题的设计不是随手出的每道题对应一类独立的知识点而且难度是阶梯式上升的。我先把知识地图列出来做题之前你先有个整体概念做到哪道题就知道在练什么。题号核心考点难度对应场景第1题变量定义、单双引号区别入门理解shell解析规则第2题位置参数、默认值、退出码入门脚本接收外部参数第3题整数运算、命令替换入门数字计算与赋值第4题文件检测、if条件判断初级备份/部署脚本第5题for循环、字符串截断初级批量文件处理第6题函数定义、return返回值初级模块化脚本第7题管道、sort、uniq中级日志分析第8题grep统计、组合命令中级故障排查可以看到从第1题到第8题是从“单条命令的手感”逐步过渡到“完整脚本的思维”。前面几题练的是语法细节后面几题练的是命令组合能力。我建议你按顺序做别跳题因为每道题都会用到前面题目的知识点跳着做容易卡壳。2. 基础送分题变量、引号与运算练的是手感2.1 第1题变量赋值与单双引号的区别题目定义一个变量name赋值为Linux Shell分别用双引号和单引号输出Hello, $name要求解释两种输出的差异。参考答案#!/bin/bash nameLinux Shell echo Hello, $name echo Hello, $name实际输出Hello, Linux Shell Hello, $name思路拆解这道题考察的是shell对引号的解析规则属于面试笔试里最高频的基础题。双引号是“弱引用”它不会阻断变量展开所以$name会被替换成变量的值输出Hello, Linux Shell单引号是“强引用”里面的内容原样输出所以$name就老老实实显示成$name。这里要额外提醒一点很多新手以为引号只是“写着好看”不重视结果在写脚本时把路径变量用单引号包起来导致变量压根没展开脚本跑出各种诡异的结果。记住一句话单引号里是什么就输出什么双引号里能展开变量和命令。另外还有个易错点变量赋值nameLinux Shell等号两边绝对不能有空格写成name Linux Shell会直接报command not found。原因其实很好理解——shell里以空格分词name会被当成一条命令去执行当然找不到。2.2 第2题位置参数和默认值提取题目写一个脚本args.sh要求不带参数运行时输出参数为空带一个参数运行时输出参数是: xxx带多个参数时输出所有参数拼接结果。参考答案#!/bin/bash if [ -n $1 ]; then echo 参数是: $1 else echo 参数为空 fi echo 所有参数: $*运行效果$ ./args.sh 参数为空 所有参数: $ ./args.sh hello 参数是: hello 所有参数: hello $ ./args.sh hello world 参数是: hello 所有参数: hello world思路拆解位置参数是脚本和外界交互的基本方式。$1是第一个参数$2是第二个以此类推$0是脚本自身的名字$#是参数个数$和$*都是所有参数区别在于$会把每个参数当成独立字符串$*会把所有参数拼成一个字符串这个区别在处理带空格的参数时特别重要。这道题我故意用了[ -n $1 ]而不是[ $1 ]这里藏着一个经典坑[ ]里如果不加引号当$1为空时条件会变成[ -n ]结果永远是“真”脚本就永远走不到参数为空那个分支。这也是为什么我一直强调变量在条件判断里一定要加引号除非你特别清楚自己在做什么。做这道题时你还可以顺手练一下${1:-默认值}这种写法它能在变量为空时给一个默认值写脚本时非常常用。2.3 第3题整数运算的三种实现方式题目定义变量a8、b3分别用$(( ))、expr、let计算a 加 b 的和并输出结果。同时要求计算出a除以b的整除结果和余数。参考答案#!/bin/bash a8 b3 sum$((a b)) echo sum$sum echo expr sum$(expr $a $b) let diva/b let moda%b echo div$div mod$mod运行效果sum11 expr sum11 div2 mod2思路拆解$(( ))是bash内置的算术展开写起来最顺手也是我最推荐的方式因为语法简单、支持变量名不带$符号还能写、这些运算符日常计算完全够用。expr是外部命令它有两个麻烦的地方一是运算符和数字之间必须用空格隔开写成expr $a$b会被当成字符串拼接二是它会把*当作通配符做乘法时必须转义成\*很容易踩坑。let是个bash内建命令不需要加$取结果但它有返回值当计算结果为0时返回1在set -e模式下会导致脚本直接退出这是一个隐蔽的坑。highlight一个细节整除运算要特别注意 Shell 里的除法是整数除法8/3结果是2而不是2.666...余数用%拿。如果要做小数运算就不能用上面这些方法了得借助bc或awk比如echo 8 / 3 | bc -l结果才是2.666...。很多初级脚本写着写着发现数字变成0就是栽在这个没有小数点的整数运算上。3. 流程控制实战题判断、循环与函数3.1 第4题文件检测与条件备份题目写一个脚本判断/var/log/syslog是否存在。如果存在把它复制成/var/log/syslog.bak.日期日期格式为YYYYMMDD如果不存在输出日志文件不存在并不做任何操作。参考答案#!/bin/bash log_file/var/log/syslog if [ -f $log_file ]; then backup_file${log_file}.bak.$(date %Y%m%d) cp $log_file $backup_file echo 备份完成: $backup_file else echo 日志文件不存在: $log_file fi思路拆解这是典型的备份脚本里最核心的一段逻辑考查点有两个。一是文件检测标志-f判断是否是普通文件-d判断是否是目录-e判断路径是否存在-r、-w、-x分别判断可读、可写、可执行。这些标志看起来简单但很多人会把-f和-e搞混实际场景里“存在但不一定是普通文件”的情况非常多比如/dev/null存在但它是字符设备文件-f判断就是假的。第二个考查点是命令替换$(date %Y%m%d)。这个语法在脚本里太常用了凡是需要把命令输出存到变量里的地方都用它比如获取当前时间、获取IP、获取磁盘使用率。早期有一种写法是反引号date %Y%m%d功能一样但可读性差嵌套的时候还容易出错新写的脚本建议统一用$()。做这道题的时候你可以顺手扩展一下备份完成后用find /var/log -name syslog.bak.* -mtime 30 -delete把30天前的旧备份清掉这样一个生产环境可用的日志归档脚本就出来了。这种“从题目到工具”的改造是提升脚本能力最有效的一条路。3.2 第5题for循环批量重命名文件题目当前目录下有一批文件file1.txt、file2.txt直到file100.txt写一个脚本把所有这些文件的扩展名从.txt改成.bak。参考答案#!/bin/bash for file in *.txt; do mv $file ${file%.txt}.bak done思路拆解这道题考察的是循环和字符串截断的组合应用。for file in *.txt这行代码干了两件事先用通配符*.txt做路径名展开把当前目录下所有.txt文件列出来再让file变量依次拿到每个文件名。这个写法比for file in $(ls *.txt)安全得多后者在文件名带空格时会把一个名字拆成两个而且ls的输出在脚本里做循环本身就不是好习惯。关键考点是${file%.txt}这个参数展开。%表示从字符串右边开始删除匹配的最短内容${file%.txt}就是“把file变量末尾的.txt删掉”所以file1.txt会变成file1再加上新的.bak后缀就成了file1.bak。与之对应的还有#表示从左边删%%、##表示删除最长匹配这些符号在工作里都非常实用建议一次性记清楚。另外我想强调一下mv $file ...里的引号。写脚本给变量加双引号是基本功它能保证变量值里有空格或特殊字符时整个值被当成一个整体。你不加引号遇到名字里有空格的文件时mv会以为文件名被拆成了多个参数脚本当场崩给你看。这不是“可能出错”的问题而是“迟早出错”的问题。3.3 第6题函数封装与返回值判断题目写一个函数is_even接收一个整数参数如果是偶数返回退出码0如果是奇数返回退出码1。在脚本主逻辑里调用它输出对应的判断结果。参考答案#!/bin/bash is_even() { local num$1 if [ $((num % 2)) -eq 0 ]; then return 0 else return 1 fi } if is_even 8; then echo 8 是偶数 else echo 8 是奇数 fi运行效果8 是偶数思路拆解函数是脚本从“一次性代码”走向“可复用代码”的分水岭。这道题有三个考点。第一是local关键字函数内部的变量用local声明函数退出后变量自动失效不会污染全局命名空间。很多新手不写local函数里用了个num结果函数外面同名的变量被悄悄改掉排查半天找不出原因。第二是return的机制。Shell函数没有真正意义上的返回值return 0到return 255只是设置这个函数的退出码“返回值”是通过退出码传出来的。第3题里提过let命令计算结果是0时退出码也是0这里要习惯这种“0代表成功非0代表失败”的约定。第三是if is_even 8; then这种直接把函数调用当条件的写法。if后面跟的是一条命令判断依据是这条命令的退出码不是表达式的真假。理解了这一点你就能看懂为什么可以直接在if里写grep -q pattern file、ping -c 1 host这类命令。这道题彻底吃透的话你再看别人的脚本就不会觉得函数部分神神秘秘了。4. 中级挑战题管道、文本处理与日志分析4.1 第7题提取日志中的请求路径并统计题目有一个访问日志access.log每行格式为IP地址 [访问时间] 请求路径 状态码。写一条命令或脚本统计每个请求路径被访问的次数并按次数从高到低排序输出。模拟日志内容192.168.1.10 [2025-03-01] /index.html 200 192.168.1.11 [2025-03-01] /api/login 200 192.168.1.10 [2025-03-01] /index.html 200 192.168.1.12 [2025-03-01] /admin 404 192.168.1.11 [2025-03-01] /api/login 200参考答案awk {print $3} access.log | sort | uniq -c | sort -rn运行效果2 /index.html 2 /api/login 1 /admin思路拆解这道题是日志分析的基本功面试里出现概率极高。整个链路用到了管道把四段命令串起来每一段做一件简单的事。awk {print $3}提取每一行的第3个字段也就是请求路径sort把相同的路径排到相邻位置这一步必须做否则uniq -c无法正确合并统计uniq -c统计重复行数量在每行前面加上访问次数最后的sort -rn按数字从大到小排序r是倒序n是让sort按数值而不是字典序排序。这里有个特别容易栽的坑如果不加nsort -r默认按字典序排序结果会是9排在10前面这不是你想要的。我见过很多人写日志统计脚本最后的排序结果永远不对就是栽在这一步。另外提醒一下access.log的字段位置不是固定的以你自己真实的日志格式为准把$3换成对应的字段序号就行。处理更复杂的日志时awk里还可以用正则做字段匹配比如awk $4 ~ /^4/ {print $3}提取所有状态码以4开头也就是4xx错误的请求路径。4.2 第8题统计访问TOP IP和404次数题目继续使用同一个access.log完成两个统计一是输出访问量排名前3的IP地址二是统计404状态码出现的总次数。参考答案#!/bin/bash echo 访问量TOP3 IP: awk {print $1} access.log | sort | uniq -c | sort -rn | head -3 echo 404出现次数: grep -c 404 access.log运行效果访问量TOP3 IP: 2 192.168.1.10 2 192.168.1.11 1 192.168.1.12 404出现次数: 1思路拆解第8题是第7题的延伸考点在于命令组合和细节处理。head -3截取排序后结果的前3行这是“取TOP N”的标准套路。grep -c 404 统计包含404的行数注意我在404前后都加了空格这是刻意为之——不加空格的话它会连1404、4040这种状态码一并匹配结果就偏了。这种对真实数据格式的敏感度恰恰是脚本工程师和只会抄命令的人之间的差距。此处还有一个隐藏考点就是404判断不够严谨。grep -c 404 只适合这种演示日志。实际生产日志里状态码格式可能不完全固定比如行尾的404后面没有空格或者状态码和其他字段之间是多个空格这时候grep 404 就会漏统计。更稳妥的做法是用awk {print $4} access.log | grep -w 404 | wc -l或者干脆awk $4 404 access.log | wc -l。做题的时候多想想“如果真实数据不是这么规整怎么办”这是从刷题走向实战最关键的一步。这里再推荐一个高频组合统计某时间段内的错误数grep 2025-03-01 access.log | grep -c 404 。先按时间过滤再按状态码统计日常工作排查突发故障时会频繁用到。5. 做题过程中最常见的坑与排查方法5.1 几个高频语法错误空格、引号、运算符刷这套题的时候我发现大家掉进坑里的位置出奇地一致这里集中整理一下。第一是空格问题。if [ $a -eq 1 ]这个写法里[是一个命令不是语法符号所以[和条件之间必须有空格最后的]前面也必须有空格。写成if[$a -eq 1]或者if [ $a -eq 1]都会报错。我第一次带新人时光这一个错误一天能看到十次懂了原理之后就再也不犯了。第二是和混用。在[ ]里做字符串比较应该用不是在[[ ]]里两者都可以但更符合其他编程语言的习惯。我个人的建议是字符串相等比较统一用[[ $1 start ]]整数比较用-eq这样既清晰又少踩坑因为[[ ]]里你还能直接写正则匹配比如[[ $path *.log ]]。第三是$?用歪了。$?是上一条命令的退出码但很多新手写完一条命令后中间又执行了一堆别的语句回头再看$?取到的已经是别的命令的退出码了。正确姿势是用完立刻存进变量比如cp source dest; result$?之后再怎么折腾都没关系。5.2 用 bash -x 和 set 选项快速定位逻辑错误语法错误好排查报错信息直接告诉你在哪一行。逻辑错误才真的让人头大脚本能跑但结果不对这时候就得靠调试手段。我最常用的是bash -x script.sh它会逐行打印脚本执行过程并在每行前面显示变量展开后的结果加号的标准输出一眼就能看到逻辑走到哪一步、变量变成什么值了。打个比方bash -x相当于给你装了透视镜能看到脚本在后台干的每一件事。比如第2题的判断语句如果[ -n $1 ]里的引号没加用bash -x跑一遍会看到执行结果是[ -n ]而不是[ -n hello ]问题立刻水落石出。除了bash -x还有一个手段是在脚本开头加set -euo pipefail。-e让脚本在任意一条命令失败时立即退出-u让变量未定义时报错-o pipefail让管道里任何一段失败都算失败。这个组合能让很多隐藏的问题提前爆出来不过要提醒一句-e在某些场景下会误伤比如grep没匹配到内容返回非0会导致脚本退出所以用的时候要清楚副作用建议调试期先开上线前根据情况决定是否保留。5.3 报错提示速查表做题踩坑最怕的就是“报错看不懂”。我把这套题里最常冒出来的报错整理成表格你下次再遇到直接对号入座。报错信息常见原因解决方法command not found变量赋值时等号两边有空格或命令名拼错检查等号两侧检查变量名拼写用type 命令名确认命令存在syntax error near unexpected tokenif/for 后缺;或then独占了单独一行没加分号把if和then写一行要加分号或把then另起一行二选一[: missing ][ ]判断时右括号前没有空格改成if [ 条件 ]; then注意最后]前必须有空格unary operator expected条件判断里的变量为空且没加引号给所有变量加双引号如[ $name abc ]Permission denied脚本没有执行权限chmod x script.shexpr: syntax errorexpr的运算符和数字之间没空格改成expr $a $b乘法记得转义\*这张表是我把新人踩坑记录汇总出的结果基本覆盖了初级到中级脚本会碰到的九成报错。你现在做完题如果中招了对照表格改一遍这个坑就算正式迈过去了。6. 题目做完之后怎么继续往上走6.1 把测试题改造成能落地的真实脚本这套题做完只代表你有了基础能力离“能解决实际问题”还差一个改造环节。我一直跟朋友说写脚本最亏的做法是重复造轮子最赚的做法是把做过的题改造成自己的工具。比如第4题的备份逻辑你完全可以扩展成一个带日志、带清理策略的备份脚本放到 crontab 里每天自动跑。比如第7题的日志统计每天凌晨跑一次把结果追加到一个汇总文件里月底看趋势图。我给新人布置过一个标准改造任务用第4题的逻辑写一个“数据库每日备份脚本”要求备份文件名带日期、备份记录写入日志、30天前的备份自动删除、失败时发送告警。做完这道改造任务其实你就已经具备写生产脚本的雏形能力了。改造的过程会逼着你查命令文档、思考异常情况、处理真实路径里的各种意外这比再刷50道固定题目有用得多。6.2 用工具辅助写作少踩格式坑很多朋友问过我一个挺有意思的问题有没有跟编程工具一样的Shell脚本工具答案是不仅有而且很多。我建议至少配两样一是编辑器VS Code 装上 Shell 插件就够用语法高亮、括号匹配、局部运行都能做到二是 ShellCheck这是一个静态检查工具能指出脚本里的语法错误、逻辑隐患和不推荐写法。它的准确率高得惊人很多新人的脚本第一版都是先丢给 ShellCheck 扫一遍把明显的问题清掉再拿给我 review。我现在的日常流程是先写再跑 ShellCheck然后bash -x看执行过程最后小规模数据验证。这一套流程下来绝大多数问题在提交之前就暴露了。工具不是万能的ShellCheck 也看不懂你的业务逻辑但它能帮你把“语法粗糙、容易翻车”的部分先打磨掉剩下的重点放在真正的业务实现上。6.3 我个人带人练Shell的一点体会最后说点我自己的体会。我带过的人里有学得特别快的也有卡在原地很久的区别往往不在天赋而在一个习惯拿到一个任务是直接开始敲命令还是先想清楚“我要分哪几步完成”。Shell脚本的本质是“把任务翻译成命令流程”所以写之前多花两分钟想想步骤比写完之后反复调试省的时间多得多。我自己当初学Shell全靠一次一次的“痛苦尝试”现在回头看很多效率极低的弯路完全可以靠一套有体系的题目避开。这套初级测试题的价值不在于它有多难而在于它把Shell最核心的思维方式和最容易踩的坑浓缩在一份可控的练习里。你做的时候别追求“秒答”每道题都折腾一下哪怕做错了再查再改那个折腾的过程才是真正长本事的地方。做完之后再从这些题目里挑一个自己工作里最相关的场景把它改造成真实脚本跑上一周你对Shell的感觉会完全不一样。如果后面有机会我再把中高级的坑——数组、关联数组、getopts、进程替换、文件描述符这些整理成一版测试题那套题更烧脑但练完之后写复杂脚本基本就不怵了。先把这套基础题啃透比什么资料都管用。