做技术这行,Windows 和 Linux 之争聊了多少年,真正落到干活上,你会发现 Linux/Unix 系那套文本处理哲学,依然是效率上限最高的那一档。哪怕现在有各种 JSON 格式化工具、日志平台、Kibana 看板,但凡是你要在服务器上临时排查问题、批量处理数据、写个自动化脚本,最顺手、最通用的依然是那群老伙计:grep、sed、awk、sort、uniq、cut、xargs。这篇文章就把它们从头到尾串一遍,不讲花架子,全是在生产环境里真正能救命、能省时间的用法和套路。
这篇文章适合谁?刚接触 Linux 的运维和开发新人,想系统性补齐文本处理基本功的;或者已经在用但只会grep xxx和vim手动改文件的同学。我会从设计思想讲起,把命令怎么组合、为什么这么组合讲清楚,最后附上我踩过的坑和排查技巧。全程没有意识形态,没有多余废话,纯技术实战。
1. 设计思路:为什么文本处理是 Linux/Unix 的看家本领
1.1 一切皆文件与文本流的哲学
Linux/Unix 系统里有个核心抽象:一切皆文件。普通文件是文件,目录是文件,设备是文件,甚至进程的输入输出也是文件描述符。这意味着不管你面对的是什么数据源——日志文件、配置文件、命令输出、网络抓包结果——只要它能被读成字节流,你就能用同一套文本处理工具去处理它。
这套设计的能量在于:它不强制你使用某个特定软件打开特定格式,而是把所有数据都投影到“文本流”这一层。日志是文本,配置文件是文本,数据库导出的 CSV 也是文本。于是,grep找到的东西可以喂给awk重新排版,awk处理完的结果可以再交给sort排序,整条流水线通过管道|串起来,数据在内存里流动,根本不需要产生中间文件。这就是管道哲学——每个命令只做好一件事,通过组合完成复杂任务。
我举一个特别直观的对比。假设你要从一个 2GB 的 Nginx 访问日志里找出状态码 500 的请求,并按 IP 统计次数,Windows 下你可能要写 Python 脚本,读文件、正则匹配、字典计数、排序输出,至少二十行代码。Linux 下呢?
grep ' 500 ' access.log | awk '{print $1}' | sort | uniq -c | sort -rn一行搞定。而且这行命令从执行到出结果通常只要几十秒,因为它在管道里流式处理,逐个读取日志行,不需要一次性加载全部数据到内存。这就是基本功的价值。
1.2 组合优于单点工具
很多教程喜欢逐个命令讲,讲完grep讲sed,讲完sed讲awk,仿佛它们是孤立的。但实际工作中,单独用一个命令的场景很少,更多是像管道一样串联起来。我习惯把命令分成几类角色:
- 过滤类:
grep、awk,负责从数据流中筛选出符合条件的行或列。 - 变换类:
sed、tr、cut,负责修改行内容、替换字符、裁剪字段。 - 统计类:
sort、uniq、wc,负责排序、去重、计数。 - 执行类:
xargs,负责把前一个命令的输出变成后一个命令的参数或输入。
打个比方,就像工厂流水线。原材料是一大堆杂乱的日志文本,grep是先质检员,把符合特征的产品挑出来;awk是拆分工人,按空格把每个产品分解成零件编号;sort和uniq是仓库管理员,把零件按编号归类排序并清点数量;最后到head这里,只要出货量最大的前十个。理解这个角色划分,你自然就知道什么场景该用什么命令了。
1.3 为什么今天的开发者依然要学
你可能会说:现在有各种日志采集系统、数据库查询、可视化工具,文本命令是不是过时了?我自己的体验是,恰恰相反。看一下这些场景:Docker 容器日志不支持 SQL 查询,你只能docker logs拉出来然后字符串处理;Kubernetes 的kubectl describe输出是文本,你要 grep 关键状态;配置管理、CI/CD 脚本里到处都是对文本的判断和替换;甚至你用 Ansible、SaltStack 写 playbook,底层执行的还是命令。这些场合下,文本处理命令就是最通用、最不会被版本迭代淘汰的接口。掌握了这套基本功,你就拥有了在任何 Linux/Unix 环境里快速理解和操作数据的能力,这比记住某个工具的具体操作界面更有价值。
2. 查询与过滤:grep 和它的黄金搭档
2.1 grep 不是只会搜关键字
grep全称是 Global Regular Expression Print,全局正则匹配并打印。它最基本的用法确实是grep pattern file,但实战中我很少用这么裸的形式,多半会组合选项。下面这些是我日常出现频率最高的:
grep -E 'ERROR|FATAL' app.log # 扩展正则,匹配多个关键词 grep -w 'error' app.log # 精确匹配整个单词,避免匹配到 error_handler grep -i 'warning' app.log # 忽略大小写 grep -v 'debug' app.log # 反向匹配,排除包含 debug 的行 grep -c '200' access.log # 返回匹配行数,而不是内容 grep -l 'error' *.log # 只列出包含匹配的文件名 grep -r 'timeout' /etc/nginx/ # 递归搜索目录 grep -n 'listen' nginx.conf # 显示行号,改配置时特别有用 grep --include='*.conf' -r 'root' /etc/ # 只搜索指定后缀文件这些选项单独看不复杂,但组合起来能解决大问题。比如有一次我要排查线上所有虚拟主机配置里有没有重复的 server_name,直接grep -r 'server_name' /etc/nginx/conf.d/ | sort | uniq -d,几秒钟就定位到了重复项。
需要注意一个细节:grep -E用的是扩展正则,支持+、?、|、()这些元字符;基础正则grep里这些符号要加反斜杠转义才能用,写作\+、\|。语法习惯不同容易踩坑,我自己的习惯是统一用grep -E,省得记两套规则。还有一个隐藏技巧:grep支持-P参数使用 Perl 正则,比如需要\d、\w这种写法时非常方便,但要注意-P在某些精简基础镜像(比如 Alpine)里不支持,生产环境频繁切换容器要留意。
另外grep的退出码也有讲究。匹配到结果时返回 0,没匹配到返回 1,命令本身出错返回 2。合理利用退出码可以写判断逻辑:
if grep -q 'healthy' healthcheck.txt; then echo "服务健康" else echo "服务异常" fi-q是静默模式,只要状态码不要输出。在 Shell 脚本里这样用,比解析字符串靠谱得多。
2.2 cut、sort、uniq 无痛统计
grep负责“选行”,cut负责“选列”。用cut的时候核心是搞清楚分隔符和字段位置:
cut -d' ' -f1 /etc/passwd # 按空格切,取第一列(用户名) cut -d: -f1,3 /etc/passwd # 按冒号切,取第一和第三列 cut -d: -f2- /etc/passwd # 第二列到行尾之前那个统计 IP 的例子,里面awk '{print $1}'本质上做的也是选列的工作。cut和awk的区别在于cut更简单快速,适合分隔符规整的场景;awk则能处理更复杂的分隔符和条件逻辑。实话说,如果只是按固定字符取列,cut的执行速度略胜一筹,在几 GB 大文件上差距能明显感受到。
接着是sort。单独用sort只是按字典序排,但加上选项才能发挥威力:
sort -n numbers.txt # 按数值大小排序,而不是字典序 sort -k3 -t: /etc/passwd # 指定第3列排序,分隔符是冒号 sort -rn -k2 result.txt # 按第2列数值降序 sort -u file.txt # 排序并去重,等价 sort file.txt | uniq新手经常无视-n,结果 10 排在 2 前面,因为字典序里 '1' 小于 '2'。如果处理的是数字,务必加上-n。
uniq的逻辑有点反直觉,它只能去除“连续相邻”的重复行,不能全文去重。所以标准用法一定是和sort搭配:
sort access.log | uniq -c # 统计每行出现的次数 sort access.log | uniq -d # 只显示重复行 sort access.log | uniq -u # 只显示不重复行wc在统计场景也很常用:wc -l数行数,wc -c数字节数。结合grep就是写监控脚本经常用到的测试手段:
logs=$(wc -l < app.log) errors=$(grep -c 'ERROR' app.log) rate=$(awk "BEGIN {print ($errors / $logs) * 100}") echo "错误率: ${rate}%"利用awk的BEGIN可以直接在命令行里做浮点运算,不用另起计算器。
3. 流式编辑器 sed 与 awk 的实用战场
3.1 sed 的定位:批量替换与正则手术
sed全称 Stream Editor,流式编辑器。它最大的价值在于你不需要打开文件就能自动完成修改,特别适合批量操作配置文件、处理大数据量文本。最常用的就三个功能:替换、删除、打印指定行。
替换是 sed 的主场,基本格式sed 's/旧/新/标志':
sed 's/old/new/g' file.txt # 全行所有匹配替换,g 表示全局 sed 's/^#//g' nginx.conf # 删除行首的 #,即注释符号 sed 's/\/usr\/local/\/opt/g' path.txt # 路径中的斜杠需要转义斜杠转义很丑,实际写的时候我更喜欢用其他字符做分隔符,比如#或者|:
sed 's#/usr/local#/opt#g' path.txt sed 's|http://|https://|g' urls.txt删除行和打印行也是高频操作:
sed '/^$/d' file.txt # 删除所有空行 sed '10,20d' file.txt # 删除 10 到 20 行 sed -n '5,10p' file.txt # 只打印 5 到 10 行,-n 关闭自动打印 sed -n '$p' file.txt # 打印最后一行,$ 表示最后一行sed -i直接修改文件,这是日常最危险也最常用的选项。危险在于一旦写错没有后悔药,所以我强烈建议任何重要文件操作前先备份。带备份的写法更安全:
sed -i.bak 's/foo/bar/g' config.conf # 生成 config.conf.bak 再修改如果你想批量把某个服务的所有.conf文件里的端口从 8080 改为 9090,一条命令扫完:
sed -i 's/8080/9090/g' /etc/myapp/*.conf在使用 sed 处理配置文件时有个容易忽略的细节:如果你用sed -i修改了软链接指向的配置文件,某些系统上会直接把软链接替换为普通文件。原因在于sed -i的实现方式一般是创建临时文件再重命名,软链接关系就断了。我踩过一次这个坑,修复方法是修改前先readlink -f确认真实路径。
3.2 awk 的范式:列提取与统计逻辑
如果说 sed 是外科手术刀,awk 就是瑞士军刀。它不仅做文本处理,还能写分支条件、循环、数组统计,甚至可以直接当一门迷你语言用。awk 的基本执行模型是:逐行读取文件,按分隔符拆成多个字段,然后对每行执行{ }里的动作。$1表示第一列,$0表示整行,NF表示当前行的字段数,NR表示当前处理到第几行。
日常用得最多的几个场景我给拆开讲。
第一个是字段提取。awk '{print $1, $3}'比cut直观,而且还能插入自定义文本:
awk '{print "IP: " $1 " 状态: " $9}' access.log | head -20第二个是条件过滤。awk 的过滤能力实际上比 grep 更强,因为它可以按字段值过滤:
awk '$9 == 500 {print $1, $7}' access.log # 状态码字段等于 500 awk '$9 != 200' access.log # 状态码不是 200 awk '$11 > 0.5 {print $1, $11}' access.log # 请求耗时超过 0.5 秒的记录第三个是统计汇总。这是 awk 相对其他命令最强大的地方,因为它自带关联数组:
awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' access.log | sort -rn | head这段逻辑解释起来很简单:每读一行,以第一列 IP 为下标的计数器加一;文件全部处理完后,进入END块,遍历所有 IP 并打印次数,最后再接个sort -rn和head取出访问量最高的几个 IP。你可以用同样的办法统计 PV 数、统计各接口被调用的次数:
awk '{count[$7]++} END {for (url in count) print count[url], url}' access.log | sort -rn | head -20如果你想计算所有请求的平均响应时间,直接累加后除以行数:
awk '{sum += $NF} END {print "平均耗时:", sum / NR}' app.log这里$NF是取最后一列,假设你已经提前把耗时输出到日志最后一列了。awk 也支持浮点运算和格式化输出:
awk '{sum += $NF} END {printf "平均耗时: %.2fms\n", sum / NR}' app.log还有一个生产环境经常用到的场景——按行号精准提取日志片段。比如一个服务崩溃前最后的 50 行,可以先用grep -n找到关键错误在第几行,然后用sed -n或者awk 'NR >= 1000 && NR <= 1050'精准拉取范围。字符串和行号结合起来,比在 Vim 里翻页快得多。
3.3 注意 awk 的字段分隔规则
awk 默认按空白字符(空格和 Tab)分隔字段,多个连续空格也算一个分隔符,这点和cut -d' '按单字符分隔不同。如果日志列间是多个空格,用 awk 处理反而比 cut 更省心。但如果你想自定义分隔符,用-F:
awk -F':' '{print $1, $3}' /etc/passwd # 以冒号分隔,打印用户名和 UID awk -F',' '{print $2, $5}' data.csv # 以逗号分隔,处理 CSV顺便提一下,awk 里的BEGIN块在读取文件前执行,适合定义变量、打印表头;END块在文件处理完后执行,适合输出汇总结果。这三个块的结构让 awk 几乎成了一个小型的流式编程语言。我经常在写脚本时先本地造几个假数据行测试 awk 逻辑,确认无误再跑真实大文件,这一步能省下大量调试时间。
4. 批量操作与文件对比的进阶套路
4.1 xargs:把输出变成参数
xargs是一个典型的“桥接”命令,它读取标准输入,把它拆分成参数,然后传给后面的命令执行。没有它,某些命令无法直接从管道接收输入。比如find和grep、rm、cp的配合就很依赖它。
最经典的组合是批量删除和批量打包:
find /data/logs -name '*.log' -mtime +30 | xargs rm -f find /data/logs -name '*.log' -mtime +30 | xargs tar -czf old_logs.tar.gz这里-mtime +30表示修改时间超过 30 天的文件。第一条命令直接删掉过期日志,第二条把它们打包归档。注意管道左边是文件名列表,右边是rm或tar,中间的xargs相当于把它们一个个接上。
处理文件名带空格的问题是新手最容易踩的坑。默认情况下xargs用空格和换行作为分隔符,一个名为Game Log.txt的文件会被拆成Game和Log.txt两个参数。解决办法是让find用-print0输出以空字符分隔,xargs对应加-0:
find . -name '*.txt' -print0 | xargs -0 rm -f这行代码里-print0对路径中的空格、换行、特殊字符一律免疫,是打包删除、批量移动时最稳的写法。虽然-print0可能让你一时不习惯,但养成习惯后真的能避免很多玄学故障。
xargs还有几个好用的选项。-n1表示每条命令只用一个参数,配合下载或逐个处理很常用:
cat urls.txt | xargs -n1 curl -O-P可以指定并发进程数,实现并行处理:
cat urls.txt | xargs -P 8 -n1 curl -O-I可以自定义占位符,实现更灵活的参数位置控制:
find . -name '*.txt' | xargs -I {} mv {} /data/backup/这里{}会被实际文件名替换,然后传给mv。
不过要注意一个隐藏问题:管道中输出的行数特别多时,xargs会把参数批量分组执行多次,这是它的内部机制。像xargs rm -f即使有一万个文件,也会分批执行,不会因为参数列表过长报错。但如果你的命令本身需要一次性接收全部参数(比如某些不支持多次调用的命令),就要考虑用xargs -n控制分组。
4.2 find 与 grep 的协作检索
find常常被划入“查找文件”而不是“文本处理”,但它在文本处理流水线里其实是个重要的起点:它能快速圈定要处理的文件集合,再交给后面的文本命令。除了按文件名,我平时最常用的是按修改时间、权限、所属用户过滤:
find /var/log -name '*.log' -mmin -60 # 最近一小时内修改过的日志文件 find /etc -type f -perm -644 # 找出权限为 644 的普通文件 find /home -user zhangsan -name '*.sh' # 指定用户的脚本文件配合grep检索代码、配置文件里的内容也很顺手:
find /opt/app -name '*.conf' -exec grep -l 'password' {} \; find /opt/app -name '*.yml' | xargs grep -E 'replica|master'第一条里-exec ... {} \;是 find 自带执行机制,{}是文件占位符;第二条则用管道交给 xargs-grep。两种方式效果差不多,-exec更安全,不用担心里面有空格文件名的分割问题,但性能上xargs并行效率更优。如果你追求简单可靠,我通常建议-exec;处理海量小文件时再换xargs -P提速。
4.3 diff 和摘要式响应,检测文件差异
diff是行级对比工具,常用于比较配置文件的差异、代码变更、两份导出数据。基础用法是diff file1 file2,输出是二选一差异列表。不过默认格式可读性一般,我更推荐统一格式:
diff -u old.conf new.conf # 带上下文和 + - 符号的统一格式 diff -rq dir1/ dir2/ # 比较两个目录哪些文件不同 diff -u <(cat remote.conf) local.conf # 进程替换,直接比较远端文件和本地文件如果只需要知道“两个文件是否一致”,用diff -q或者cmp更快,cmp到第一个不同字节就会停下来,不带多余输出。diff配合patch可以生成和应用补丁:
diff -u old.txt new.txt > change.patch patch < change.patch现在 Git 已经内置了类似功能,但理解diff和patch的原理依然有助于了解版本控制的底层逻辑。
这里有个容易踩的坑:Linux 和 Unix 系统上的换行符通常是\n(LF),Windows 是\r\n(CRLF)。如果你在 Windows 上编辑了文件再传到 Linux,用diff对比会出现“看起来完全一样,却整行都被标记为差异”的情况。解决方法是先转换换行符再对比:
sed 's/\r$//' win_file.txt > linux_file.txt diff -u linux_file.txt original_linux.txt或者直接安装dos2unix工具完成转换。类似的问题还有文件末尾是否有换行符、是否有 BOM 头。这些细节在排查“为什么配置完全一样但程序不识别”时经常会遇到,提前知道能省不少功夫。
4.4 tr 与字符集处理的隐藏价值
tr用于字符集转换、删除、压缩。它不能处理单词和正则,只能处理单个字符或字符类,但正因为简单,在处理批量小改动时效率极高。
tr 'a-z' 'A-Z' < file.txt # 小写转大写 tr '\t' ',' < data.tsv > data.csv # Tab 转逗号 tr -d '\r' < file.txt > file_clean.txt # 删除所有回车符,处理 CRLF tr -s ' ' < file.txt # 把连续多个空格压缩成一个tr -s在日志分析里特别实用。某些命令输出有大量空行或空格,先用tr -s压缩,再做列提取,就不容易被空字段干扰。tr的输入输出都是标准文件流,所以它和管道配合非常顺手。
还有一类场景,我们需要把命令输出的多行变成单行,比如把公钥拼到一行写入 authorized_keys,这种活一般用xargs或tr:
cat id_rsa.pub | tr -d '\n' >> authorized_keys或者在处理文件列表时,把多行文件名转成逗号分隔:
ls *.log | tr '\n' ',' | sed 's/,$//'5. 运维现场常见问题与排查技巧
5.1 命令组合中的权限与缓冲陷阱
组合命令时最常见的问题就是权限。用find或grep扫描/var/log时,有些日志文件只允许 root 访问,普通用户运行时你会看到一堆Permission denied错误刷屏。解决方法是把标准错误重定向掉:
grep 'ERROR' /var/log/*.log 2>/dev/null find / -name '*.conf' 2>/dev/null | head另一个容易被忽视的坑是管道缓冲。默认情况下,管道是缓冲的,不是立即逐行传递的。当你执行tail -f app.log | grep ERROR时,grep 的输出往往不会立刻显示,要等缓冲区满或进程结束。这个特性在服务日志实时监控时非常烦人。解决办法是用grep --line-buffered强制行缓冲,或者干脆用stdbuf -oL修改标准输出的缓冲方式:
tail -f app.log | grep --line-buffered 'ERROR' tail -f app.log | stdbuf -oL awk '{print $4, $1}'这个细节我在刚接触 tail -f 管道时困惑了很久,以为是命令没生效,实际上只是输出被缓冲了。如果你在实时观察日志时发现结果半天不出来,优先检查是不是缓冲问题。
还有一个和字符编码相关的坑:某些系统区域的 locale 不是 UTF-8,grep处理含中文的日志时可能报错或匹配不到。排查时可以先看看当前 locale:
locale export LC_ALL=C.UTF-8在脚本里混用grep -P时尤其要注意 locale 对正则库的影响,必要的时候销毁现场,带上LC_ALL=C跑任务,规避掉这层干扰。
5.2 大日志分析的标准排查流程
我处理线上经常出现“服务变慢、报错陡增”这类问题,会把整套流程固化成几个基本步骤:先看时间范围,再做筛选,再统计 TOP N,最后关联上下文定位。具体到命令就是一组组合拳:
# 1. 先看总请求数与错误数 wc -l < access.log grep -c ' 500 ' access.log # 2. 确认错误的时间段分布 grep ' 500 ' access.log | awk '{print $4}' | cut -d: -f2 | sort | uniq -c # 3. 按接口统计错误请求 TOP grep ' 500 ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 # 4. 提取一段时间的日志上下文 grep -E '2024-06-01T10:1[5-9]' app.log | sed -n '1,200p'这套流程几分钟内就能让你大致拼出故障图景:某个特定接口在某一分钟开始大量 500,错误消息是什么,当时请求量有没有突增。我习惯把每一步的统计结果简化成表格或者数字,而不是直接导出原始日志,因为原始数据太容易淹没真正的问题了。
5.3 文本处理命令易踩坑清单
用过一段时间后你会发现,这类工具真正的问题多半不是“不懂选项”,而是“在错的格式上操作”。下面列几个高频坑,我基本每个月都会在同事那边看到其中一两例。
第一个坑是 grep 匹配到了文件本身或二进制文件。grep -r搜索目录时会扫描所有文件,包括二进制文件和压缩文件,输出乱码还可能卡住终端。稳妥的做法是加-I忽略二进制文件,或者用--include='*.log'限定文件类型。
第二个坑是sort -u与uniq的混用。很多人以为uniq可以不排序就完成全局去重,实际上它只处理相邻行,乱序文件里重复的行相距很远就不会被合并。所以规则很简单:想全局去重就sort -u;想统计次数就sort | uniq -c。
第三个坑是sed -i修改文件时误伤不匹配行。s///不带g标志时只替换每行第一个匹配,这是正常的,但如果你以为它是全局替换而漏看选项,结果会出乎意料。建议替换前先不加-i跑一遍看看输出:
sed 's/127.0.0.1/0.0.0.0/g' app.conf | diff app.conf - # 检查差异确认无误后,正式用sed -i执行。
第四个坑是管道命令的退出状态。管道中最后一个命令才决定整个管道的退出码。比如grep 'error' file | wc -l,即使 grep 没找到任何内容,管道整体的退出码也是wc -l的 0,因为 wc 成功执行了。如果你在脚本里用if 管道判断是否匹配,会永远进入成功分支。正确做法是直接if grep -q 'error' file,或者用set -o pipefail让管道中任一命令失败都返回非零。
5.4 踩坑后总结的两个个人习惯
说实话,文本处理命令本身不难,难的是在你面对的海量日志和诡异配置中保持冷静。我这里有两个从实践中养成的习惯,可能对你也实用。
第一个习惯是“先看头尾再处理”。面对一个新日志或新配置文件,先用head -5和tail -5看一眼,确认列结构、分隔符、时间格式,再做 awk 和 cut。跳过这步直接跑 awk,经常因为列位置不对而输出整片空白。看 CSV 文件我甚至会先跑head -1确认表头,字段数量有心算概念再写命令。
第二个习惯是“命令全部先打印再执行”。凡是涉及修改、删除的操作,我会先跑一遍不带写动作的版本,把输出或匹配结果看清楚了再真改。删除日志用find ... | xargs rm前,先跑find ...看列表;批量替换前先跑 sed 不加-i。多花半分钟,却可能挽回一整天的损失。这不能说是什么高明技巧,只是踩了太多坑以后形成的肌肉记忆。希望你把这些坑都记下来,少走我走过的弯路。