AWK & SED 生产力教程 / 第 13 章:脚本编写
第 13 章:脚本编写
好的脚本不只是能运行,更要可靠、可读、可维护。
13.1 SED/AWK 脚本文件
SED 脚本文件
# 创建 sed 脚本文件
cat > transform.sed << 'EOF'
#!/usr/bin/sed -f
# transform.sed — 配置文件转换脚本
# 用法: ./transform.sed config.txt
# 删除空行和注释
/^$/d
/^[[:space:]]*#/d
# 去掉行首尾空白
s/^[[:space:]]*//
s/[[:space:]]*$//
# 标准化等号两边的空格
s/[[:space:]]*=[[:space:]]*/=/
# 将 key=value 转为大写的 KEY
s/^\([^=]*\)=\(.*\)/\U\1\E=\2/
EOF
chmod +x transform.sed
$ ./transform.sed config.txt
AWK 脚本文件
# 创建 awk 脚本文件
cat > report.awk << 'EOF'
#!/usr/bin/awk -f
# report.awk — 生成销售报告
# 用法: ./report.awk sales.csv
BEGIN {
FS = ","
OFS = "\t"
printf "%-15s %-10s %12s %12s %12s\n", "产品", "类别", "销量", "单价", "总收入"
printf "%-15s %-10s %12s %12s %12s\n", "----", "----", "----", "----", "------"
}
NR > 1 {
revenue = $3 * $4
category_revenue[$2] += revenue
category_count[$2]++
total_revenue += revenue
printf "%-15s %-10s %12d %12.2f %12.2f\n", $1, $2, $3, $4, revenue
}
END {
printf "%-15s %-10s %12s %12s %12s\n", "----", "----", "----", "----", "------"
print "\n分类汇总:"
for (c in category_revenue) {
printf " %-15s 总收入: %12.2f 平均: %12.2f\n",
c, category_revenue[c], category_revenue[c]/category_count[c]
}
printf "\n总收入: %.2f\n", total_revenue
}
EOF
chmod +x report.awk
$ ./report.awk sales.csv
13.2 Shell 脚本中的 AWK/SED
基本集成模式
#!/bin/bash
# analyze.sh — 分析日志文件
set -euo pipefail
LOG_FILE="${1:?用法: $0 <日志文件>}"
if [[ ! -f "$LOG_FILE" ]]; then
echo "错误: 文件不存在: $LOG_FILE" >&2
exit 1
fi
# 将 awk 结果存储到变量
total_requests=$(awk 'END{print NR}' "$LOG_FILE")
error_count=$(awk '$9 >= 400 {count++} END{print count+0}' "$LOG_FILE")
unique_ips=$(awk '{print $1}' "$LOG_FILE" | sort -u | wc -l)
echo "=== 日志分析结果 ==="
echo "总请求数: $total_requests"
echo "错误请求数: $error_count"
echo "独立 IP 数: $unique_ips"
# 计算错误率
if (( total_requests > 0 )); then
error_rate=$(awk "BEGIN{printf \"%.2f\", $error_count/$total_requests*100}")
echo "错误率: ${error_rate}%"
fi
参数传递
#!/bin/bash
# 将 Shell 变量传递给 AWK
THRESHOLD=100
DATE=$(date +%Y-%m-%d)
awk -v threshold="$THRESHOLD" -v date="$DATE" '
{
if ($3 > threshold) {
printf "[%s] 高值警告: %s = %d (阈值: %d)\n", date, $1, $3, threshold
}
}' data.txt
管道中的错误处理
#!/bin/bash
# safe_pipeline.sh — 带错误处理的管道
set -o pipefail
INPUT_FILE="${1:?用法: $0 <输入文件>}"
# 使用临时文件
TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"' EXIT
# 步骤 1:预处理
if ! sed 's/^[[:space:]]*//; s/[[:space:]]*$//' "$INPUT_FILE" > "$TEMP_FILE"; then
echo "错误: 预处理失败" >&2
exit 1
fi
# 步骤 2:过滤和统计
result=$(awk '!/^#/ && NF > 0 {count++} END{print count+0}' "$TEMP_FILE")
echo "有效行数: $result"
13.3 错误处理
AWK 中的错误处理
# 检查文件是否存在
awk '
BEGIN {
if (ARGV[1] == "") {
print "用法: awk -f script.awk <文件>" > "/dev/stderr"
exit 1
}
}
{
# 正常处理
print
}' "$input_file"
# 检查字段数量
awk '{
if (NF < 3) {
printf "警告: 第 %d 行字段不足: %s\n", NR, $0 > "/dev/stderr"
next
}
# 处理数据
print $1, $2, $3
}' data.txt
# 安全的除法(避免除以零)
awk '{
if ($2 > 0) {
result = $1 / $2
} else {
printf "警告: 第 %d 行除数为零\n", NR > "/dev/stderr"
result = 0
}
print $1, $2, result
}' data.txt
SED 中的错误预防
# 备份原文件
sed -i.bak 's/old/new/g' file
# 先测试再修改
if sed 's/old/new/g' file | diff -q file - > /dev/null 2>&1; then
echo "文件未改变"
else
sed -i 's/old/new/g' file
echo "文件已更新"
fi
# 限制修改范围
sed -i.bak '1,10s/old/new/g' file # 只修改前 10 行
13.4 调试技巧
AWK 调试
# 打印变量值
awk '{
print "DEBUG: NR=" NR, "NF=" NF, "$1=" $1 > "/dev/stderr"
# 正常处理
}' file
# 使用 END 块检查最终状态
awk '{
data[NR] = $0
count[$1]++
}
END {
# 调试信息
print "DEBUG: 总行数=" NR > "/dev/stderr"
print "DEBUG: 唯一键数=" length(count) > "/dev/stderr"
# 正常输出
for (i=1; i<=NR; i++) print data[i]
}' file
# GNU AWK 的调试模式
gawk --debug 'BEGIN{print "start"} {print} END{print "end"}' file
SED 调试
# 使用 l 命令显示不可见字符
sed -n '/pattern/{l;p}' file
# 分步测试
sed 's/step1/replacement1/' file > /tmp/step1.txt
sed 's/step2/replacement2/' /tmp/step1.txt > /tmp/step2.txt
diff file /tmp/step2.txt
# GNU sed 调试模式(4.2.2+)
sed --debug 's/old/new/g' file
通用调试技巧
#!/bin/bash
# debug_mode.sh — 支持调试模式的脚本
DEBUG=${DEBUG:-false}
debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# 使用示例
debug "开始处理文件: $1"
debug "阈值设置为: $THRESHOLD"
awk -v debug="$DEBUG" '
function debug_print(msg) {
if (debug == "true") print "[AWK-DEBUG] " msg > "/dev/stderr"
}
{
debug_print("处理第 " NR " 行: " $0)
# 处理逻辑
}' "$1"
# 运行时启用调试
$ DEBUG=true ./script.sh data.txt
13.5 代码组织
模块化 AWK 函数库
# 创建 AWK 函数库
cat > lib/utils.awk << 'EOF'
# utils.awk — 通用工具函数库
# 去除首尾空白
function trim(s) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
return s
}
# 格式化字节大小
function human_size(bytes, units, i) {
split("B KB MB GB TB", units, " ")
i = 1
while (bytes >= 1024 && i < 5) {
bytes /= 1024
i++
}
return sprintf("%.2f %s", bytes, units[i])
}
# 百分比格式化
function pct(value, total) {
if (total == 0) return "0.00%"
return sprintf("%.2f%%", value / total * 100)
}
# 重复字符
function repeat_char(ch, n, s, i) {
s = ""
for (i = 0; i < n; i++) s = s ch
return s
}
# 进度条
function progress_bar(value, max, width, filled, empty) {
filled = int(value / max * width)
empty = width - filled
return "[" repeat_char("█", filled) repeat_char("░", empty) "]"
}
EOF
# 在主脚本中引用
cat > main.awk << 'EOF'
#!/usr/bin/awk -f
@include "lib/utils.awk"
{
printf "%-20s %s %6.1f%%\n", $1, progress_bar($2, 100, 20), $2
}
EOF
Shell 函数封装
#!/bin/bash
# lib.sh — 通用函数库
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 错误: $*" >&2
}
die() {
error "$@"
exit 1
}
# 安全的 AWK 执行
safe_awk() {
local script="$1"
local input="$2"
if [[ ! -f "$input" ]]; then
die "文件不存在: $input"
fi
awk "$script" "$input"
}
# 统计文件行数(排除空行和注释)
count_active_lines() {
local file="$1"
awk '!/^#/ && !/^$/ && NF > 0 {count++} END{print count+0}' "$file"
}
# 提取配置值
get_config_value() {
local file="$1"
local key="$2"
awk -F= -v key="$key" '$1 == key {print $2; exit}' "$file"
}
13.6 代码风格
命名规范
# AWK 变量命名
awk '{
# 好的命名
field_count = NF
line_number = NR
total_amount = 0
# 不好的命名
n = NF # 不清楚含义
x = NR # 不清楚含义
t = 0 # 不清楚含义
}' file
# 函数命名
awk '
function calculate_average(sum, count) { # 动词+名词
return (count > 0) ? sum / count : 0
}
function format_currency(amount) { # 动词+名词
return sprintf("$%.2f", amount)
}
' file
代码组织最佳实践
# 1. 注释清晰
awk '
# 处理配置文件
# 输入: key=value 格式
# 输出: 标准化的配置
BEGIN {
FS = "="
# 初始化计数器
count = 0
}
# 跳过注释和空行
/^#/ || /^$/ { next }
# 处理有效配置行
{
key = trim($1)
value = trim($2)
# 存储配置
config[key] = value
count++
}
END {
# 输出统计
printf "处理了 %d 个配置项\n", count
}
' config.txt
# 2. 避免魔法数字
awk '{
# 不好
if ($3 > 100) print
# 好
THRESHOLD = 100
if ($3 > THRESHOLD) print
}' file
# 3. 使用 BEGIN/END 初始化和清理
awk '
BEGIN {
# 初始化
total = 0
count = 0
FS = ","
}
{
# 处理
total += $3
count++
}
END {
# 汇总
printf "平均值: %.2f\n", total / count
}' data.csv
13.7 测试
单元测试框架
#!/bin/bash
# test_awk.sh — AWK 脚本测试
run_test() {
local test_name="$1"
local input="$2"
local expected="$3"
local script="$4"
actual=$(echo "$input" | awk "$script")
if [[ "$actual" == "$expected" ]]; then
echo "✅ 通过: $test_name"
else
echo "❌ 失败: $test_name"
echo " 期望: $expected"
echo " 实际: $actual"
fi
}
# 测试用例
run_test "trim 函数" \
" hello " \
"hello" \
'{gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print}'
run_test "数值计算" \
"10 20 30" \
"20.00" \
'{sum=0; for(i=1;i<=NF;i++) sum+=$i; printf "%.2f", sum/NF}'
run_test "条件过滤" \
$'a\nb\nc' \
"b" \
'/b/ {print}'
测试数据生成
# 生成测试数据
generate_test_data() {
local n=${1:-100}
awk -v n="$n" 'BEGIN {
srand()
for (i = 1; i <= n; i++) {
printf "user_%03d dept_%02d %d\n", i, int(rand()*5)+1, int(rand()*10000)+3000
}
}'
}
# 使用测试数据
$ generate_test_data 1000 | awk '{count[$2]++} END {for (d in count) print d, count[d]}'
13.8 实战:完整脚本示例
#!/bin/bash
# log_analyzer.sh — 日志分析脚本
# 用法: log_analyzer.sh [选项] <日志文件>
set -euo pipefail
# 默认配置
THRESHOLD=100
TOP_N=10
OUTPUT_FORMAT="text"
# 函数定义
usage() {
cat << EOF
用法: $0 [选项] <日志文件>
选项:
-t NUM 错误阈值 (默认: $THRESHOLD)
-n NUM 显示前 N 条 (默认: $TOP_N)
-f FMT 输出格式: text, csv, html (默认: $OUTPUT_FORMAT)
-h 显示帮助
示例:
$0 -t 50 -n 20 -f html access.log
EOF
exit 0
}
log() { echo "[$(date '+%H:%M:%S')] $*" >&2; }
error() { echo "[错误] $*" >&2; exit 1; }
# 解析参数
while getopts "t:n:f:h" opt; do
case $opt in
t) THRESHOLD="$OPTARG" ;;
n) TOP_N="$OPTARG" ;;
f) OUTPUT_FORMAT="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
LOG_FILE="${1:?请指定日志文件}"
# 参数验证
[[ -f "$LOG_FILE" ]] || error "文件不存在: $LOG_FILE"
[[ "$THRESHOLD" =~ ^[0-9]+$ ]] || error "阈值必须是数字"
[[ "$TOP_N" =~ ^[0-9]+$ ]] || error "TOP_N 必须是数字"
log "开始分析: $LOG_FILE"
# 主要分析逻辑
awk -v threshold="$THRESHOLD" -v top_n="$TOP_N" -v format="$OUTPUT_FORMAT" '
BEGIN {
total = 0
errors = 0
}
{
total++
ip_count[$1]++
status_count[$9]++
path_count[$7]++
bytes += $10
if ($9 >= 400) errors++
}
END {
if (format == "text") {
printf "总请求数: %d\n", total
printf "错误请求数: %d (%.2f%%)\n", errors, errors/total*100
printf "总传输量: %.2f MB\n\n", bytes/1048576
printf "Top %d IP:\n", top_n
for (ip in ip_count)
printf "%8d %s\n", ip_count[ip], ip
}
}' "$LOG_FILE" | ( [[ "$OUTPUT_FORMAT" == "text" ]] && cat || sort -rn | head -"$TOP_N" )
log "分析完成"
扩展阅读
下一章:第 14 章:性能优化 — 大文件处理、并行处理、内存管理。