
三剑客这三个词在shell文本处理圈子里被念叨了二十多年却一点不过时。我最早是被日志分析逼上路的服务一挂几十万字的老日志翻到手软grep半天捞不到关键行更别说把错误码按次数排个名次。后来才琢磨明白grep只管“找行”sed管“改行”awk管“拆列统计”三把刀各有各的脾气。这篇就把我从实际项目中攒下来的用法、避坑经历和能直接抄走的组合命令一次讲清楚。适合刚接触Linux命令行的人也适合那些天天写脚本但始终没搞懂正则分组、awk数组怎么用的老手。1. 先搞清三把刀的职责边界再动手写命令1.1 它们不是一个时代的产物却各管一片grep、sed、awk被并称为文本处理三剑客不是因为长相相似而是因为在“逐行处理文本”这个场景里它们的职责刚好互补谁也不能完全替代谁。grep是最早出现的它的核心能力只有一个按规则匹配行然后输出这些行。它不会改内容也不关心你的文本分几列只关心“这一行到底有没有命中规则”。所以当你只想确认日志里有没有某个关键字、找出哪些配置文件引用了某个路径时grep是最快的一把刀。sed是流编辑器全称Stream Editor。它的核心能力是按行修改把A替换成B删除第几行到第几行或者把某个范围内的行打印出来。它不像vi那样有交互界面所有修改通过命令一次性告诉它它像流水线一样把文件从头读到尾逐行处理完直接输出。批量改端口、批量去掉配置里的注释都是它的主场。awk其实不是典型的编辑工具它更像一门小型数据语言。它擅长把每一行按分隔符拆成多个字段然后对字段做判断、运算、统计、格式化输出。你想算出日志里某个接口的平均响应时间grep和sed都很别扭awk几行命令就出来了。一句话总结找行用grep改行用sed拆列算数用awk。三者的共同点是都支持正则表达式而且都遵循“逐行处理”的哲学这决定了它们能天然地在管道里协同工作。1.2 什么时候用哪把刀一张最小决策表我见过不少新手一上来就背几百个选项结果真到用的时候全忘了。我自己更习惯先想场景再决定工具选项是靠场景带出来的。场景首选工具典型命令雏形只想知道哪些行命中关键字grepgrep error app.log想统计某个词出现了多少次grep uniqgrep -o code x.log想把配置里的8080改成9090sedsed s/8080/9090/g app.properties想删掉文件里所有注释行和空行sedsed /^#/d;/^$/d conf.ini想按分割列统计聚合数据awkawk {a[$1]} END{for(k in a) print k,a[k]} x.log想找出响应时间超过1秒的请求并求和awkawk $NF1000{s$NF;n} END{print s,n} x.log这张表不需要背它反映的是一个判断链路输入是“整行”输出也是“整行”那就grep输入是“整行”输出却要“改动后的行”那就sed输入需要“拆开看字段”输出是“字段加工后的结果”那就awk。我之前在项目中帮同事优化过一个慢脚本原来他用grep从文件里抠出数字再用cut拆列再用循环求和整个脚本30多行跑一次还要好几秒。换成awk之后一行命令就出结果。这不是炫技而是工具选对了复杂度自然就降下来了。2. grep最快的行匹配器很多人的用法其实很粗糙2.1 常用选项-E、-F、-o、-v、-c 的组合规律grep的命令格式是grep [选项] 模式 [文件]不带文件名时它读取标准输入。日常最值得花时间练熟的选项就六个-E启用扩展正则表达式。默认的BRE格式里、?、|、{、}这些符号都要转义才能用写起来特别容易踩坑。加了-E之后直接写、|、{m,n}就行跟直觉相符。-F把模式当作固定字符串不做正则解释。搜IP、搜URL、搜带点号的配置项时用这个选项能避免转义而且速度快一个量级。-o只输出命中部分不输出整行。配合sort、uniq可以做精准统计。-v反向匹配输出没命中的行。-c统计命中行数不输出行内容。-n输出行号排查问题定位的时候几乎必用。一个常见误区是把grep -c当成“出现次数”。它统计的是命中的行数。同一行里出现了10次目标词grep -c也只会算1次。如果想知道真正出现多少次得靠grep -o pattern file | wc -l。2.2 正则里的“行”思维grep正则的匹配单位是一整行。它的匹配逻辑是从当前行的任意位置开始找子串只要有一个位置能匹配上这一行就算命中。所以grep abc并不是判断这一行等于abc而是包含abc。很多初学者写grep 192.168.1.1结果把192.168.111.1也搜出来了原因就在点号。正则里点号代表任意一个字符必须写成192\.168\.1\.1才是字面量。更保险的做法是加-F直接把整串当固定文本来搜。关于常用符号我按照从低到高的优先级列几个^匹配行首$匹配行尾。注意$在不同上下文里含义不同在vim替换里它也有特殊含义但在grep里就代表行尾。[0-9]、[a-zA-Z]表示字符集合取反写[^0-9]。\b表示单词边界。搜\btest\b不会匹配contest或testable。(和)是分组配合|可以做二选一比如grep -E error|failed。我从实际日志过滤中练出来的一个经验先想清楚目标特征的最短唯一模式不要一上来就写一个超长正则。比如要过滤内网IP段写成^10\.和^192\.168\.各来一条结合-E和|比在一个正则里堆三层嵌套分组好维护得多。2.3 用 -o 从日志中提取并统计错误码我经常需要统计一份大日志里各种HTTP错误码的数量做法是先-o抠出错误码片段再排序去重计数。比如日志里有形如error_code:10023的行可以这样grep -oE error_code:[0-9] app.log \ | sort \ | uniq -c \ | sort -rn这一步的中间产物是若干条error_code:10023sort排序后相同错误码会相邻uniq -c再压缩并计数最后的sort -rn按次数降序排列。整个过程没有awk参与但已经解决了“错误码排行”这个需求。如果要再进一步只关心数量最多的前三名就在末尾加| head -n 3。头部的head不会影响前面的数据流因为管道是逐行流动的head取够3行后会终止读取整个命令链也会随之结束。2.4 多文件搜索和空文件名风险当要搜索整个项目目录时grep -r很常用但有两个细节值得注意。第一-r默认不会进入符号链接目录如果想跟随得用-R。第二搜索结果出现在终端时加了-H会显示文件名。把grep当成“行筛选器”丢进管道时注意一个原则能用grep过滤的内容就不要先cat整个文件再传给grep。grep pattern file和cat file | grep pattern结果一样但前者少开一个进程对超大文件还有IO上的优势。这个习惯对后面三剑客协作很重要。在多个文件中找出包含特定词的文件清单经常配合xargs做批量处理grep -rlZ timeout config/ \ | xargs -0 sed -i.bak s/timeout30/timeout60/g这里-Z表示输出以\0结尾配合xargs -0能正确处理文件名里的空格。如果不加这个万一某个配置文件路径里带空格命令就会把文件名拆成两半轻则操作目标错误重则删错东西。3. sed流式编辑器的核心三板斧替换、删除、取行3.1 地址定址sed命令的门槛和钥匙sed的语法可以拆成sed 地址 动作。地址决定“对哪些行操作”动作决定“对命中的行干什么”。没有写地址时默认对所有行操作。地址有三种写法行号5代表第5行1,5代表第1到5行1~2代表第1行开始每隔2行也就是奇数行。$最后一行。1,$代表全文。正则/pattern/代表所有匹配该正则的行。动作常见的有p打印、d删除、s替换、a追加、i插入、c整行替换。这里最大的坑在于sed默认会输出所有输入行而p会把命中的行再打印一遍结果就是行内容出现两次。所以单独用p时几乎必须加-n参数表示“关闭默认输出只输出我指定要的内容”。举一个项目里很常见的场景查看某个时间段附近上下文grep有-A和-B选项能做但手工指定范围时sed的地址组合更灵活sed -n /2025-03-01T10:23:45/,30p service.log这条命令会从匹配到2025-03-01T10:23:45的下一行开始连续打印30行。如果我想从该行往前看10行sed做不到实时回溯但可以换成grep -B 10 -A 30 pattern。工具之间本来就是补位关系不必强行用一把刀解决所有问题。3.2 替换命令的细节定界符、全局标志、反向引用替换是sed用得最频繁的动作写法是sed s/原模式/替换内容/。默认情况下s只替换每一行中第一个命中位置要处理一行的所有命中得在末尾加gsed s/8080/9090/g app.properties当替换内容里有反斜杠或路径符号时建议换定界符)之类不常用最稳妥的是把/换成#或|sed s#/opt/app#/data/app#g config.path反向引用是另一个高频知识点。模式里的括号分组在替换时可以用\1、\2引用。注意使用-E时分组符号直接写()引用仍然写\1。我一开始总以为扩展正则模式下引用也要改成别的写法实际上完全不用。假设有一批日期格式是2025-03-01想统一成2025/03/01sed -E s#([0-9]{4})-([0-9]{2})-([0-9]{2})#\1/\2/\3#g dates.txt这种写法把脏活交给正则sed只做搬运。批量重命名文件名时也经常用这个思路先用ls或find把文件名列出来再通过sed生成新的文件名再用mv执行。3.3 删除空行和注释行配置文件清洗最常见sed删除动作的大概逻辑同替换一样地址选在哪d就删除哪行。清理配置文件时我常用这一串sed /^#/d; /^$/d nginx.conf分号表示在同一行里连续执行多个sed子命令等价于sed -e /^#/d -e /^$/d nginx.conf。这条命令会去掉所有以#开头的行和所有空行。要注意前后有空格或制表符的空行并不算严格意义的^$更严谨的写法是/^[[:space:]]*$/d。我想强调一点sed只是在输出流里把这些行删掉了并没有修改原文件。很多人误以为sed操作完文件就变了实际上如果想落盘要么重定向到新文件要么用-i。给新手最常见的建议永远是先用不带-i的命令看输出确认结果没问题再加-i。3.4 原地修改 -i 的跨平台陷阱-i选项能直接改文件。但GNU sed和BSD sed在参数上有差异这是跨平台脚本最容易翻车的地方。Linux系统自带的是GNU sed支持sed -i s/a/b/g file。macOS自带的BSD sed强制要求-i后面跟一个备份后缀必须写成sed -i.bak s/a/b/g file否则会报错。想让两边都能跑通用写法是sed -i.bak这样Linux上会生成file.bak备份BSD上也正常。如果实在不想留备份文件GNU下用-i即可BSD下可以写-i 但引号里的空格细节很容易出错。我之前给某个跨平台部署脚本踩过坑在macOS开发机上执行好好的sed -i命令一上Linux生产环境反而报错查了半天发现是两种sed对-i的参数处理方式完全不同。后来统一改成sed -i.bak两边不再打架。-i之前一定要先做无参预览这是血的教训。我曾经在生产环境上一个配置批量修改命令少了转义一整批配置文件里的内容全乱了最后从备份里费了好大劲才恢复。现在我的习惯是三步走先grep -n看目标内容再sed不带-i做输出预览最后确认无误才加-i。3.5 不只是替换sed 的拼接顺序和二进制安全提醒多数人对sed的印象停留在替换其实它还常用来做“提前拼接”和“范围提取”。比如给每一行自动加前缀后缀sed s/.*/item\/item/ source.list这里的代表整个命中的字符串替换后原行会被包进一对item标签。写脚本生成XML或HTML片段时非常好用。注意在替换字符串里是特殊符号如果替换内容里真的想写得写成\。还有一个我遇到过很多次的坑sed处理二进制文件时会输出类似“binary file matches”或乱码。用grep搜二进制文件时也类似。解决方案是加-a选项把二进制文件当文本处理或者干脆用strings命令把可打印字符提取出来再处理后者在分析二进制包时更靠谱。4. awk按列思考和统计的利器4.1 程序结构行循环、字段拆分、BEGIN和ENDawk对很多初学者来说最难因为它不是单一的查找替换而是一个“模式-动作”结构。它的执行模型可以这样理解自动逐行读取输入。每一行awk先按分隔符把行切成若干字段$0代表整行$1代表第一个字段依次类推。如果写了“条件”部分也叫模式就只在条件为真时执行“动作”部分。BEGIN块在所有行处理前执行通常用来设置变量、定义分隔符、打印表头。END块在所有行处理完后执行通常用来输出统计结果。默认的字段分隔符是任意连续空白包括空格和制表符。想自定义分隔符用-F或者BEGIN里的FS变量。处理CSV时-F,是最常见写法。我经常用-F[,\t]来处理既带逗号又带制表符的导出文件正则作为字段分隔符的灵活度是cut完全比不上的。举一个秒懂的入门例子。假设score.txt内容如下三列分别是姓名、科目、分数张三 语文 89 李四 数学 93 张三 数学 88统计张三的总分awk {if($1张三){s$3;n}} END{printf %s total%d, avg%.1f\n, 张三, s, s/n} score.txt这里s和n是自定义变量。awk很多用法就是把Excel里干的那套统计逻辑搬到命令行里字段即列循环则是隐式的。4.2 统计求和、平均值、最大值一行搞定awk统计能力在生产里最常用的场景之一就是分析接口响应时间。假如latency.log只有一列数字awk {sum$1; if($1max) max$1; n} END{printf sum%.2f max%.2f avg%.2f\n, sum, max, sum/n} latency.log如果日志是完整的access log响应时间在最后一列那就用$NF这是awk内置变量代表最后一个字段。这个技巧可以避免去数字段却死活数错的尴尬。我曾经分析过一个批量任务日志任务耗时都在每行最末尾格式还不太统一有毫秒有秒。如果用sed光是单位换算就够呛awk却能做到按字段先拆分再统一单位。在END块里做汇总输出整个过程不会引入临时文件。4.3 按列过滤grep管不到的地方grep只能按整行正则过滤想“某列大于多少才输出”awk才是正解。最典型的场景是看磁盘占用df -h | awk $1data $580 {print $1, $5}再比如从一份tsv文件里抽出第3列长度大于10的行awk -F\t length($3)10 {print $0} data.tsvawk内置函数length直接提供列内字符计数不用在正则里痛苦地写{11,}。我说过一句话“awk天生适合干在多个字段之间做逻辑判断的活”这句话的底气就在于它的条件部分可以是多个字段的组合。比如awk $1 ~ /^10\./ $2open {count} END{print count} netstat.log这里的~是正则匹配运算符表示“左边字段是否匹配右边正则”。反向不匹配用!~。这个语义比grep整行匹配更精细也更贴近业务含义。4.4 数组、去重和统计排行awk里的数组本质上是一个关联数组下标可以是字符串这让“按某个维度分组统计”变得极其简单。比如统计一个访问日志里各来源IP的请求次数awk {cnt[$1]} END{for(ip in cnt) print ip, cnt[ip]} access.log | sort -k2 -nr我刚开始接触awk数组时最纠结的是它没有END块里的数组顺序概念for(ip in cnt)的遍历顺序是哈希顺序不是写入顺序也不是排序顺序。所以如果要top N再丢给sort处理最省事。去重也是awk的强项。保留第一次出现的行逻辑核心是靠数组记录“是否见过这个字段”awk !seen[$0] duplicat.txt这行代码的精妙处在于seen[$0]这个表达式是通过“先取值再自增”的顺序判断的。第一次遇到某行时seen[$0]是0!0为真所以打印第二次遇到时seen[$0]是1!1为假直接跳过。这个技巧简洁但不好读我建议写进脚本里时注释清楚免得两星期后自己都看不懂。4.5 printf与字段格式化awk里常用的printf格式符和C语言的几乎一样%s字符串、%d整数、%.2f保留两位小数、%-10s左对齐宽度为10的字符串。它在生成报告的时候堪称神器。比如把统计结果以表格形式输出awk {cnt[$1]} END{printf %-15s %8s\n, IP, Count; for(ip in cnt) printf %-15s %8d\n, ip, cnt[ip]} access.log这比直接print好读得多也更方便粘到邮件或文档里。awk还有substr、split、index等字符串函数能处理不少轻量级文本格式转换但如果转换逻辑太复杂我建议直接用Python一把梭awk的功能边界也就到这里了。5. 组合拳管道协作就是一段迷你ETL5.1 管道本质上一命令的stdout是下一命令的stdin三剑客真正威力是它们在管道里协作。管道不产生临时文件数据像水流一样从左到右流动。这带来一个好处和一个陷阱。好处是处理大文件时不会占用额外磁盘也不会在目录里留下一堆中间产物。陷阱是管道中某一步出错时后面拿到的就是残缺数据排错时容易头晕。我的经验是排错时把管道拆开执行逐步加段。比如grep ... | sort | uniq | awk ...如果结果不对先跑grep段看输出再加sort看中间结果最后再加awk。命令行里还有一个隐形选择无脑使用cat file | ...其实是冗余的。文件路径可以直接放在grep/sed/awk后面。写脚本时尽量避免无意义的管道因为每多一个进程大文件处理时间就多一分。5.2 场景一按时间段统计接口状态码假设有一个access log每一行格式是IP 时间 状态码 耗时要统计某天15点到16点之间各状态码的数量grep 15: access.log \ | awk {count[$3]} END{for(code in count) print code, count[code]} \ | sort -k2 -nr如果时间格式是25/Mar/2025:15:23:45那么第一个字段就不是我们要的时间字段需要调整字段位置比如用$4。写命令前先head -n 2看一眼日志格式永远是最稳的做法。这种两步走模式在日志分析里极其常用grep负责行为级粗筛awk负责字段级聚合。两步之间靠管道交接逻辑清晰性能也够用。5.3 场景二批量修改配置并保留备份我在部署某些应用时经常需要把一批配置文件里的监听地址从旧IP换成新IP。习惯做法是三步先grep -rl 192.168.1.10 conf/找到所有涉及到的文件。再对这些文件逐一执行替换预览确认替换结果。最后原地替换并保留备份。一条管道的写法是grep -rlZ 192.168.1.10 conf/ | xargs -0 sed -i.bak s/192.168.1.10/192.168.1.20/g有人会问这时候为什么不用for循环其实也可以for f in $(grep -rl 192.168.1.10 conf/); do sed -i.bak s/192.168.1.10/192.168.1.20/g $f; done但$(...)遇到空格路径会出问题而grep -Z | xargs -0更健壮。命令行里能用管道表达的操作我优先用管道循环越多出错可能性越大。5.4 场景三从报表里提取关键词生成CSVsed和awk还能携手完成一次迷你ETL。假设有一个企业应用的日志文件里面的行有不同级别格式是INFO 模块名 耗时现在要提取所有ERROR级且耗时超过1000的行并且按模块名,耗时输出成CSVsed -n /^ERROR/p app.log \ | awk $NF 1000 {print $2 , $NF} \ | sort -t, -k2 -nr这里的-t,指定sort的分隔符后面-k2 -nr表示按第二列数字倒序。CSV如果字段里有逗号这种简单做法会出问题但处理内部日志足够用了。更严谨的CSV转义会复杂很多不是三剑客的主要战场。5.5 组合中的常见坑grep -v 之后还要再处理空行有一次我从日志里反向过滤掉INFO行用了grep -v INFO结果输出的文件里还有大量空行后续awk统计时因为字段数量不对计算结果全都错了。后来我在管道的下一步加了sed /^$/d问题立刻解决。这类“数据依旧不干净”的坑非常多。每次组合工具时都应该在心里过一遍当前管道输出的数据格式能安全地被下一个工具解析吗空行、行尾空格、字段分隔符突变是最常见的三类污染源。grep -v INFO app.log | sed /^[[:space:]]*$/d | awk {...}6. 避坑实录与性能调优三剑客踩过的常见大坑6.1 三套正则标准BRE、ERE、PCRE别混着用grep默认使用基本正则BRE在这个模式里、?、|、()这些字符都被当作普通字符想表达正则含义必须加反斜杠写出来是\、\?、\|、\(...\)。很多新手的第一个坑就出现在这里明明在别的工具里能用的正则粘到grep里全部失效。我强烈的建议是日常使用统一加-E。grep -E是扩展正则ERE、?、|直接生效不需要转义。而grep -P使用PCRE兼容更多语言风格但性能通常略逊于-E而且在某些最小化系统里-P可能没编译进去。生产环境脚本尽量别依赖-P。sed同样分BRE和ERE默认BRE-E切换ERE。awk则默认就是类似ERE的语法所以、?、|直接可用。写脚本前先确定工具再选择正则写法可以省掉一大半调试时间。6.2 贪婪匹配正则看起来没毛病结果却不对正则从左到右匹配默认是贪婪的它会在保证整体匹配成功的前提下尽可能匹配更长的内容。经典案例是提取引号里的内容如果你写.*去匹配a and b它会一次匹配到a and b中间引号也被吞掉了。原因就是.*太贪婪它一直滑到最后一个引号才罢休。解决办法不是去学正则里的“懒匹配”选项而是用更精确的字符集合grep -oE [^]* data.txt[^]*表示“任意多个不是双引号的字符”它从第一个引号开始到第一个不是引号后又遇到引号的地方就停下来。这个模式比.*?更直观也更稳定。我对所有文本提取类需求的建议都是同一句话在能明确排除分隔符时永远优先用排除型字符组而不是依赖贪婪/懒匹配。6.3 不同系统、不同版本之间的行为差异Mac依然默认保留BSD sed和一套老版本bashLinux则是GNU系二者在-i、正则支持、行尾处理上都有差异。awk也同样有的系统自带的是mawk有的则是gawk它们在浮点精度、strtonum函数、中文字符支持上略有差别。如果脚本要跨平台跑我的建议是sed统一使用-i.bak避免-i参数差异。awk脚本里避免使用gawk特有的扩展函数如strtonum、gensub尽量只用POSIX标准函数。正则尽量别用\d、\w这类Perl风格符号改用[0-9]、[[:alnum:]]。涉及日期、locale时考虑在脚本开头设置LC_ALLC避免中文本地化环境干扰字符匹配。我曾在一个自动化巡检脚本里用awk /[[:alpha:]]/{print}过滤只含字母的行在Linux上正常在某个老Solaris机器上却把中文混进来不少。后来统一LC_ALLC行为才稳定。6.4 大文件性能管道进程、固定字符串与循环三剑客处理几十GB的日志时容易产生性能问题。性能瓶颈通常不是工具本身而是错误用法。第一能用grep -F固定字符串就不要写成正则。-F会跳过正则编译和匹配速度提升两到三倍。比如你只需要找GET /api/order/list这一串用grep -F GET /api/order/list。第二awk里尽量不要一行匹配写一个单独的正则把多次匹配合并到一次条件判断里。比如awk /error|failed|timeout/ {count[$1]}只运行一次正则比写三个if要快。第三避免在awk中频繁调用system()或管道。比如awk {while((grep $1 file)|getline) ...}这种写法每行都会启动一个子进程处理一万行就会启动一万次命令性能极其糟糕。本质上说明需求设计得不对应该先把数据整理好再一次性处理。第四能用管道、单遍扫描完成的就不要引入for循环加临时文件。三剑客的优势在于全程流式一旦落盘IO开销马上变大。6.5 中文和字符集\w、\b的坑在UTF-8环境下\w在很多实现里会匹配汉字因为汉字属于Unicode的“单词字符”。这会导致你认为只在过滤英文字母结果把中文也带了出来。同理\b词边界对CJK文本的判定也不可靠。解决方案是尽量不要依赖这种Perl风格字符类改用POSIX字符类[[:alpha:]]匹配字母[[:digit:]]匹配数字它们受locale影响但加LC_ALLC之后行为更可预期。在纯ASCII日志场景下我一般会在脚本开头加LC_ALLC彻底避免本地化干扰。6.6 文件路径含空格xargs 的经典翻车现场这其实是三剑客管道协作时最容易踩的坑。find、grep输出文件名时如果文件名含空格xargs默认按空白分割输入会把一个文件名拆成多个参数。我在某次批量改配置文件时就因为一个带空格的文件名导致sed把命令参数拼错直接报错退出排错花了快两个小时。正解是在源头就使用空字符分隔。grep -rlZ、find -print0、xargs -0是一套完整方案-Z和-0就是让数据流以\0而不是空白分割。只要路径可能带空格一律用这套组合。还有一点如果用管道把grep输出喂给while循环建议写成while IFS read -r line; do ...; done (grep ...)或grep ... | while ...。前者能保持循环变量不回传但细节多篇幅原因这里就不展开了。最后再分享一个值得长期用的小技巧这三把刀用久了最值得养成的习惯不是背命令而是“先看数据结构再挑工具”。任何文本到了你手上先问三个问题我要匹配的是“整行”还是“列”输出要不要修改原行要不要做分组统计答案基本就决定了grep、sed、awk各自登场还是组合上阵。我自己经常把一段三剑客命令存成shell脚本再用一个参数控制输入文件和输出列。比如一个统计脚本日后可以复用只要改模式串和awk字段位置就行。这样的脚本比直接用Python重写要轻量启动快在服务器上没装Python环境时也能跑。如果你刚开始练习我建议每天拿一份真实日志做十分钟训练先用grep -n找到目标段再用sed批量修改最后用awk做一次统计。坚持两个月你的命令行效率和排错直觉都会明显变化。三剑客从来不是最潮的技术却是运维和开发手里最可靠的一把老锯磨一磨比很多新工具都好使。