强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Unix 设计哲学教程 / 第 5 章:经典工具设计

第 5 章:经典工具设计

“You don’t need to know every tool, but you need to master the ones that matter.”

Unix 提供了数百个命令行工具,每个都遵循"做一件事,做好它"的原则。本章深入讲解最核心的文本处理工具:catgrepsedawk,以及 sortuniqcuttr 等辅助工具。


5.1 工具设计的共同原则

接口一致性

设计准则说明示例
从 stdin 读取支持管道输入grep "pattern" 等待 stdin
写入 stdout输出可以继续管道cat file | sort
出错写 stderr错误不污染正常数据流grep "x" bad 2>/dev/null
退出码表示结果0=成功,非0=失败grep -q "x" file; echo $?
- 表示 stdin显式指定从标准输入读cat - file.txt
支持多文件一次处理多个文件grep "x" *.log

5.2 cat —— 连接与输出

核心功能

cat(concatenate)是最简单的 Unix 工具之一:读取文件并输出到 stdout。

# 基本用法
cat file.txt               # 输出文件内容
cat file1.txt file2.txt    # 连接多个文件
cat -                      # 从 stdin 读取

# 常用选项
cat -n file.txt            # 显示行号
cat -b file.txt            # 只对非空行编号
cat -s file.txt            # 压缩连续空行为一行
cat -A file.txt            # 显示不可见字符($ 表示行尾,^I 表示 Tab)
cat -T file.txt            # 将 Tab 显示为 ^I
cat -v file.txt            # 显示非打印字符

cat 的设计哲学

# cat 的设计体现了 Unix 哲学:
# 1. 功能简单 —— 只做连接和输出
# 2. 不加修饰 —— 原样输出,不添加格式
# 3. 可组合 —— 输出可以成为任何程序的输入

# 常见的"无用 use of cat"(UUOC)
# ❌ 多余的 cat
cat file.txt | grep "pattern"
# ✅ 直接给 grep 文件名
grep "pattern" file.txt

# 但是,cat 仍然是有用的:
# 1. 连接多个文件
cat file1.txt file2.txt | sort

# 2. 管道起点,提高可读性
cat /var/log/*.log | grep "error" | sort | uniq -c
# 比这样更清晰:
grep "error" /var/log/*.log | sort | uniq -c

# 3. 交互式输入
cat << 'EOF' > config.txt
key1=value1
key2=value2
EOF

5.3 grep —— 模式搜索

核心功能

grep(Global Regular Expression Print)是最常用的 Unix 工具,在输入中搜索匹配指定模式的行。

# 基本用法
grep "pattern" file.txt            # 搜索模式
grep -i "pattern" file.txt         # 忽略大小写
grep -v "pattern" file.txt         # 反向匹配(显示不匹配的行)
grep -n "pattern" file.txt         # 显示行号
grep -c "pattern" file.txt         # 只显示匹配行数
grep -l "pattern" *.txt            # 只显示包含匹配的文件名
grep -r "pattern" /path/           # 递归搜索目录
grep -w "word" file.txt            # 全词匹配
grep -m 5 "pattern" file.txt       # 最多匹配 5 行

正则表达式

# 基本正则表达式(BRE)—— 默认
grep "^start" file.txt             # 以 start 开头
grep "end$" file.txt               # 以 end 结尾
grep "^$" file.txt                 # 空行
grep "a.b" file.txt                # . 匹配任意单个字符
grep "ab*c" file.txt               # * 零个或多个 b
grep "[0-9]" file.txt              # 数字
grep "[^a-z]" file.txt             # 非小写字母

# 扩展正则表达式(ERE)—— 使用 -E 或 egrep
grep -E "ab+c" file.txt            # + 一个或多个 b
grep -E "ab?c" file.txt            # ? 零个或一个 b
grep -E "cat|dog" file.txt         # | 或运算
grep -E "(ab)+" file.txt           # () 分组

# Perl 正则表达式(PCRE)—— 使用 -P(仅 GNU grep)
grep -P "\d+" file.txt             # 数字
grep -P "\b\w+\b" file.txt        # 单词
grep -P "(?<=key=)\w+" file.txt   # 前瞻断言
grep -P "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" file.txt  # IP 地址

grep 的实用模式

# 1. 查找配置文件中的非注释行
grep -v "^#" /etc/ssh/sshd_config | grep -v "^$"

# 2. 递归搜索代码库,排除特定目录
grep -rn "TODO" --include="*.py" --exclude-dir=".git" .

# 3. 显示匹配行的上下文
grep -B 3 -A 3 "error" logfile.txt    # 前3行 + 后3行
grep -C 5 "error" logfile.txt          # 前后各5行

# 4. 使用文件中的模式列表
grep -f patterns.txt input.txt

# 5. 统计每个文件的匹配数
grep -rc "import" --include="*.py" . | sort -t: -k2 -rn

# 6. 仅匹配匹配的部分(非整行)
grep -o "ID=[0-9]*" data.txt
grep -oP '(?<=email=)[^\s]+' users.txt

# 7. 使用 grep 进行 AND 逻辑
grep "pattern1" file.txt | grep "pattern2"

# 8. 使用 grep 进行 OR 逻辑
grep -E "pattern1|pattern2" file.txt

5.4 sed —— 流编辑器

核心功能

sed(Stream Editor)是 Unix 的流编辑器,它逐行读取输入,对匹配的行执行编辑操作,然后输出结果。

# 替换
sed 's/old/new/' file.txt            # 替换每行第一个匹配
sed 's/old/new/g' file.txt           # 替换所有匹配(全局)
sed 's/old/new/gi' file.txt          # 忽略大小写的全局替换
sed -i 's/old/new/g' file.txt        # 原地编辑(直接修改文件)
sed -i.bak 's/old/new/g' file.txt    # 原地编辑并备份原文件

# 删除
sed '3d' file.txt                    # 删除第 3 行
sed '2,5d' file.txt                  # 删除第 2-5 行
sed '/pattern/d' file.txt            # 删除匹配行
sed '/^$/d' file.txt                 # 删除空行

# 打印
sed -n '5p' file.txt                 # 只打印第 5 行
sed -n '2,5p' file.txt              # 打印第 2-5 行
sed -n '/pattern/p' file.txt        # 打印匹配行(同 grep)

# 插入和追加
sed '3i\新插入的行' file.txt         # 在第 3 行前插入
sed '3a\新追加的行' file.txt         # 在第 3 行后追加
sed '/pattern/i\新插入的行' file.txt # 在匹配行前插入

sed 的地址与范围

# 地址类型
sed '5s/old/new/g' file.txt          # 只在第 5 行替换
sed '2,5s/old/new/g' file.txt        # 只在第 2-5 行替换
sed '2,$s/old/new/g' file.txt        # 从第 2 行到末尾
sed '/start/,/end/s/old/new/g' file.txt  # 在 start 和 end 之间的行

# 多个命令
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt
# 或
sed 's/foo/bar/g; s/baz/qux/g' file.txt

# 使用不同的分隔符(处理路径时很有用)
sed 's|/usr/local|/opt|g' file.txt
sed 's#http://#https://#g' file.txt

sed 的高级用法

# 1. 使用捕获组(反向引用)
sed 's/\(.*\)=\(.*\)/\2=\1/' file.txt   # 交换等号两边
sed -E 's/([0-9]+)-([0-9]+)/\2-\1/g' file.txt  # ERE 语法

# 2. 多行处理
# N 命令:将下一行追加到模式空间
sed '/^START/{N;s/\n/ /;}' file.txt

# 3. 条件执行
sed '/DEBUG/{s/DEBUG/INFO/; s/old/new/;}' file.txt

# 4. 打印行号(类似 grep -n)
sed -n '/pattern/=' file.txt

# 5. 在每行末尾添加内容
sed 's/$/ # suffix/' file.txt

# 6. 在每行开头添加内容
sed 's/^/# prefix /' file.txt

# 7. 删除文件开头和结尾的空行
sed '/./,$!d' file.txt        # 删除开头的空行
sed -e :a -e '/^\n*$/{$d;N;ba;}' file.txt  # 删除结尾的空行

# 8. 使用 sed 进行 HTML 转义
sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g;' file.html

5.5 awk —— 文本处理语言

核心功能

awk 是 Unix 中最强大的文本处理工具,它实际上是一种完整的编程语言。

# 基本语法
awk 'pattern { action }' file.txt

# 默认行为:按空白分隔,打印整行
awk '{ print }' file.txt          # 同 cat
awk '{ print $1 }' file.txt       # 打印第一列
awk '{ print $1, $3 }' file.txt   # 打印第一和第三列
awk '{ print NR, $0 }' file.txt   # 打印行号和整行
awk '{ print NF }' file.txt       # 打印每行的字段数

# 自定义分隔符
awk -F: '{ print $1 }' /etc/passwd        # 以 : 分隔
awk -F',' '{ print $2 }' data.csv         # CSV 文件
awk -F'\t' '{ print $1 }' data.tsv        # TSV 文件

# 条件过滤
awk '$3 > 100 { print $1, $3 }' data.txt  # 第3列大于100
awk '/error/ { print }' logfile.txt        # 匹配行
awk 'NR >= 5 && NR <= 10' file.txt        # 第5-10行

awk 的编程特性

# 1. 变量和计算
awk '{ sum += $3; count++ } END { print "平均:", sum/count }' data.txt

# 2. BEGIN 和 END 块
awk 'BEGIN { print "=== 开始 ===" }
     { print $0 }
     END { print "=== 结束,共", NR, "行 ===" }' file.txt

# 3. 关联数组(哈希表)
awk '{ count[$1]++ } END { for (k in count) print k, count[k] }' access.log

# 4. 字符串函数
awk '{ print length($0) }' file.txt       # 行长度
awk '{ print toupper($0) }' file.txt      # 转大写
awk '{ print substr($0, 1, 10) }' file.txt # 截取前10字符
awk '{ gsub(/old/, "new"); print }' file.txt # 全局替换

# 5. 数学函数
awk '{ print sqrt($1) }' numbers.txt      # 平方根
awk '{ print int($1) }' numbers.txt       # 取整
awk 'BEGIN { srand() } { print rand() }' file.txt  # 随机数

# 6. 格式化输出
awk '{ printf "%-20s %10d %8.2f\n", $1, $2, $3 }' data.txt

# 7. 多文件处理
awk 'FNR == 1 { print "=== " FILENAME " ===" } { print }' *.txt

awk 实战案例

# 1. CSV 转 Markdown 表格
awk -F, 'BEGIN { print "| Col1 | Col2 | Col3 |"; print "|------|------|------|" }
         { printf "| %s | %s | %s |\n", $1, $2, $3 }' data.csv

# 2. 统计 Web 日志中的状态码分布
awk '{ status[$9]++ } END { for (s in status) print status[s], s }' access.log | sort -rn

# 3. 计算列的统计信息(最小、最大、平均、总和)
awk '{
    sum += $1; count++
    if (NR == 1 || $1 < min) min = $1
    if (NR == 1 || $1 > max) max = $1
} END {
    printf "Count: %d\nMin: %.2f\nMax: %.2f\nAvg: %.2f\nSum: %.2f\n",
           count, min, max, sum/count, sum
}' numbers.txt

# 4. 转置矩阵
awk '{
    for (i = 1; i <= NF; i++) {
        a[NR][i] = $i
        if (NR == 1) ncols = i
    }
    nrows = NR
} END {
    for (i = 1; i <= ncols; i++) {
        for (j = 1; j <= nrows; j++) {
            printf "%s%s", a[j][i], (j < nrows ? " " : "\n")
        }
    }
}' matrix.txt

# 5. 合并连续行(将多行合并为一行)
awk '/^  / { printf " %s", $0; next } { print; }' file.txt

# 6. 提取 IP 并统计 Top 10
awk '{ ip[$1]++ } END { for (i in ip) print ip[i], i }' access.log | sort -rn | head -10

5.6 辅助工具

sort —— 排序

# 基本排序
sort file.txt                    # 按字典序
sort -n file.txt                 # 按数值
sort -r file.txt                 # 逆序
sort -u file.txt                 # 排序并去重
sort -f file.txt                 # 忽略大小写

# 按指定列排序
sort -k2 file.txt                # 按第 2 列排序
sort -k2 -n file.txt             # 按第 2 列数值排序
sort -t: -k3 -n /etc/passwd      # 以 : 分隔,按第 3 列数值排序
sort -k1,1 -k2,2rn file.txt     # 先按第1列排序,再按第2列数值逆序

# 大文件排序(使用临时目录)
sort -T /tmp/sort_tmp -S 1G bigfile.txt

# 人类可读的大小排序
du -sh * | sort -rh               # 按文件大小降序

uniq —— 去重与统计

# 注意:uniq 只去除连续的重复行,通常需要先 sort
sort file.txt | uniq              # 去重
sort file.txt | uniq -c           # 统计每行出现次数
sort file.txt | uniq -d           # 只显示重复行
sort file.txt | uniq -u           # 只显示唯一行
sort file.txt | uniq -ci          # 忽略大小写的统计

# 经典组合:统计词频
cat file.txt | tr -s ' ' '\n' | sort | uniq -c | sort -rn | head -20

cut —— 按列提取

# 按字符位置提取
cut -c1-10 file.txt              # 提取每行前10个字符
cut -c5- file.txt                # 从第5个字符到行尾

# 按分隔符提取
cut -d: -f1 /etc/passwd          # 以 : 分隔,提取第1列
cut -d: -f1,3 /etc/passwd        # 提取第1和第3列
cut -d, -f2-4 data.csv           # 提取第2-4列

# 与 sort/uniq 组合
cut -d: -f1 /etc/passwd | sort | uniq -c | sort -rn

tr —— 字符转换

# 字符替换
tr 'a-z' 'A-Z' < file.txt       # 小写转大写
tr 'A-Z' 'a-z' < file.txt       # 大写转小写

# 删除字符
tr -d '0-9' < file.txt           # 删除所有数字
tr -d '\r' < windows.txt         # 删除 Windows 回车符

# 压缩重复字符
tr -s ' ' < file.txt             # 压缩连续空格为单个空格
tr -s '\n' < file.txt            # 压缩连续空行为单个空行

# 补集操作
tr -cd '0-9\n' < file.txt        # 只保留数字和换行
tr -cd '[:print:]\n' < file.txt  # 只保留可打印字符

wc —— 统计

wc file.txt                      # 行数 词数 字节数
wc -l file.txt                   # 只显示行数
wc -w file.txt                   # 只显示词数
wc -c file.txt                   # 只显示字节数
wc -m file.txt                   # 字符数(多字节字符)

# 统计当前目录下的文件数
ls -1 | wc -l

# 统计代码行数
find . -name "*.py" -print0 | xargs -0 wc -l | tail -1

head 和 tail —— 截取首尾

head -20 file.txt                # 前20行
head -c 1024 file.txt            # 前1024字节
tail -20 file.txt                # 后20行
tail -f file.txt                 # 实时跟踪文件末尾
tail -f file.txt | grep "error"  # 实时过滤
tail -F file.txt                 # 跟踪(即使文件被轮转)

diff 和 patch —— 比较与补丁

diff file1.txt file2.txt                 # 比较两个文件
diff -u file1.txt file2.txt > changes.patch  # 生成统一格式补丁
patch < changes.patch                     # 应用补丁
diff -r dir1/ dir2/                       # 递归比较目录
diff --side-by-side file1.txt file2.txt   # 并排显示

5.7 工具组合模式

常见组合模式

# 模式 1: 过滤 → 转换 → 统计
grep "ERROR" log.txt | awk '{print $4}' | sort | uniq -c | sort -rn

# 模式 2: 提取 → 排序 → 去重
cut -d, -f2 data.csv | sort -u

# 模式 3: 搜索 → 上下文
grep -n "function" code.py | head -1 | cut -d: -f1 | xargs -I{} sed -n '{},+10p' code.py

# 模式 4: 批量处理
find . -name "*.txt" -print0 | xargs -0 grep "pattern"

# 模式 5: 数据转换
cat data.csv | tr ',' '\t' | sort -k2 | column -t

# 模式 6: 日志聚合
cat /var/log/*.log | grep "$(date +%b\ %d)" | sort -k3 | uniq

工具选择指南

需求推荐工具替代方案
搜索文本grepawk '/pattern/'
简单替换sedawk '{gsub(); print}'
按列提取awk '{print $N}'cut -d: -fN
排序sort
去重sort | uniqsort -u
行数统计wc -lawk 'END{print NR}'
字符转换trsed / awk
复杂数据处理awkpython / perl
文件查找findlocate

注意事项

  1. grep 的 BRE vs ERE vs PCRE:默认 grep 使用 BRE(基本正则表达式),grep -E 使用 ERE(扩展正则表达式),grep -P 使用 PCRE(Perl 正则)。\+\?\| 在 BRE 中需要反斜杠,在 ERE 中不需要。
  2. sed 的 GNU vs BSD 差异:GNU sed(Linux)和 BSD sed(macOS)在 -i 选项上有差异。GNU sed -i 's/x/y/',BSD sed -i '' 's/x/y/'
  3. awk 的 NR vs FNRNR 是全局行号,FNR 是当前文件的行号。处理多文件时需注意区别。
  4. 管道中的 locale 影响:排序顺序受 LC_COLLATE 影响。使用 LC_ALL=C sort 获得字节序排序,速度更快。
  5. 大文件处理:对于 GB 级别的文件,awk 比多次 grep + sed 效率更高,因为只遍历一次。

扩展阅读