Shell 文本三剑客(grep/awk/sed)高频用法?

2026年 阅读约 8 分钟 面试指南 · Linux/Docker面试

Linux面试题:Shell脚本与文本三剑客,grep/awk/sed高频用法、日志统计top IP、批量替换、find与xargs组合、crontab,附手写题实战。

一句话总结

grep 找行、awk 切列、sed 改内容——三剑客分工。面试手写题集中在:日志统计(Top IP/状态码分布)、批量替换、字段提取。必背管道组合:awk '{print $1}' access.log | sort | uniq -c | sort -rn | head。Shell 基础则抓住:变量、条件、循环、退出码、set -e。

初级理解

grep:按模式过滤行

grep -r 'ERROR' /var/log/app/ # 递归搜目录 grep -c 'ERROR' app.log # 统计匹配行数 grep -i -n 'timeout' app.log # 忽略大小写 + 显示行号 grep -A 3 -B 1 'Exception' app.log # 显示后 3 行前 1 行(看上下文) grep -v 'DEBUG' app.log # 反向:排除 DEBUG 行 grep -E 'ERROR|WARN' app.log # 扩展正则,多模式

awk:按列处理(默认空格分列,$1 第 1 列,$NF 最后 1 列)

awk '{print $1, $7}' access.log # 取第 1、7 列 awk -F: '{print $1}' /etc/passwd # 指定冒号分隔 awk '$9 == 500 {print $1}' access.log # 条件过滤:状态码 500 的 IP awk 'END{print NR}' access.log # 总行数(比 wc -l 更灵活)

sed:流编辑(查找替换是主角)

sed 's/foo/bar/' file.txt # 每行第一个 foo → bar sed 's/foo/bar/g' file.txt # 全局替换 sed -i 's/8080/9090/g' app.conf # 直接改文件(改前建议备份) sed -n '100,200p' file.txt # 打印 100~200 行(超大文件翻页看)

中级深入

日志统计三连(手写题 TOP1,逐词都能解释):

# 访问量 Top10 的 IP awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10 # ↑ 排序 ↑ 相邻去重计数 ↑ 按次数倒序 # 原理链:awk 抽列 → sort 把相同值排到一起 → uniq -c 才能计数 → sort -rn 数字倒序 # 状态码分布 awk '{print $9}' access.log | sort | uniq -c | sort -rn # 每分钟请求量(取时间字段前 17 位聚合) awk '{print substr($4,2,17)}' access.log | sort | uniq -c

find + xargs 组合(批量操作):

find /data/logs -name '*.log' -mtime +30 -delete # 删 30 天前日志 find . -name '*.tmp' | xargs rm -f # xargs 转成参数 find . -name '*.sh' -print0 | xargs -0 grep -l 'TODO' # 文件名带空格用 -print0/-0 find . -name '*.java' | xargs grep -l 'OldApi' # 找引用旧 API 的文件

Shell 脚本骨架(写"规范脚本"的印象分):

#!/bin/bash set -euo pipefail # 出错即退 / 未定义变量报错 / 管道任一环失败即失败 LOG_DIR=${1:-/var/log/app} # 参数默认值 if [[ ! -d "$LOG_DIR" ]]; then echo "目录不存在: $LOG_DIR" >&2 # 错误信息走 stderr exit 1 fi for f in "$LOG_DIR"/*.log; do echo "处理: $f" done

高级拓展

awk 进程级应用(能写小程序):

# 统计接口耗时 P95(假设第 N 列是耗时 ms) awk '{a[NR]=$NF} END{asort(a); print a[int(NR*0.95)]}' access.log # 分组求和:按 URL 统计总耗时 Top5 awk '{sum[$7]+=$NF; cnt[$7]++} END{for(u in sum) print sum[u], cnt[u], u}' access.log \ | sort -rn | head -5 # 两个文件按 key 关联(类似 join) awk 'NR==FNR{m[$1]=$2; next} {print $0, m[$1]}' map.txt data.txt

crontab 要点:五段 分 时 日 月 周;*/5 * * * * cmd 每 5 分钟。三个必答坑:① 环境变量与登录 shell 不同——用绝对路径或脚本里 source profile;② 输出没重定向会发邮件——统一 >> /var/log/xx.log 2>&1;③ 脚本要加锁防重入(flock -n /tmp/x.lock)。

性能与安全意识:大文件避免 shell 循环逐行处理(awk 单遍快一个数量级);变量永远加引号(防空格拆词);不要 eval 拼接用户输入;敏感信息别 echo 到日志。

面试加分项:主动对比替代工具——统计用 goaccess、JSON 日志用 jq、复杂转换写 Python 更可维护。"知道什么时候不用三剑客"比"三剑客滚瓜烂熟"更显工程判断。

实战场景

场景一:线上 5 分钟内统计慢请求(面试模拟真实压力)

# 找出最近 5 分钟耗时超过 1s 的请求及分布 tail -n 100000 access.log \ | awk '$NF > 1000 {print $7, $NF}' \ | sort | uniq -c | sort -rn | head -20 # 先 tail 限定范围再 awk(全量 awk 慢),输出"接口 出现次数 耗时"

场景二:批量替换配置并备份

# 替换所有 conf 里的旧域名,逐个备份 .bak find /etc/app -name '*.conf' \ | xargs -I {} sh -c 'cp {} {}.bak && sed -i "s/old.example.com/new.example.com/g" {}' # -I {} 占位逐个执行;sh -c 让 cp 和 sed 成组

场景三:服务健康检查脚本(常用题)

#!/bin/bash set -euo pipefail URL=http://localhost:8080/health CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$URL") if [[ "$CODE" == "200" ]]; then echo "OK" else echo "UNHEALTHY: $CODE" >&2 exit 1 fi # 配合 flock 防重入 + crontab 每分钟执行 + 连续失败才告警(状态文件)

面试模拟

Q:为什么统计 Top IP 要先 sort 再 uniq -c?

A:uniq 只对相邻的重复行去重计数——不排序的话同一个 IP 散落在各处会被算多次。所以标准链是 awk 抽列 → sort 归组 → uniq -c 计数 → sort -rn 按次数倒序。数据量极大时可以只用 awk 哈希一步完成:awk '{c[$1]++} END{for(k in c) print c[k],k}',省掉排序进程,快且少一次磁盘 IO(但输出无序,仍要 sort)。

Q:set -e 不够吗,为什么要 set -euo pipefail?

A:set -e 有著名盲区:① 命令在 if 条件或 && 链里失败不触发退出;② 管道只看最后一个命令的退出码——grep 无匹配 | wc -l 时 wc 成功,grep 的失败被吞掉。pipefail 让整条管道任一环失败即失败;-u 把未定义变量当错误(防 typo 造成空值灾难);-e 在非预期失败时立即退出。三个一起才构成"出错快停"的脚本基线。

Q:日志文件 10 个 G,怎么只看第 500 万行附近?

A:不能用 head|tail 全量过两遍(虽可行但慢)。首选 sed -n '4999990,5000010p' file(sed 流式处理,到目标区间即止但仍需扫描前段);更快的方案是先 grep -n 定位关键字的行号再局部 sed;如果是结构化定位,用 awk 'NR>=4999990 && NR<=5000010'。考察点:知道行号定位在大文件上没有索引捷径,能按"扫描范围最小化"组织命令。