Shell文本处理四件套:cut、sed、awk、sort原理与实战
2026/9/15 21:35:05 网站建设 项目流程

很多人学 Shell 文本处理,上来就背参数,背了三天发现还是不会用。真到了日志文件面前,一排数据不知道用 cut 还是 awk;想改配置文件,sed 和 perl 到底哪个顺手也分不清;sort 排完序发现数字排得乱七八糟,怀疑人生。

这套东西其实没那么玄。cut、sed、awk、sort 合称文本处理四件套,各有明确分工:cut 管按列切,sed 管按行改,awk 管结构化分析,sort 管排序聚合。把这四个命令的底层逻辑摸透了,90% 的日常文本处理场景都能直接覆盖。这篇我按自己多年的实际使用经验,从原理到实战,把每个命令的边界、常见坑、组合用法一次说透。

1. 先搞清楚分工:四个命令到底各管哪一段

很多人学这四个命令最大的误区,是拿它们互相替代。有个同事处理 CSV,非要用 awk 切第一列,明明cut -d, -f1一行就完了,他写得又长又容易错。反过来,有人拿到 JSON 日志想格式化,用 sed 写了一大堆正则,最后发现根本搞不定嵌套结构——这就是典型的工具选型错误。

我习惯用一个简单的类比来记这四个命令的职责:

命令处理维度擅长场景类比
cut按列(字段)结构化文本、CSV、定长文本一把切刀,按位置切
sed按行(流)替换、删除、插入、行范围处理一条流水线,逐行加工
awk按记录+字段条件统计、格式化输出、多文件关联一台小型数据处理机
sort全局排序按字段排序、去重、合并统计一套分类整理系统

这四个命令的分工是有历史渊源的。cut 来自早期的文本处理工具集,设计目标就是"切字段",所以它只关心列。sed 是流编辑器,继承了 ed 编辑器的命令语法,核心是"对每一行执行编辑操作"。awk 则完全不同,它是三个人(Aho、Weinberger、Kernighan)用 C 写的"数据驱动语言",天生就带了变量、循环、条件分支。sort 来自老 Unix 的排序程序,核心追求是稳定、高效、支持复杂排序键。

在实际工作中,我通常这样判断用哪个:

  • 只取某一列、某几列 → 优先 cut,别一上来就 awk
  • 要把文件中某个词批量换成另一个词 → 优先 sed
  • 要做统计、求和、分组汇总 → 必须 awk
  • 要把结果按某列排大小 → sort 自然衔接

四者不是竞争关系,而是上下游关系。一个典型的处理流水线是:cat access.log | cut -d' ' -f1 | sort | uniq -c | sort -rn——先用 cut 取 IP 列,再用 sort 把相同 IP 排到一起,uniq -c 去重并计数,最后再 sort -rn 按次数降序排列。你看,每一环干一件事,各司其职,这就是四件套的正确用法。

2. cut:格式化文本的"切片刀",基础用法与边界在哪

cut 这命令看起来简单到不值一提,但正因为大家觉得它简单,很多边界情况反而没人讲清楚。

2.1 核心机制:它只认"分隔符"和"字节/字符位置"

cut 的底层逻辑是:把每一行当作一串字符,然后按你指定的方式来"切"。常见的三个参数:

  • -d指定分隔符(默认是 Tab)
  • -f指定取第几个字段(配合 -d 使用)
  • -c按字符位置切
  • -b按字节位置切

比如/etc/passwd文件,每行用冒号分隔成 7 个字段:

cat /etc/passwd | cut -d: -f1,3

这行的意思是:以冒号为分隔符,取每行的第 1 和第 3 个字段。输出就是"用户名:UID"两列。这里有个关键细节:多个字段用逗号分隔,连续范围用-,比如-f1-3取前三列,-f1,3-5取第 1 列和第 3 到第 5 列。

注意,cut 不支持"按分隔符分割但跳过空字段"这类复杂逻辑。如果文本里连续两个分隔符,表示中间有个空字段,cut 会老老实实地把空字段算进去。这一点跟 awk 默认行为不同,后面讲 awk 时我会对比。

2.2 按字符位置切:处理定长文本的利器

很多刚从 Windows 转过来的同学,处理日志时都忽视了-c的威力。比如某系统导出的文件,每一行格式是:前 8 位日期、第 9 到 12 位时间、后面是消息内容,没有分隔符。用 awk -F 指定不了分隔符怎么办?这时候直接:

cut -c1-8,9-12 file.txt

就能只保留日期和时间两列。这里要强调一点:-c是按"字符"切,-b是按"字节"切。在纯 ASCII 文件里两者没区别,但一旦涉及中文等多字节字符,-c能正确处理字符边界,而-b按字节切容易把中文字符劈成两半。真正处理 UTF-8 中文文本时,我一般用-c,除非我有意要按字节切来做编码处理。

还有一个实用小技巧:cut -c1-表示从第 1 个字符到行尾,cut -c-5表示从行首到第 5 个字符,cut -c3-5则是第 3 到第 5 个字符。注意没有cut -c3-cut -c-5这种写法,实际它们是合法的,分别表示"从第 3 个到结尾"和"开头到第 5 个"。

2.3 实战中容易踩的坑

坑一:默认分隔符是 Tab,不是空格。很多日志文件用多个空格对齐,你用cut -d' '会发现切出来的字段超级多,因为连续空格中间全是空字段。这时候要么先用tr -s ' '把连续空格压缩成一个,要么干脆用 awk。awk 默认会把连续空格当作一个分隔符,这是它比 cut 在这类场景下更灵活的原因。

坑二:cut 不能重新排列字段顺序。cut -f2,1不会把第 2 列排到第 1 列前面,它永远保持原文件中的字段顺序,只会忽略不选的字段。如果你想调换列顺序,得用 awk 或者 paste 来组合。要记住:cut 是"减法",不是"重组"。

坑三:管道里遇到空行。如果某一行是空的,cut 处理完输出还是空行,这个没问题;但如果一行里分隔符数量不够,比如你-f5但该行只有 3 个字段,cut 会直接跳过这一行不输出。这一点常常被忽略,导致处理后的行数和原文件不一致。做数据校验时务必留意。

3. sed:流式编辑的核心是"模式空间",不只用来替换

sed 大概是四件套里最让人又爱又恨的。爱的是它一条命令搞定一堆文件的批量替换,恨的是它那一堆眼花缭乱的参数和地址范围写法。我个人的经验是:先把 sed 的工作模型搞懂,参数自然就通了。

3.1 工作模型:每一行进来,走一遍脚本,输出,走人

sed 的全称是 stream editor,流编辑器。它的运行机制可以概括为三步:

  1. 从输入流中读一行,放进"模式空间"(pattern space)
  2. 对模式空间里的内容执行你给定的脚本命令
  3. 把处理完的内容输出(默认行为),然后读下一行

这个循环往复的过程,决定了 sed 的两个重要特性:它天生擅长单行并行处理,不会把整个文件加载进内存(所以能处理超大文件);它对多行的跨行匹配支持较弱(需要用到NH等命令,属于进阶内容)。

常用形式:

sed 's/旧/新/g' file.txt # 替换 sed '/pattern/d' file.txt # 删除匹配行 sed -n '1,20p' file.txt # 打印前20行 sed -i 's/foo/bar/' file.txt # 原地修改(原地替换)

很多面试题喜欢考sedgrepawk什么时候加-n。这里统一规律:凡是"只输出我想要的",就加-n;凡是"在原有基础上修改",就别加。比如sed -n '1,20p'是只打印前 20 行,如果没有-n,sed 会先把每一行原样输出一遍,再把前 20 行重复输出一遍,得到的是 2N 行垃圾。

3.2 替换命令 s 的细节:分隔符、分组引用与全局标志

s/旧/新/是 sed 里最常用的子命令。这里有几个细节值得单独说:

分隔符不一定是斜杠。如果替换内容里包含http://这种带斜杠的字符串,逐个转义反斜杠会让你写到心态爆炸。sed 允许你把分隔符换成别的字符,比如#|

sed 's#http://#https://#g' config.txt

这条命令把http://替换成https://,因为分隔符用了#,所以 URL 里的斜杠完全不用转义。这是老手们的惯用写法。

分组引用用\1\2比如要把key=value格式改成value:key,可以这样:

sed 's/\(key\)=\(value\)/\2:\1/' file.txt

注意括号要写成转义形式\(\)。这是 sed 的 BRE(基本正则表达式)语法要求,跟 grep 默认模式类似,跟 awk 和 grep -E 的 ERE(扩展正则)不同。每次从 grep 切到 sed 我都会下意识踩这个坑,现在养成的习惯是:写 sed 正则时,凡是需要分组的,第一时间提醒自己加反斜杠。

替换标志默认只替换每行第一个匹配。这是新手最容易踩的坑。sed 's/foo/bar/' file.txt把每行的第一个 foo 换成 bar,后面的 foo 原样保留。要全局替换必须加g。而g也不是唯一的标志,还有i(忽略大小写)、p(打印替换后的行)、w 文件(写入文件)等。组合使用时注意顺序:s/foo/bar/gp这样写是正确的。

3.3 不只是替换:删除、打印、插入、修改

多数人用 sed 只停留在 s///,但 sed 的编辑能力远不止此:

  • d删除匹配的行
  • p打印匹配的行(配合 -n)
  • i在匹配行前插入一行
  • a在匹配行后追加一行
  • c用新文本替换整行

举例,在包含version = 1.0的行后面插入一行注释:

sed '/version = 1.0/a # this is the version line' config.ini

再比如,删除所有空行:

sed '/^$/d' file.txt

这些地址匹配不仅支持正则,还支持行号和范围。sed -i '100,200d'直接删除第 100 到 200 行,做文件瘦身时特别方便。

3.4 sed 处理大文件时的性能优势与 -i 的注意事项

sed 是流式处理,不需要把整个文件装载到内存,所以即使是几十 GB 的日志,用 sed 做替换也基本不会卡死。我给一个真实案例:之前处理一个 30GB 的 Nginx 访问日志,需要把HTTPS替换成HTTP2,用 sed -i 一条命令跑完,大概耗时几秒到十几秒(取决于磁盘 I/O),内存占用几乎可以忽略。而如果你用 Python 读入整个文件再写回内存,30GB 直接爆内存。

不过-i这个原地修改参数有个大坑:它是先创建一个临时文件,把处理后的内容写入临时文件,再替换原文件。如果你的磁盘空间不足,写入会失败,而原文件可能已经被截断。所以生产环境执行 sed -i 之前,务必确认磁盘余量充足。更稳的做法是先输出到新文件,确认无误后再用mv覆盖原文件:

sed 's/foo/bar/g' file.txt > file.tmp && mv file.tmp file.txt

-i后面可以直接带后缀,表示备份原文件,比如sed -i.bak 's/foo/bar/g' file.txt,这样原文件被保存为 file.txt.bak。第一次做批量修改我强烈建议加这个后缀。

提示:千万别在-i和正则之间留奇怪的空格,写成sed -i.bak 's/...',别写成sed -i .bak 's/...',后者含义完全不同,会直接报错。

4. awk:真正理解它的执行模型,才算掌握了这门外语

awk 被很多人当成"比 cut 高级一点点"的字段提取工具,这是对 awk 最大的误解。awk 实际上是一种完整的编程语言,有变量、数组、函数、循环、条件分支。它的强大不在于"取字段",而在于"按记录+字段做结构化的数据分析"。

4.1 三个关键概念:记录、字段、与默认行为

awk 的基本模型是:

  • 把输入按"记录"分隔,默认记录分隔符是换行符\n,也就是每一行是一条记录
  • 每条记录再按"字段分隔符"分成多个字段,默认字段分隔符是"连续的空白符"(空格或 Tab)
  • $0表示整条记录,$1表示第一个字段,$2是第二个字段,以此类推

注意,awk 的默认字段分隔符和 cut 不同。cut 默认 Tab,awk 默认连续空白。这意味着处理对齐的文本时,awk 直接$1$2就行,不需要提前压缩空格。这一点让 awk 在很多场景下比 cut 更顺手。

awk 的基本语法结构是:

awk 'pattern { action }'

对于每条输入记录,如果 pattern 匹配(或者省略 pattern),就执行 action。如果省略 action,则默认打印整条记录(等价于{ print })。如果省略 pattern,则每条记录都执行。

这个"pattern 匹配才执行 action"的结构,让 awk 天然就是一个条件过滤器加处理器。比如:

awk '$1 == "ERROR" { print $2, $3 }' log.txt

意思是:找到第一列是 ERROR 的行,打印第二列和第三列。

4.2 BEGIN 块和 END 块:数据处理的前后端

awk 脚本可以分成三个段:

BEGIN { ... } # 读取第一行之前执行一次,通常用来初始化变量、打印表头、定义分隔符 { ... } # 对每一行执行一次,主处理逻辑 END { ... } # 所有行处理完之后执行一次,通常用来输出汇总结果

举个经典例子:统计日志文件行数、总字节数、平均行字节数:

awk '{ total += $0; count++ } END { print "lines:", count; print "avg length:", total/count }' file.txt

BEGIN 块最常见的作用是设置字段分隔符。比如处理 CSV:

awk 'BEGIN { FS="," } { print $1, $3 }' data.csv

这里的FS是字段分隔符变量,在 BEGIN 里设置比在命令行用-F,要更直观,尤其是脚本复杂的时候。同样的,OFS是输出字段分隔符,设置成逗号可以方便地生成 CSV 结果:

awk 'BEGIN { FS=","; OFS="," } { print $1, $3 }' data.csv

4.3 变量、数组、循环:awk 的"迷你编程语言"身份

awk 的变量不用声明类型,直接赋值就能用,默认值是 0 或空字符串。这一点非常方便但也很容易写出隐蔽 bug。

数组是 awk 处理统计类问题的大杀器。awk 的数组是关联数组,下标不一定是整数,可以是任意字符串。最经典的用法是"按 key 计数":

awk '{ count[$1]++ } END { for (ip in count) print ip, count[ip] }' access.log

这条命令统计 access.log 里每个 IP 出现了多少次。核心逻辑就一行:count[$1]++。第一次遇到某个 IP 时,count[ip]默认是 0,++ 之后变成 1;后面每遇到一次再加 1。END 块里用 for 循环遍历数组,逐个打印 IP 和计数。

这种"关联数组 + 自动初始化"的组合,用来做分组统计简直是降维打击。用 Python 你得先设 defaultdict(int),用 awk 直接写就行。

再比如,找出访问量前 10 的 IP:

awk '{ count[$1]++ } END { for (ip in count) print count[ip], ip }' access.log | sort -rn | head -10

先按count[ip]和 IP 打印,再交给 sort -rn 排序,最后 head 截取前 10。这就是前面说的"四件套协同工作"的典型例子。

awk 也有循环和条件分支,语法跟 C 非常接近:

awk '{ if ($3 > 100) print $1, "high"; else print $1, "low" }' data.txt

还有awk 'NR >= 10 && NR <= 20'这种按行号卡范围的写法,NR 是内置变量"当前已读记录数",处理日志分段时特别好用。

4.4 几个必知的内置变量和内置函数

初学者熟练掌握这几个内置变量,基本可以应付大多数场景:

变量含义典型使用
NR当前记录的序号(行号)NR==1只处理第一行
NF当前记录的字段数打印字段数,判断行是否完整
FS输入字段分隔符BEGIN 里设置为 FS=","
OFS输出字段分隔符设置后 print 用逗号分隔自动生效
RS输入记录分隔符默认换行,可改成空行来按段落处理
$0 / $1 / $n整行 / 第 n 个字段数据提取核心

内置函数里,我常用这几个:

  • length(s)返回字符串长度
  • substr(s, start, len)截取子串
  • split(s, arr, sep)按 sep 分割字符串到数组
  • gsub(old, new, s)全局替换字符串中的内容
  • toupper(s)/tolower(s)大小写转换
  • sprintf(format, ...)格式化字符串

一个实战例子:日志里时间戳是2025-01-15 14:30:22,想只取日期部分:

awk '{ print substr($2, 1, 10) }' log.txt # 这里假设 $2 是 "2025-01-15"

如果时间戳在$2里是2025-01-15T14:30:22,则substr($2,1,10)也能抽出日期。

4.5 awk 常见的坑:字段分隔符、浮点精度、空文件没输出

坑一:FS 是正则表达式,不只是字符。-F','-F', '含义完全不同,后者表示逗号加空格。如果你希望按多个可能的分隔符切,可以用正则:awk -F'[,;:]'同时按逗号、分号、冒号切分。这个特性很有用,但很多人不知道。

坑二:浮点运算精度。awk 用的是 C 的 double 浮点数,处理大数或精确小数时会有精度损失。比如算金额,0.1 + 0.2在 awk 里也会输出0.3,但实际浮点存储并不是精确的。需要精确到分的场景,建议把数值放大成整数再算,避免浮点误差。

坑三:文件为空啥也不输出。awk 'END { print NR }' empty.txt会输出 0,因为 END 块总会执行。但awk '{ print }' empty.txt什么都不输出。这个逻辑本身没问题,但新手容易误解:以为 END 是"处理完后"的必然输出。实际上,如果文件没有任何记录,主块和 END 块虽然都会执行,但主块没有机会执行任何打印。

坑四:printprintf的区别。新手用print拼接变量很容易写出丑陋的输出。print $1, $2默认用空格分隔字段,print $1 $2则完全没分隔符。想要控制格式,用printf,语法跟 C 一样:

awk '{ printf "IP: %-15s count: %d\n", $1, $3 }' file.txt

5. sort:排序不是 sort 一个单词的事,三个细节必须吃透

sort 命令表面上是"给文本排序",但每次看到有人用sort file.txt之后输出一堆不符合预期的结果,我就知道肯定有人没搞清楚 sort 的默认排序规则和坑。

5.1 默认排序是字典序,不是数值序

这是 sort 最大的陷阱。sort file.txt默认按字典序(lexicographic)排序,也就是按字符的 ASCII 码顺序排。"10" 会排在 "2" 前面,因为字符 '1' 的 ASCII 码比 '2' 小。如果你要对数字进行真正的数值排序,必须加-n参数:

sort -n numbers.txt

这个-n-g有区别。-n处理常规整数和有限的小数;-g是通用数值排序,支持科学计数法(如1e3)、NaNInfinity等,但性能较慢。实际工作中绝大多数场景用-n就够了。

5.2 指定排序键:第几列、分隔符、列范围

排序日志、表格时,我们往往不是排整行,而是按某一列排序。sort 用-k参数来指定排序键:

sort -t: -k3 -n /etc/passwd

这条命令按冒号分隔,取第 3 列(UID),按数值大小排序。注意参数顺序:-t指定分隔符,-k指定第几列,-n表示数值排序。

-k还可以指定列范围,比如-k2,3表示从第 2 列到第 3 列作为排序键。这在结果需要"先按第二列排,再按第三列排"时很有用。sort 支持多级排序键:-k1,1 -k2,2n表示先按第一列字典序排,在第一列相同的情况下,再按第二列数值排。注意-k1,1的写法:逗号后面指定的是结束列,如果只写-k1表示从第 1 列到行尾。这两个写法的排序结果顺序完全不同。

5.3 sort 的稳定性与去重:uniq 为什么非要配上 sort

一句话总结:uniq 只能去除"连续且相同"的行,所以必须先 sort 让相同内容排到一起,再 uniq 才有意义。

sort file.txt | uniq -c

uniq -c统计每行连续出现的次数。它不会去重非连续的行。所以如果直接用 uniq 去对一个完全没排序的文件去重,大概率会漏掉重复项。

sort 加-u也能去重,但它和uniq的行为在列排序时会有微妙差别。sort -u 是"整个排序键重复就只保留一个",而 uniq 是对"相邻的完全相同的行"做合并。如果你用 sort 指定了-k只按第一列排序,那么-u去重时也只看第一列,后两列即使不同也会被丢弃。这个行为有时候很方便,有时候很坑。

sort 命令另外两个高频参数:

  • -r降序排列,从大到小
  • -k2,2nr组合写法:按第二列、数值、降序排

这里-r也可以单独写,但要记住它是对整个排序过程生效还是对某个键生效。放在-k后面(如-k2,2nr)只对当前键生效;单独写-r对所有键生效。

5.4 区域设置对排序的影响

一个经常被忽略的坑:LC_ALL=C环境变量不同,排序结果也可能不同。GNU sort 在 UTF-8 locale 下默认按当前区域设置的排序规则来排,这会导致大写字母、小写字母、带重音字母之间的相对位置跟纯 ASCII 字典序不一样。

如果要确保跨机器、跨环境排序结果一致,最好显式加上:

LC_ALL=C sort file.txt

LC_ALL=C强制 sort 按字节排序,这在处理纯英文和纯数字时最可预测。对于中文文本排序,LC_ALL=C会按字节(UTF-8 编码顺序)排,看起来不一定是字典序,所以具体用哪个区域设置要看你的需求。

另外,sort默认把每行末尾的换行符忽略,但不会忽略行内的开头空格。如果你想忽略开头空格做排序,加-b

sort -b file.txt

有些日志的对齐空格特别多,不加 -b 排序结果会乱。

6. 四件套组合实战:日志分析的标准链路

开头我说了四件套是上下游关系,这一节我用一个完整的真实案例把四个命令串起来。假设有一个 Nginx 访问日志 access.log,日志格式是:

127.0.0.1 - - [15/Jan/2025:14:30:22 +0800] "GET /api/v1/users HTTP/1.1" 200 1234

需求:统计访问量最高的前 5 个接口路径,并输出每个接口的访问次数。

6.1 第一步:用 awk 提取关键字段

日志里接口路径藏在$7(请求路径),状态码在$9。先用 awk 把$7字段提取出来:

awk '{ print $7 }' access.log

如果只想统计状态码为 200 的请求,可以在 awk 里加条件:

awk '$9 == 200 { print $7 }' access.log

6.2 第二步:用 sort + uniq 计数

把上一步的输出通过管道传给 sort 排序,再 uniq -c 计数:

awk '$9 == 200 { print $7 }' access.log | sort | uniq -c

6.3 第三步:用 sort 对计数结果降序排序

uniq -c 输出的第一列是计数,第二列是路径。按第一列数值降序排:

awk '$9 == 200 { print $7 }' access.log | sort | uniq -c | sort -rn

这里的sort -rn是先以数值(-n)排,再反向(-r)降序。注意,sort -rn默认对整个行做排序,但因为我们排序的对象第一列是计数、后面是路径,所以结果会按计数从大到小排。

6.4 第四步:head 取前 5

最后接个 head:

awk '$9 == 200 { print $7 }' access.log | sort | uniq -c | sort -rn | head -5

这一条管道命令,用到了 awk(提取 + 条件过滤)、sort(分组)、uniq(计数)、sort(数值排序)、head(截取)。如果某个路径含有空格,需要在 awk 里把整个字段引用起来,防止 sort 按空格错位。

如果你对这个需求换用纯 Python 写,大概得 10 行以上,而且还得自己写文件读取、字典计数、排序。Shell 四件套,一条命令完事。

6.5 再进一步:生成一个带表头的汇总报告

到了这一步,如果只是看结果,前面的命令已经够了。但假如你想把结果写进一个带表头的报告文件,可以这样组合:

{ echo "count path" awk '$9 == 200 { print $7 }' access.log | sort | uniq -c | sort -rn | head -5 } > report.txt

或者用 awk 的 BEGIN 块打印表头:

awk 'BEGIN { print "count path" } $9 == 200 { print $7 }' access.log | sort | uniq -c | sort -rn | head -5

注意的时候:第二种写法里,表头也会进入后面的 sort/uniq,会被 uniq 当成普通数据处理一次。所以这个做法只适用于"数据量不会把表头排到输出里没问题"的场景,严格来说还是第一种方法更稳妥。

6.6 多文件处理和文件名输出

很多日志分析场景会涉及多个日志文件。awk 可以一次读多个文件,还内置了FILENAME变量:

awk '{ print FILENAME, $7 }' access.log access2.log

如果想给每个文件分别统计,可以在 awk 里用FNR(当前文件的记录号)配合 FILENAME 判断文件切换:

awk 'FNR==1 { if (NR>1) print "---", prev_file, "done"; prev_file=FILENAME } { print FILENAME, $7 }' access.log access2.log

这一块对新手稍显复杂,实际用起来也不多。更常见的做法是先用通配符让 shell 展开文件名,再用 awk 处理:

awk '{ print $7 }' /var/log/nginx/access.log.*

Shell 的*.log.*会自动匹配多个文件,awk 依次处理,相当于把所有文件的对应字段合并输出,然后再进入后续 sort/uniq。这种组合在处理多文件统计时极其高效。

7. 进阶踩坑记录:我从生产环境里总结的教训

最后这块我写点这些年真实踩过的坑。这些细节比较琐碎,但每一条都让不少人在生产环境里抓狂过。

7.1 大文件处理时 shell 管道与内外层引号

写复杂的 awk / sed 命令时,引号嵌套是最容易出问题的点。我的原则是:外层用单引号,内层用双引号,尽量避免多层嵌套。如果必须在 awk 内部拼接 shell 变量,用-v传参:

key="api/users" awk -v k="$key" '$7 == k { print $7 }' access.log

绝对不要图省事把 shell 变量直接扔进 awk 脚本里,像awk '$7 == $key'这种写法,awk 会把$key当作 awk 自己的字段引用而不是 shell 变量。用-v是正确做法,既避免转义噩梦,又能防止注入问题(比如变量值包含空格、单引号时)。

7.2 管道与 while 循环:为什么管道里的变量改不了外部变量

有个经典面试题:在管道里用 while 循环累加一个外部变量,循环结束后变量还是 0。

count=0 cat file.txt | while read line; do count=$((count+1)) done echo $count # 输出 0

原因:管道会启动一个子 shell,while 循环在子 shell 里执行,count 的修改发生在子 shell 环境里,子 shell 退出后修改就丢失了。解决办法是用进程替换,让 while 循环在当前 shell 里执行:

count=0 while read line; do count=$((count+1)) done < <(cat file.txt) echo $count # 正确输出行数

在四件套的实战中,这种"用 while 循环逐行处理管道输出"的场景很常见,一定要记住这个子 shell 的坑。

7.3 sed -i 与 macOS 和 Linux 的差异

macOS 自带的 BSD sed 和 Linux 的 GNU sed 有一个明显差异:macOS 的-i必须带一个后缀参数,否则会报错。比如sed -i 's/foo/bar/g' file.txt在 Linux 上正常执行,换到 macOS 上就挂了。macOS 要写成:

sed -i '' 's/foo/bar/g' file.txt

这里-i ''表示不需要备份。这个差异简直是跨平台脚本头号坑。如果你的脚本需要考虑 macOS 兼容性,要么统一用sed -i.bak,要么改用其它不涉及 -i 的方式(比如输出到临时文件再 mv)。

7.4 awk 里千万别用sort命名变量

awk 里给变量取名字时,别用sortsplit这类和内置函数同名的词。有些版本会直接报错或者行为诡异。这不是理论问题,我确实见过有人写了sort=0,结果逻辑全程错乱。同理,sed 里的正则里如果有!,在双引号包裹的脚本里会被 shell 展开成历史替换,所以复杂命令尽量用单引号包起来。

7.5 大量小文件时避免逐个调用外部命令

如果你在 for 循环里面对 1000 个小文件执行 sort、awk,每调用一次命令就要 fork 一个进程,内存开销和启动时间都很可观。这种情况下,更高效的方式是先把文件内容拼接再一次性处理:

cat dir/*.txt | awk '{ ... }' | sort

这样整个处理链路只有几个外部命令进程,而不是成百上千个。这个优化在处理批量小日志、配置扫描等场景非常明显。

8. 面试与自查:这些高频考点你是否都能答上来

这些都是实际面试中经常被问到的点,也是我在带新人时反复提的问题。如果你的目标是笔试过关或者面试表现稳一点,把这几个问题自己过一遍,会很有帮助。

  1. cut -d' ' -f2 file.txtawk '{print $2}' file.txt对于连续多个空格分隔的文件,输出会有什么差异?为什么?

  2. 如何用 sed 删除文件中所有包含ERROR的行?

  3. 如何用 awk 统计文件中每行字段数?awk '{print NF}' file.txt

  4. sort 默认排序规则是什么?为什么sort -n对数字排序必不可少?

  5. sort file.txt | uniq -csort -u file.txt去重的效果什么时候不同?

  6. 如何用 awk 实现"取出 /etc/passwd 中 UID 大于 1000 的所有用户名"?

  7. sed 的正则里,圆括号()需要转义成\(\)才能作为分组捕获,为什么?

  8. awk 的NRFNR有什么区别?

这些问题如果每道都能不卡壳地回答出来,说明你对这四个命令已经建立了足够扎实的心智模型。回答不出来也没关系,倒回去把这篇文章对应的部分再刷一遍,重点看"为什么"而不是"是什么"。

我个人带新人时习惯让他们做一个小练习,比背十个参数列表有效得多:

# 从 access.log 中找出请求次数最多的前 3 个 IP # 要求只用一条管道命令完成

能独立写出来,说明 cut/sed/awk/sort 的配合逻辑已经通了。很多年后你会发现,这个练习背后蕴含的"字段提取 → 排序分组 → 计数排序 → 截取",是大多数文本处理需求的通用骨架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询