
最近带一位刚接触命令行的朋友排查日志他盯着一个几百MB的文件发愁“用编辑器打开都要卡半天怎么才能快速找到那一行报错”我说你需要的不是打开它而是“找”“切”“汇总”三步对应的正是Linux里最经典的三板斧grep、sed、awk。再配合管道符把数据在命令之间流转本来要人工作几小时的事情一两行命令就能搞定。这篇内容就是给所有刚入门Linux、想系统掌握文本处理思路的人准备的不需要提前背多少参数跟着场景走一遍你就能理解为什么说“Linux下一切皆文件”以及如何用这三个命令对文本做真正的加工。很多人以为这三个命令是三个孤立的小工具其实它们更像一条流水线grep负责“从一堆里找出我想要的”sed负责“按行把内容收拾干净”awk负责“把每行拆成字段算账”。一旦你理解了它们各自的定位再看到复杂命令时就不会觉得是天书。我尽量从设计思路讲起把参数放在真实场景里解释最后再给出一份常见问题速查方便你以后踩坑时回来翻。1. 三板斧是什么为什么绕不开它们1.1 从“Linux一切皆文件”说起Linux系统里配置文件是文本运行日志是文本命令的输出也是文本。哪怕一个程序内部是二进制数据对外提供的往往也是文本格式的接口。文本虽然没有数据库那么结构化但它几乎无处不在而且处理起来足够灵活。文本处理三板斧——grep、sed、awk就是围绕这种“行式文本”设计的三个基础工具。grep的定位是“查找”它像扫描仪一样逐行读入内容把匹配某个模式的行挑出来sed的定位是“过滤与编辑”它能在不打开文件的情况下直接删除、替换、插入指定行awk的定位更偏“数据计算”它会自动把每一行按分隔符拆成多个字段然后允许你对字段做判断、统计、格式化输出。三者之间不是竞争关系而是互相配合的关系。1.2 三者的边界与协作我见过不少新手一开始就纠结“我到底该学grep还是awk”。其实它们分工很清晰用一句话概括grep负责按模式找行sed负责对行做增删改awk负责按字段做计算。如果把一行文本看成“一条记录”grep是筛选记录sed是编辑记录awk是对记录的各个属性做运算。你可以很快把它们的核心能力放进一张表里先记住边界后面用起来就不会含糊命令擅长的操作典型场景最常用用法grep按正则匹配并输出某行从日志中找报错grep ERROR app.logsed按行地址或模式进行替换/删除/插入批量改配置文件、清理文本sed s/old/new/gawk按分隔符拆字段做统计/条件判断求平均值、统计IP次数、格式化打印awk {print $1, $NF}从处理粒度上也可以这样理解grep只关心行sed也只关心行但可以修改行awk关心的是行里的“列”。所以当你发现一个任务需要“先找出特定的行再把行里的某些字段拿出来算一算”时基本就是grep与awk的组合。而sed常常用来做中间一步的清洗比如去掉行首的时间戳、把某个分隔符统一替换掉让awk的后续拆分更稳定。1.3 理解标准流数据流转的地基三板斧能串联起来真正依靠的是Linux的“标准流”机制。每个命令启动后都会从“标准输入(stdin)”读取数据把结果写到“标准输出(stdout)”把错误信息写到“标准错误(stderr)”。文件也可以被当作输入但更高级的用法是让一个命令的标准输出直接变成下一个命令的标准输入这就是管道符|做的事情。管道和重定向是数据流转的两种基本方式。管道侧重“命令与命令之间的传递”重定向侧重“把数据送到文件或从文件取回来”。比如grep ERROR app.log | wc -l就是让grep输出的行不断增加地流到wc里统计行数而grep ERROR app.log error.log则是把结果保存到文件里。理解了“数据像水一样流过命令”之后再长的命令链也能一步步拆开来看。2. 查找grep从大海捞针到精准定位2.1 grep 的基本参数先把最常用的记下来grep的全称是“global regular expression print”它默认只做一件事按正则表达式匹配行并把匹配的行打印到标准输出。基础语法是grep [选项] 模式 文件。如果文件省略grep会从标准输入读取这正是它常用在管道开头的原因。我最常用的几个参数按实用程度排序-E启用扩展正则省去大量反斜杠转义。比如grep -E error|fail app.log可以匹配两种内容。-i忽略大小写。排查日志时很有用因为有的程序会写成ERROR有的会写成error。-v反向匹配即“排除掉包含模式的行”。比如grep -v ^# config.conf可以忽略注释行。-n显示匹配行在文件中的行号方便回头定位。-c只统计匹配行数量而不是打印内容。-l只显示“包含匹配内容的文件名”常用于在多个文件里找哪个文件出问题。-r或-R递归搜索目录下的所有文件。-w匹配整个单词避免把“error”匹配到“error_handler”这种词里。-o只输出匹配到的部分而不是整行在提取特定内容时非常顺手。这些参数可以组合使用。打个比方grep -rniE timeout|retry /var/log/就是在/var/log/目录下递归查找忽略大小写只要匹配timeout或retry并且显示行号。这种写法在故障排查时很常用。2.2 正则表达式入门不需要背但要会查grep的威力一半来自正则表达式。正则就是一套描述字符串模式的语言核心元字符并不多。基础正则里^匹配行首$匹配行尾.匹配任意一个字符*表示前一个字符重复0次或多次[]表示字符集合[^]表示不在集合中。比如^2024-能匹配以“2024-”开头的行error$能匹配以error结尾的行。启用-E后常见的扩展写法还有表示前一个字符出现至少1次?表示出现0次或1次|表示或()用于分组{m,n}表示出现次数范围。我自己常用的一个例子是匹配IP地址grep -E ^[0-9]{1,3}(\.[0-9]{1,3}){3} access.log。虽然这个表达式无法严格校验IP是否合法比如“999.1.1.1”也会被匹配到但用来从日志中快速抓IP已经很够用。这里给新手一个学习建议不要试图一天内把所有正则语法都背下来先记住^ $ . * [ ]这五个在用的时候慢慢拓展到 ? | () {}。正则的难点不是语法而是“抽象地描述你要找的内容”这个能力只能靠多看例子、多写几次来培养。2.3 实战从日志里精准抓取错误现场假设有一个app.log内容混着INFO、WARN、ERROR三种级别每条日志开头是时间戳。我想快速知道今天有哪些ERROR并且大概什么时候发生的可以这样# 查看所有ERROR级别日志显示行号并忽略大小写 grep -ni error app.log # 只看ERROR后的堆栈第一行排除掉常见的空行和at com.xxx grep -A1 ERROR app.log | grep -v ^--$ | grep -v at | head -50-A1表示输出匹配行的后1行这在看异常堆栈时很有用-B1则输出前1行用于看上下文。如果你只是想统计某个错误出现了多少次用grep -c 特定错误 app.log即可。但要注意-c统计的是“匹配行数量”不是“出现次数”如果一行里出现了两次关键字它也只算一次。想统计准确的出现次数可以用grep -o 关键字 app.log | wc -l。我踩过一个很典型的坑想从代码里找出哪些引用了一个旧函数结果因为忘了加-w把所有包含类似字段名的行都搜出来了多了几十倍假结果。所以当你想精确匹配某个“完整词”时别吝啬-w。想进一步排除干扰内容时再叠加-v比如grep -w createUser *.php | grep -v function createUser就能找出调用点而不是定义点。3. 过滤sed流式处理与批量修改3.1 sed 的工作模型每一次处理都是逐行过sed是“流编辑器”它的设计思想非常朴素一次读一行把这一行放进“模式空间”执行你给出的编辑命令然后输出结果。整个过程不会把整个文件一次性加载到内存所以即使处理几个GB的日志它也能从容应对。sed的语法结构是sed [选项] 脚本 文件。脚本里最常写的两部分是“地址范围”和“操作命令”。地址范围指定对哪些行生效操作命令指定做什么。例如sed -n 10,20p file-n关闭默认输出10,20p表示只打印第10到第20行。没有地址时命令作用于所有行比如sed s/foo/bar/ file会替换所有行里的第一个foo。理解地址范围是sed的关键。地址可以是一个行号5、一个区间2,8、一个模式/pattern/也可以组合/start/,/end/。这些看似简单实际能玩出很多花样。比如只修改某个配置块内的旧参数就可以写sed /[mysqld]/,/^$/ s/port3306/port3307/ my.cnf它只会在[mysqld]到第一个空行之间替换端口配置。3.2 高频指令增删改查一次看完sed的指令不算多日常高频的是以下几个s替换。sed s/old/new/默认只替换每行第一个匹配要全部替换需加g标志写成s/old/new/g。d删除。sed 3,5d删除第3到5行sed /^#/d删除所有以#开头的行。p打印。通常配合-n用sed -n /ERROR/p file类似grep但它可以做更精细的行号控制。a行后追加文本。sed /end/a new line在匹配行之后插入内容。i行前插入文本。sed /end/i before line在匹配行之前插入内容。c整行替换。sed /pattern/c replacement把匹配行整行变成新内容。替换语法里的分隔符也可以换成#、等比如路径替换经常用sed s#/usr/local#/opt#g这样就不必对斜杠做转义了。符号可以代表匹配到的原内容比如把某个变量名包一层引号sed s/keyword// file结果会变成keyword。3.3 实战批量改配置、清空干扰行我用得最多的场景是批量修改配置文件。比如有一批服务器上部署了旧配置IP地址要统一从192.0.2.10改成198.51.100.23端口从8080改成9090。直接手工改几十个文件会疯掉而sed配合-i选项可以原地修改# 逐个替换先改IP再改端口 sed -i s/192\.0\.2\.10/198.51.100.23/g config.ini sed -i s/8080/9090/g config.ini注意IP里的点必须写成\.否则会匹配任意字符导致意外替换。这是新手最容易出事的地方。我还经常用sed做“清理”工作把文件里的空行和注释行去掉方便后续解析sed -e /^#/d -e /^$/d config.ini-e可以挂载多个脚本命令执行顺序是从左到右。如果要用更复杂的逻辑可以把多个动作写在一个花括号块里比如sed /^#/{s/^#//; s/^/comment:/} config.ini这表示对以#开头的行先去掉第一个#再在行首加上comment:。sed的语法虽然紧凑但写多了会非常顺手。使用-i之前强烈建议先备份sed -i.bak s/foo/bar/ file这样会生成一个file.bak的原文件副本。别问我为什么要强调备份我见过太多改完才发现正则写错、已经没法还原的惨剧。4. 数据流转awk让文本变成可计算的数据4.1 awk 的基本架构BEGIN、主体、ENDawk定位就不是简单过滤了它更像一个“微型数据处理语言”。awk处理逻辑分三段BEGIN块在读取文件前执行用来初始化变量或打印表头中间主体块会对文件的每一行执行一次END块在所有行处理完后执行常用于输出汇总结果。标准写法是awk BEGIN { 初始化 } { 每行处理 } END { 收尾计算 } 文件awk默认把一行按“空白符”空格或制表符拆成多个字段$1表示第1个字段$2表示第2个字段$0表示整行。NF是当前行的字段数量NR是当前已读入的行号。如果你想按冒号或竖线拆分就指定-F参数或设置FS变量。例如读取/etc/passwd时用-F:因为它的字段分隔符是冒号。4.2 内置变量与格式化输出awk里最常用到的内置变量就几个FS输入字段分隔符、OFS输出字段分隔符、NR当前行号、NF当前行字段数、$0整行、$1~$NF各字段。NR和NF经常被搞混记住一句话NR是“行号”NF是“这一行有多少列”。比如awk -F: {print NR, NF, $1} /etc/passwd会输出行号、字段数、用户名。awk的print默认用空格分隔输出如果你想输出“表格整齐”的效果就上printf。printf的格式符和C语言类似%s字符串、%d整数、%.2f保留两位小数。比如awk -F: {printf %s 的UID是 %d\n, $1, $3} /etc/passwdawk也支持完整的条件判断和循环常见的写法是模式匹配加动作。awk $3 100 {print $1}表示只处理第3列大于100的行。这里的比较运算会自动把字段转成数值非常方便。4.3 实战统计日志中的IP次数与金额汇总awk最能体现价值的就是做统计。假设某访问日志每行第一个字段是IP地址我想知道哪个IP访问最频繁一行命令就能搞定awk {count[$1]} END {for (ip in count) print ip, count[ip]} access.log | sort -k2 -nr | head -10这里count[$1]是awk里非常经典的“计数器数组”用法以IP为下标每出现一次就加1。处理完所有行后for (ip in count)遍历每个IP并打印次数再通过sort -k2 -nr按第二列数字倒序排列head -10取前十。整个过程就是数据流转的典型示范。再比如一个交易明细文件字段格式是用户名|订单号|金额现在要统计总金额、最大金额和订单条数可以这样awk -F| {if ($3 max) max $3; sum $3; count} END {print 条数:, count, 总金额:, sum, 最大单笔:, max} orders.txt-F|指定管道符为分隔符if ($3 max)用来找最大值sum $3累加金额。如果金额字段里混有货币符号或空格比如$100.5要先清理awk -F| {gsub(/[^0-9.]/,,$3); sum $3}这个gsub可以把第三个字段里除了数字和点之外的字符全部删掉。日志处理中这种“脏数据清洗”非常常见awk的gsub和sub函数就是为此而生的。5. 协作用管道与重定向实现数据流转5.1 一键串成流水线到了真正干活的时候通常不是单独用某一个命令而是把三板斧和排序、去重等命令串成一条管道。管道的含义很简单前一个命令的标准输出接住后一个命令的标准输入。看这个链子grep -E ERROR app.log \ | sed s/^2024-..-.. // \ | awk {print $1, $2} \ | sort \ | uniq -c \ | sort -nr \ | head -20这条命令做的是先从日志里筛出ERROR行然后sed去掉时间戳前缀awk取出前两个字段接着sort排序uniq -c计数再按次数倒序排列最后取前20。每一步的输出都在变化先是大文本再是筛选后的行再是精简字段最后变成统计结果。这就是“数据流转”最直观的体现。我习惯把长管道拆成多行来写行尾用反斜杠\续行这样不仅可读性好排查问题时也能逐段验证。如果某一步输出不符合预期可以把管道截断先在上一段加个| head看看。5.2 重定向的细节覆盖、追加、错误流管道处理的是命令之间的连接而要把数据“落地”到文件就要用重定向。是把输出写入文件并覆盖原内容是追加到文件末尾。这两者用错一次就可能出大问题尤其是s脚本里执行错误的重定向会瞬间清空原文件。我自己的习惯是凡是写文件的操作只要不是刻意清空一律用或者先执行到临时文件再改名。标准错误的重定向也要学会。有时你想把错误信息也纳入同一个文件可以写21意思是“把标准错误发送到当前标准输出指向的地方”。是bash里把两者都重定向的简写。常见的坑是运行脚本时只看到屏幕上的报错但想日志里分析时怎么都找不到就是因为没有重定向标准错误。tee是个很实用的命令它一边把数据写到文件一边还能让数据继续流向后一个命令。比如grep ERROR app.log | tee error.txt | wc -l既能把ERROR行存到文件又能统计行数。这在需要“留底”又要实时计算时非常方便。这里要特别提醒一个细节管道传递过程中如果中间的某个命令提前退出比如head -5只读了5行就结束上游命令可能会收到系统信号而异常终止。比如grep -r 关键字 / | head -1有时会刷出“Broken pipe”之类信息这不一定是你命令写错了而是管道自然断裂。了解这一机制的人不会被这种报错吓到。5.3 完整场景一个订单日志的清洗与汇总把前面的知识点串起来我用一个模拟场景彻底演示一遍。假设有一份order.log每行以|分隔格式是2024-06-01 10:00:00|user01|100.50|success 2024-06-01 10:05:00|user02|250.00|fail 2024-06-01 10:08:00|user01|30.00|success现在要做三件事只看success的订单计算每位用户成功订单的总额按总额从高到低输出。grep success order.log \ | awk -F| {amount[$2] $3} END {for (u in amount) print u, amount[u]} \ | sort -k2 -nr这里grep负责“过滤行”awk负责“分列求和”sort负责“最终排序”。如果你还要把金额格式化成两位小数可以再传入awk格式化。整个链条没有任何中间文件数据从order.log一路跑到屏幕效率很高。如果数据量巨大这种流式处理的优势会更明显因为内存占用非常小。5.4 管道与子shell的一个经典坑有不少人在编写shell脚本时会在管道里设置一个变量结果发现在管道结束后变量是空的。例如cat file | while read line; do count$((count1)); done echo $count # 输出为空或0原因是在bash中管道两侧的命令都运行在“子shell”里子shell中的变量修改不会影响当前shell。解决方法是使用进程替换或者先重定向文件while read line; do count$((count1)); done file或者让循环体在管道直接输出。这个坑很隐蔽但一旦理解“子shell”的概念以后就能少踩很多雷。顺带说一句写awk统计反而没有这个问题因为awk本身就是独立进程它会自己维护内部的变量状态。6. 常见问题与排查技巧实录6.1 grep 明明有内容却匹配不到遇到“文件里能看到某个词grep却搜不出来”的情况先别怀疑人生按顺序排查文件可能是UTF-16或GBK编码而终端环境是UTF-8。先用file 文件名看一下编码用iconv转码后再搜。文本里有不可见字符比如Windows下常见的CRLF换行。可以用cat -A 文件查看行尾是否有^M用dos2unix或sed去掉\r。你搜索的内容里包含正则元字符比如.会匹配任意字符[]会当作字符集合。如果只查找普通文本但要包含这些符号用grep -F 字符串关闭正则解析。忘了加-i但日志里是大写。我最后再强调一次怀疑匹配不上时先cat -A看一眼文件真实内容几乎所有诡异问题在裸露的原始字符面前都会无所遁形。6.2 sed -i 在Linux和macOS上的差异同样的sed -i s/a/b/g file在Linux上是没问题的但放到macOS上会报错因为BSD版sed要求必须带备份后缀。写法差异是# Linux GNU sed sed -i s/a/b/g file # macOS BSD sed sed -i s/a/b/g filemacOS里-i 表示不生成备份文件但那个空引号不能少。为了避免跨平台折腾更稳妥的做法是写脚本时用sed -i.bak s/a/b/g file并显式指定备份后缀这样两边都能接受。如果你在类Unix环境里要写可移植脚本可以优先考虑用更简单的perl -pi -e s/a/b/g file它的行为在大多数平台上是一致的。6.3 awk 条件比较中的数字与字符串陷阱awk里$1 100这类判断默认会尝试把$1转成数值来比较。但如果字段里带了单位或者符号比较值就会变得离谱。比如$1是50msawk会尝试从头解析数字可能得到50但如果开头就不是数字则会当作0。解决方法是先清洗awk {gsub(/[^0-9]/,,$1); if ($10 100) print $0} file加上0是为了强制把字段转为数值避免字符串比较造成的奇怪顺序。字符串比较和数值比较的规则不一样100 99在字典序下是false因为“9”大于“1”但在数值比较中就是true。这个原理一旦记牢很多诡异的结果就解释得通了。6.4 管道里的变量为什么消失前面提到过子shell问题。还有一个与管道相关的场景echo $var | read也可能出现变量失效。很多人习惯用管道把内容读进变量但read在管道里读入的数据无法传回当前shell。推荐改用IFS read -r var 字符串或者直接命令替换。例如count$(grep -c success order.log)命令替换是把命令的输出作为变量值这是最可靠的方式。需要提取多行数据时可以用mapfile或临时文件。记住一条经验在bash里尽量少依赖管道去传递“变量值”管道是用来传递“数据流”的。6.5 文本文件的中文乱码与行尾统一在日志分析、脚本输出中遇到乱码多半是字符编码不匹配。查看文件编码用file -i 文件转码用iconv -f 原编码 -t UTF-8 文件 新文件。很多Windows下生成的CSV文件还会带着CRLF行尾会让awk的最后一个字段莫名多出一个\r导致比较失败。这时候先统一转一下sed s/\r$// input.csv clean.csv或者直接用dos2unix clean.csv。数据流转最忌讳的就是“脏格式”所以我通常在进入排查流程时第一步就是做字符集和换行符的清洗。这也是为什么sed经常出现在管道的第二三段的原因。7. 最后再说两句实践心得我自己刚开始学这三板斧的时候也觉得记忆量太大grep一堆参数sed语法像天书awk更像一门编程语言。但后来发现真正让我记住它们的不是看书而是反复处理真实日志。你现在可以立刻打开一个日志文件或数据文本先试试grep 关键字 文件再加-i -v -E然后对结果执行awk {print $1}最后把这些输出接到sort和head后面。你会发现原来一行命令真的能完成一个“迷你数据分析过程”。如果让我给你一条最实在的建议我会说别追求一次记住所有参数先把grep -E、sed -n s/p/d、awk {print $N}这三组最常用的写好然后再一点点扩展。每次需要查新功能时用man grep或man awk现场翻但更重要的是结合场景去用比如给历史命令的日志写个统计脚本或者给某个配置文件写个批量修改命令。踩几次坑以后那些曾经觉得难记的语法会变成你肌肉记忆的一部分。最后分享一个小技巧在正式执行可能会改动数据文件的命令之前先去掉-i跑一遍把输出结果看一遍确认无误再加回-i真正落地。看起来多了一步操作实际上能救回很多次误操作。