干运维和开发这些年,我面试过不少候选人,也帮朋友的公司出过技术笔试题。说实话,能扛住逻辑题、算法笔试的人不少,但一到Shell这块,很多人就露馅了:能写出for i in $(cat file)的循环,却不知道xargs怎么限并发;会用grep -v过滤数据,见到 JSON 日志就傻眼。标题里说的"Shell进阶必会的几个工具",其实就是我把关候选人时最常考的盘面——jq、awk、sed、xargs、sort、uniq、tr这一挂。这篇不是从零讲命令参数,而是直接对应企业里真实考核点,把每个工具"面试官希望你掌握到哪一层"讲清楚。准备面试的、打算把脚本写得更专业的,都可以照着这份清单自查一遍。
1. 先把"进阶"这个词想清楚:你离企业要求的Shell能力差在哪
很多人觉得Shell进阶就是会写函数、会用 case 分支、背几个[[ ]]和$()的用法。这些语法层面的东西当然重要,但企业里真正的"进阶层"是:你能不能靠几条命令的组合,在最短时间内把一个脏乱差的数据源清洗成可直接使用的格式。这才是Shell工具链存在的意义——它不是给你写大型软件的,它是给你在终端里快速解决问题的。
我这边面试有个固定环节:给候选人一个生产环境的日志片段,让他找出某个接口报错的 Top 5 IP。这个题目单看每一步都不难,但组合起来足以筛掉一大批人。有的候选人从grep开始写,写完grep又用sed去截字段,再用sort手动排——能跑通,但速度很慢,代码也很啰嗦。而进阶的候选人会直接一条命令:
grep 'ERROR' app.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -5这道题里出现了四个工具:grep过滤、awk取列、sort排序、uniq -c计数、sort -rn倒序、head截取。每一个单拎出来都不难,难的是你脑子里有这条"管道流水线"的意识。
这也是我对"进阶"的定义:不是会用更多命令,而是在面对不确定结构的数据时,能下意识地拆解出"过滤—提取—聚合—排序"这样的处理链。顺着这个思路,下面几组工具是企业面试里出现频率最高的,我一个个拆开讲。
2. jq:JSON处理这块硬骨头,面试官最爱问
前些年Shell面试题里很少出现 JSON,因为日志和接口返回常是文本行。但现在不一样了,几乎每家公司都有JSON格式的日志、接口响应、配置文件。jq这个工具的地位,已经从"小众辅助"变成了"必会项"。
2.1 为什么是jq而不是grep/sed硬着头皮上
JSON是嵌套结构,用正则去匹配JSON字段非常痛苦。grep能做的只是"找出包含某个字段的行",但如果你需要"取某个数组里的第几个元素""按某个字段过滤""把key重新命名",正则和sed会直接写崩溃。jq的本质是给终端配了一个迷你JSON解析引擎,它把JSON看作对象图,让你通过类似路径和过滤器的方式取值。
最简单的用法:
# 取对象字段 echo '{"name": "alice", "age": 30}' | jq '.name' # "alice" # 取嵌套字段 echo '{"data": {"user": {"id": 123}}}' | jq '.data.user.id' # 123 # 数组按下标取元素 echo '[{"id": 1}, {"id": 2}]' | jq '.[1].id' # 2这里很多人会忽略一个细节:jq '.name'输出的是带引号的字符串,如果你要把它直接塞给下一个命令当参数,需要加-r参数输出原始字符串:
# 错误示范:输出还带着双引号 echo '{"name": "alice"}' | jq '.name' | xargs echo # "alice" <- 引号会被当作内容一部分 # 正确示范:-r 去掉引号 echo '{"name": "alice"}' | jq -r '.name' # alice这个-r的坑我在面试题里设过,能注意到的人不多。很多人在脚本里用jq取值后一直得到带引号的结果,然后又用sed 's/"//g'去清洗——绕了一大圈,问题就出在没理解-r。
2.2 过滤、映射、排序:企业场景三件套
JSON日志最常见的需求是:筛选出满足条件的记录,并提取某些字段重新格式化。
假设有个接口日志文件api.json,每行是一个JSON对象:
{"time": "2024-05-01 10:00:00", "userId": 101, "status": 500, "path": "/api/order", "latency_ms": 320} {"time": "2024-05-01 10:00:05", "userId": 102, "status": 200, "path": "/api/user", "latency_ms": 45} {"time": "2024-05-01 10:00:08", "userId": 103, "status": 500, "path": "/api/order", "latency_ms": 510}找出status == 500的记录的userId和latency_ms,按耗时倒序:
jq -rc 'select(.status == 500) | {userId, latency_ms}' api.json | jq -s 'sort_by(-.latency_ms)'拆开看这条命令:
select(.status == 500):类似awk里的 if 条件,过滤出状态码为500的记录。| {userId, latency_ms}:管道到下一级,做字段提取和对象重写。-c:每个JSON对象输出成一行,方便后续观察。- 末尾的
jq -s 'sort_by(-.latency_ms)':-s表示把多行JSON当作一个数组读入,再整体排序。
这里有个实战技巧:如果数据量特别大,比如几十万行日志,不要再组合两次jq进程,直接用一次jq搞定。第一次jq已经把不需要的字段过滤掉了,第二次jq是在内存里做数组排序,数据量可以接受,但如果几十个G的日志,你就要考虑用sort -t对纯文本来排,而不是让jq -s把全部数据加载进内存。jq -s的本质是把整个输入加载成一个大数组,内存吃紧的生产环境一定要谨慎。
2.3 jq里容易被问住的三个冷知识点
第一,jq里数值排序默认是字符串排序,所以按数值排序时要用sort_by(.field)而不是sort_by(.field | tostring)。面试里我常问"为什么我按时间排序排不对",很多人答不上来,其实是对sort_by的类型比较逻辑不熟。
第二,jq可以对key进行修改和新增:
echo '{"name": "alice"}' | jq '.age = 18' # {"name":"alice","age":18}第三,map与.[]的区别。.[]会把数组里的每个元素展开成独立的流,map(f)则是保留数组结构。这个区别直接决定了你后续管道是处理"一行一个对象"还是"一个整体数组"。比如:
echo '[1,2,3]' | jq '.[] | . * 2' # 2 # 4 # 6 # 这里管道后面处理的是流 echo '[1,2,3]' | jq 'map(. * 2)' # [2,4,6]如果这种细节你不熟悉,建议单独花半小时把jq的官方手册过一遍,再刷几个面试题。毕竟它现在几乎和grep一样是Shell的标配。
3. awk与sed的组合拳:一页脚本解决80%文本需求
不管是传统运维还是云原生场景,文本处理都是Shell面试里绕不开的重头戏。awk负责按列和条件处理结构化文本,sed负责按模式和行号做流式编辑,两者经常搭档出现在同一条管道里。企业面试的经典题目"将某日志中第2列等于online的行,把第5列字段中的debug替换为info,再只保留前10行",标准答法必然会用到awk和sed。
3.1 awk:你以为你会了,其实你只会打印列
很多人在简历上写"熟练使用awk",实际只会awk '{print $1}'。面试官只要问三个点就能探到底:
NR和NF分别是什么?NR是当前行号,NF是当前行字段数。要在文件开头加总行数统计:
awk 'END {print NR}' data.txtFS和OFS怎么用?默认按空白字符分列,但如果是CSV文件,字段分隔符是逗号:
awk -F',' '{print $2, $3}' data.csv- 关联数组有什么用?这是awk最强大的地方。谁说awk不能做分组统计?用关联数组就能实现类似
GROUP BY的效果。
有一个经典的面试题:统计日志里每个接口的调用次数和平均耗时。日志格式:
2024-05-01 /api/user 45ms 2024-05-01 /api/order 320ms 2024-05-01 /api/user 60ms用awk就能完成:
awk '{cnt[$2]++; sum[$2]+=$3} END {for (k in cnt) printf "%s %d %.2fms\n", k, cnt[k], sum[k]/cnt[k]}' api.logcnt[$2]就是对"每个$2值"维护一个计数器,sum[$2]累加耗时,最后在END里遍历数组输出。这个写法在面试题里出现过无数次,因为它的原理是关联数组,比普通for循环更贴近shell的思想。
另外注意一个细节:awk的数组遍历for (k in cnt)不保证顺序,如果需要按访问量排序,要再通过管道sort -k2 -rn处理。这也是生产实践里经常要做的一步。
3.2 sed:替换之外,你还得会定位和范围操作
sed的基础是替换:sed 's/old/new/g',我再强调一个重点——sed不带-i时只是输出到屏幕,不改原文件。有些候选人面试时写完sed 's/foo/bar/g' file就说改完了,实际文件一点没动。正确姿势是:
# 替换并保存到新文件 sed 's/foo/bar/g' input.txt > output.txt # 原地修改(GNU版本) sed -i 's/foo/bar/g' input.txt我见过不少线上事故就是有人忘了-i或者在不同系统上-i参数不兼容(macOS 的sed是sed -i ''),所以建议脚本里统一使用sed '...' file > tmpfile && mv tmpfile file这种保险写法。
sed进阶功能里,最值得花时间的是地址范围和正则定位。比如只处理第10到20行:
sed -n '10,20p' file删除空行:
sed '/^$/d' file从包含BEGIN的行开始处理直到文件末尾:
sed -n '/BEGIN/,$p' file这些定位能力在企业脚本里比单纯替换用得更频繁,但面试时很多人只会背s///g。
3.3 组合实战:一个日志清洗场景的完整解
假设这样一个面试题:一个访问日志,字段顺序为IP 时间 请求路径 状态码 响应大小,现在要筛出状态码为500的请求,把路径里的/api/v1/前缀去掉,按IP分组统计次数,取Top 3。
我期望的完整解:
awk '$4==500 {print $1, $3}' access.log \ | sed 's#/api/v1/##' \ | awk '{cnt[$1]++} END {for (k in cnt) print cnt[k], k}' \ | sort -rn | head -3这条命令里awk做了两次,第一次取数,第二次分组;sed做字符串清洗;sort和head做排序截取。这就是生产里很常见的"组合拳"。我拿这道题面过很多人,能完整写好的人不到三成,主要是因为不少人对sed的替换分隔符不熟——sed 's#模式#替换#'里#作为分隔符可以避免路径里的/转义问题,这个技巧很实用。
4. xargs、sort、uniq、tr:那些被低估的管道利器
这一组工具看起来每个人都会敲两下,但真正能在面试里用出水平的少之又少。它们单个都不难,难在组合场景里的边界问题。这里我只讲企业面试最高频的几个细节。
4.1 xargs的经典陷阱:文件名带空格、参数过长
xargs的作用是把标准输入转成命令参数。最常见的反面教材是:
# 危险:find 出来的文件名若带空格会被拆开 find . -name "*.log" | xargs rm正确做法是用-0配合-print0:
find . -name "*.log" -print0 | xargs -0 rm面试时我会特别问:为什么find ... | xargs不行?很多人答不出"空格会破坏参数边界"这个点。这是线上真实踩过的坑,文件名里一旦有空格,后面涉及的文件基本全会被错误处理。
还有一个进阶点是并发控制。批量压缩几百个文件,一个个操作太慢,xargs用-P参数控制并行度:
ls *.log | xargs -P 4 -I {} gzip {}-I {}指定占位符{},-P 4表示最多4个进程并行。有次我在生产环境批量转码图片,用-P 8把原来一个多小时的任务压到十几分钟,这体验直接拉升了一个档次。
4.2 sort:除了排序,你还要知道稳定排序和字段规则
sort处理日志时最容易被忽略的是字段分隔符。默认按空格分列,但遇到:、,分隔的数据就得用-t:
sort -t: -k2 -n /etc/passwd-k2指定按第二列排序,-n按数值排。很多人在-k2后面忘记加-n,导致100排在20前面。这个错误在企业数据统计里特别致命。
还有一个冷门但面试常考的点:sort是稳定排序吗?GNU 的sort默认不稳定,如果你想先按第二列排,再按第一列保持相对顺序,需要-s参数。比如:
sort -s -k2,2 -k1,1 data.txt先按第二列排序,第二列相同的再按第一列维持原始顺序。这种多级排序场景在报表场景里很常用。
4.3 uniq与tr:处理连续重复和字符集清洗
uniq的默认行为是只去重相邻的行,所以用uniq前必须先sort。uniq -c可以在行前加上出现次数,这个配合sort -rn就是排行榜的标配。uniq -d只显示重复行,uniq -u只显示唯一行,这两个参数面试时也常被问到。
tr通常是三兄弟里最不起眼的,但它做字符集替换非常高效。比如去掉文件里的Windows换行符:
tr -d '\r' < input.txt > output.txt把大写转小写:
tr '[:upper:]' '[:lower:]' < input.txt压缩连续空格:
tr -s ' '什么场景用tr而不是sed?如果只是简单字符映射、删除、压缩重复字符,tr更快更直观;如果涉及正则或模式替换,sed才是合适的。面试时如果能在合适场景主动说出用tr,会显得你对工具边界把握得很清楚。
5. 真实企业面试题:从一道初级到一道进阶的完整拆解
这一节我把之前面试里出现过的几道典型的Shell题目整理出来,附上分析和参考答案。不是背题,而是让大家看解题思路。
5.1 初级真题:找出访问量最大的前5个IP
题目描述很简短:"给定access.log,每行第一列是IP,统计访问次数最多的前5个IP。"
最直白的解法:
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5但这道题往往还有几个附加条件:
- 如果日志里混入了空行怎么办?加
grep -v '^$'或者awk 'NF'。 - 如果有些行是
-表示空IP怎么办?awk '$1!="-" {print $1}'。 - 如果只统计某个时间段的IP怎么办?先
grep '2024-05-01' access.log | awk ...。
这道初级题考察的其实是"流程化处理"的思维,而不是具体命令。一个成熟的候选人写完第一版之后,会立刻思考各种边界输入,这是面试加分项。
5.2 进阶真题:按小时统计某接口的错误率
题目:"日志格式为时间 接口 状态码 耗时,统计2024-05-01当天/api/order接口每个小时的成功率。"
示例数据:
2024-05-01 10:00:00 /api/order 200 45ms 2024-05-01 10:00:01 /api/order 500 320ms 2024-05-01 10:05:30 /api/order 200 60ms 2024-05-01 11:00:00 /api/order 500 500ms 2024-05-01 11:02:10 /api/order 200 80ms不引入其他命令,纯awk可以做:
awk '$3=="/api/order" { hour=substr($2,1,2) total[hour]++ if ($4==200) success[hour]++ } END { for (h in total) printf "%s 成功率%.2f%%\n", h, success[h]/total[h]*100 }' api.log这道题考察了:
substr的字符串截取能力;- awk的关联数组分别维护总数和成功数;
END里计算并格式化输出。
如果候选人能再想到把结果按小时排序(sort),说明他对结果的可读性也有考量。这道题能完整写出来的人,基本可以认为awk达到了进阶水平。
5.3 综合真题:JSON日志里提取慢请求并重放
这道题结合了jq和xargs,是我最近比较喜欢出的一道综合题:
"现在有一个包含耗时和请求体的JSON日志文件,需要找出所有latency_ms大于1000的记录,并用curl重放这些请求。"
jq -c 'select(.latency_ms > 1000) | {url, method, body}' slow_requests.json \ | while read -r line; do url=$(echo "$line" | jq -r '.url') method=$(echo "$line" | jq -r '.method') body=$(echo "$line" | jq -r '.body') curl -X "$method" -d "$body" "$url" done这道题的经典错误是直接jq ... | xargs curl,把整行JSON当成一个参数传给curl,curl完全无法解析。正确做法是需要先在管道里用while read逐行读取,再对小对象分别用jq -r提取字段。这里既考察JSON提取,又考察shell循环和变量传递,属于综合能力题。
这种场景在生产里也常见,比如压测平台从日志里捞请求做回放。能当场想通这个逻辑的人,就算进了进阶的门。
6. 我的建议:进阶Shell的正确路径与练习方法
工具讲了一圈,最后聊点实在的。很多人的Shell卡在"会用但不会组合"的瓶颈,原因是平时都在单命令地敲,没有刻意练习管道思维。我建议按这个顺序打磨:
第一阶段:每个工具单独刷透。把jq、awk、sed、xargs、sort、uniq、tr各自的能力边界和主要参数吃透,至少能不看手册说出每个工具的关键参数是干什么的。
第二阶段:做场景组合练习。给自己布置一个小任务:把一个日志文件从原始格式清洗成结构化表格,再做聚合统计,再筛选出异常记录,最后批量操作这些记录。整个链路必须尽量用管道和工具完成,而不是写shell脚本里的循环加一堆临时文件。
第三阶段:刻意做边界处理。比如文件名有空格、字段有逗号、字段值有空字符串、数据中间有可变的列数。这些边界才是生产环境真正会遇到的,也是面试里区分"会背命令"和"真会干活"的关键。
第四阶段:多看别人的脚本。去GitHub上找一些真实的shell脚本,看别人怎么组织函数、怎么处理管道、怎么处理错误退出。不要只满足于能跑通,要去理解别人的设计取舍。
我个人在带新人时的体会是:Shell能力不是靠背命令提升的,是靠踩坑和不厌其烦地重写命令提升的。同样一个日志统计需求,逼自己先写第一版,然后问一句"如果数据变成10倍,我的管道还跑得动吗?如果字段格式变了,我要改几处?"——带着这两个问题去重写,你的Shell水平会飞快拉开同龄人。
最后再把面试题环节的压箱底经验送给读者:面试官出一道Shell题其实不是在考你"记不记得这个参数",而是在看你在一个不熟悉的数据形态面前,能不能快速把问题拆成"过滤/提取/聚合/格式化"这几步,再用合适的工具串起来。你平时多练习用一句话管道描述业务流程,少写临时脚本和循环,这一关就自然过了。