awk 函数、printf 格式化与报表生成
定义用户函数并使用 printf,从原始数据流生成精美的表格报表。
awk 函数、printf 格式化与报表生成 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。
为什么函数和 printf 在 awk 中很重要
当您的 awk 程序从单行命令发展为多步骤管道时,有两项功能会变得不可或缺:用户定义函数和printf 格式化。
函数可以封装可复用的逻辑——例如百分比计算器、字符串去除空白函数或单位转换器——这样您只需编写一次,就能在同一程序的任意位置调用。printf 则可以精确控制输出外观:列宽、小数位数、填充方式和对齐方式。
二者结合起来,可以将原始日志行转换为精致、易读的报告,供工程师和管理人员据此采取行动。
- 函数可以减少重复,让程序能够单独进行测试。
printf会将数据对齐到固定宽度的列中,即使输入长度不同,报告仍然清晰易读。- 这两项功能都适用于 POSIX awk、gawk 和 mawk——无需任何扩展。
在 awk 中定义用户函数
用户函数使用 function 关键字声明,可以放在任意模式-动作规则之前或之后。语法如下:
function name(param1, param2, local1, local2) {
# body
return value
}一个关键的awk 惯用写法是:局部变量只需作为额外参数,并在它们前面添加额外空格即可。awk 没有 local 关键字——额外空格这一约定表示这些参数是局部变量,而不是由调用者传入的参数。
- 所有未声明为参数的变量默认都是全局变量。
- 函数可以递归调用自身。
return是可选的;没有它时,函数会返回空字符串。
#!/usr/bin/env bash
# Demonstrate a simple awk user function
echo '10 3
7 0
100 4' | awk '
function divide(a, b, result) {
if (b == 0) return "ERR"
result = a / b
return result
}
{
print $1 "/" $2 " = " divide($1, $2)
}
'使用字符串逻辑的函数
函数尤其适合处理重复的字符串操作。例如,去除开头和结尾的空白——原始日志数据在比较或生成报告前经常需要这样处理。
下面的示例使用 gsub 定义了 trim(s),并在每条记录中调用它,使主规则保持简洁易读。
gsub(regex, replacement, target)会原地修改 target,并返回替换次数。- 将正则表达式逻辑放入函数后,规则块就能专注于业务逻辑。
- 您可以在
BEGIN块中直接传入手工构造的字符串,进行单元测试。
#!/usr/bin/env bash
# trim() strips leading/trailing spaces; used to normalise CSV-like data
printf ' alice , 90 \n bob , 85 \n carol , 92 \n' | awk -F',' '
function trim(s) {
gsub(/^[ \t]+|[ \t]+$/, "", s)
return s
}
{
name = trim($1)
score = trim($2)
print name, score
}
'awk 中 printf 的简介
awk 中的 printf 遵循与 C 以及 Bash 内置 printf 相同的约定。它与 print 的关键区别在于,printf不会自动添加换行符——您必须自行添加 \n。
常见的格式说明符:
%s——字符串%d——整数%f——浮点数%e——科学计数法%g——取%f/%e中较短的表示形式
在 % 和说明符之间填写数字即可控制宽度和精度:%-20s 会在 20 个字符宽的字段中左对齐;%8.2f 会生成宽度为 8、小数点后 2 位的浮点数。
#!/usr/bin/env bash
# printf format specifiers in awk
echo '' | awk '
BEGIN {
printf "%s\n", "plain string"
printf "%10s\n", "right-pad"
printf "%-10s|\n", "left-pad"
printf "%8.2f\n", 3.14159
printf "%08d\n", 42
printf "%e\n", 123456.789
}
'使用 printf 构建报告标题
一份完善的报告需要标题行和分隔线。BEGIN 块是输出这些内容的理想位置——它会在处理任何输入之前运行一次。
诀窍是让标题中的宽度与数据行使用的宽度完全一致。在 BEGIN 中(或函数中)定义宽度常量,可以确保之后调整列宽时所有内容保持同步。
- 使用带有短横线分隔字符串的
printf绘制分隔线。 - 标题行末尾始终要加上
\n。 - 将
BEGIN标题、逐条记录的数据行和END页脚组合起来,即可形成完整的报告结构。
#!/usr/bin/env bash
# Print a report header in BEGIN, data rows per-record, total in END
printf 'alice 9200 12\nbob 8750 10\ncarol 10400 14\n' | awk '
BEGIN {
printf "%-10s %10s %6s %12s\n", "Name", "Salary", "Months", "Annual"
printf "%s\n", "----------------------------------------------"
total = 0
}
{
annual = $2 * $3
total += annual
printf "%-10s %10d %6d %12d\n", $1, $2, $3, annual
}
END {
printf "%s\n", "----------------------------------------------"
printf "%-10s %10s %6s %12d\n", "TOTAL", "", "", total
}
'使用用户函数处理格式化逻辑
构建多列报告时,通常需要在多个位置调用相同的格式化逻辑——例如,显示百分比进度条,或将字节转换为易读的单位。将这些逻辑放入函数可以避免重复,也便于统一修改格式。
下面的函数会将字节转换为 KB、MB 或 GB,并返回格式化字符串。调用它的规则只需传入 $NF(最后一个字段),然后打印返回的内容即可。
#!/usr/bin/env bash
# human_bytes() converts raw byte counts to KB/MB/GB strings
printf 'var.log 1024\naccess.log 2097152\ncore.dump 1073741824\ntiny.txt 512\n' | awk '
function human_bytes(n, s) {
if (n >= 1073741824) { s = sprintf("%.1f GB", n/1073741824) }
else if (n >= 1048576) { s = sprintf("%.1f MB", n/1048576) }
else if (n >= 1024) { s = sprintf("%.1f KB", n/1024) }
else { s = sprintf("%d B", n) }
return s
}
BEGIN { printf "%-20s %10s\n", "File", "Size"; print "------------------------------" }
{ printf "%-20s %10s\n", $1, human_bytes($2) }
'打印前在数组中累积数据
实际报告通常需要计算总数、平均值或排名——这些值必须在看完所有输入后才能计算。常见模式如下:
- 在逐条记录处理阶段,将数据累积到关联数组中。
- 在
END中计算派生值(平均值、百分比)。 - 仅从
END输出格式化结果,这样列宽就能根据完整数据集进行调整。
这种延迟输出模式是使用 awk 生成报告时最强大的惯用技巧之一。下面的示例从访问日志中按 HTTP 状态码统计请求次数和响应字节数。
#!/usr/bin/env bash
# Summarise an nginx-style access log by HTTP status code
printf '127.0.0.1 200 1234\n127.0.0.1 404 512\n10.0.0.2 200 8900\n10.0.0.3 500 256\n10.0.0.4 200 4096\n10.0.0.5 404 300\n' | awk '
{
status = $2
bytes = $3
count[status]++
total_bytes[status] += bytes
}
END {
printf "%-8s %8s %14s\n", "Status", "Requests", "Total Bytes"
print "-----------------------------------"
for (s in count)
printf "%-8s %8d %14d\n", s, count[s], total_bytes[s]
}
'awk 中的递归函数
awk 支持递归。递归的经典用途包括计算阶乘,或生成重复字符串(例如生成与终端宽度相匹配的破折号行)。递归在 awk 中不如在通用编程语言中常见,但它能够正确工作,并且所有主要实现都支持。
下面的示例定义了递归函数 repeat(s, n),并使用它绘制分隔线,使分隔线自动匹配最宽数据行的宽度——无需硬编码破折号数量。
- 对于大型输入,请避免深层递归——awk 没有尾调用优化。
- 对于简单的重复操作,使用
sprintf("%*s", n, "")配合gsub(/ /, "-")会更快,但示范性较弱。
#!/usr/bin/env bash
# Recursive repeat() to build separator lines dynamically
awk '
function repeat(s, n, acc) {
if (n <= 0) return ""
acc = s repeat(s, n-1)
return acc
}
BEGIN {
header = sprintf("%-15s %10s %10s", "Metric", "Current", "Target")
sep = repeat("-", length(header))
print sep
print header
print sep
printf "%-15s %10.1f %10.1f\n", "CPU %", 72.4, 60.0
printf "%-15s %10.1f %10.1f\n", "Mem GB", 14.2, 16.0
printf "%-15s %10d %10d\n", "Open FDs", 1024, 800
print sep
}
' /dev/null在 awk 中将 printf 输出到文件或流水线
awk 的 printf(以及 print)可以将输出重定向到文件,或通过管道传递给 shell 命令——这些操作都可以直接在 awk 程序内部完成,而不必将整个程序包装在子 shell 中。
printf "..." > "file.txt"— 写入文件(只截断一次,随后在同一次运行中追加)。printf "..." >> "file.txt"— 追加到文件。printf "..." | "sort -rn"— 通过管道传递给 shell 命令;awk 会在多条记录之间保持管道打开,并在程序结束时关闭它。
一种常见模式是:单次遍历日志,就按状态或主机拆分到多个输出文件中,从而避免反复扫描大型文件。
#!/usr/bin/env bash
# Route records into per-status files using print redirection
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
printf '200 /index.html\n404 /missing\n200 /api/data\n500 /crash\n404 /lost\n' | awk -v out="$tmpdir" '
{
file = out "/" $1 ".log"
printf "%-6s %s\n", $1, $2 > file
}
END {
close(file) # flush all open handles
}
'
echo "=== 200 ==="
cat "$tmpdir/200.log"
echo "=== 404 ==="
cat "$tmpdir/404.log"使用 awk 生成 CSV 报告
并非所有报告都是给人看的。有时输出必须是可供机器读取的 CSV,以便输入电子表格或下游流水线。awk 的 printf 可以很好地处理这一需求:将 OFS 设置为逗号,或者明确控制每个分隔符。
使用 awk 生成可靠 CSV 时,有两条重要规则:
- 可能包含逗号或换行符的字段必须用双引号括起来。
- 字段中的字面双引号要通过重复两个双引号进行转义:
He said ""hello""。
下面的函数 csv_field(s) 封装了这套引号处理逻辑,因此可以一致地应用于每个字符串字段。
#!/usr/bin/env bash
# Emit a valid CSV report from space-separated input
printf 'alice engineer 95000\nbob "sales,mgr" 82000\ncarol developer 110000\n' | awk '
function csv_field(s, safe) {
safe = s
gsub(/"/, "\"\"" , safe) # double any embedded quotes
return "\"" safe "\"" # wrap in quotes
}
BEGIN { print "Name,Role,Salary" }
{
printf "%s,%s,%d\n", csv_field($1), csv_field($2), $3
}
'端到端:完整的访问日志报告
这个最终示例将所有内容串联起来:用户函数、printf 格式化、数组累积,以及结构化的页眉和页脚。输入是简化的 Web 访问日志;输出是易于人类阅读的汇总表,其中包含按方法统计的总数和一行总计。
使用的关键技术:
count[method]++和bytes[method] += size在单次遍历中累积每种方法的统计数据。human_bytes()(来自前面的场景)会转换字节总数。- 在页眉、数据和页脚中使用宽度匹配的
printf,可确保无论输入规模如何,表格都能保持对齐。 END代码块使用for (k in arr)遍历数组,并通过连接到sort的管道输出排序结果。
#!/usr/bin/env bash
# Full access log report: method breakdown with human-readable bytes
printf 'GET /index 200 4096\nPOST /api 201 512\nGET /img 200 102400\nDELETE /item 204 0\nPOST /login 401 256\nGET /style 200 8192\n' | awk '
function human_bytes(n, s) {
if (n >= 1048576) s = sprintf("%.1f MB", n/1048576)
else if (n >= 1024) s = sprintf("%.1f KB", n/1024)
else s = sprintf("%d B", n)
return s
}
{
method = $1
size = $4
count[method]++
bytes[method] += size
grand_count++
grand_bytes += size
}
END {
fmt = "%-10s %8s %15s\n"
sep = "----------------------------------"
printf fmt, "Method", "Requests", "Bytes"
print sep
for (m in count)
printf "%-10s %8d %15s\n", m, count[m], human_bytes(bytes[m])
print sep
printf "%-10s %8d %15s\n", "TOTAL", grand_count, human_bytes(grand_bytes)
}
'知识检查:awk 函数中的局部变量
以下哪种写法按照 POSIX 惯例,正确地将 result 声明为 awk 函数内部的局部变量?
课程回顾:awk 函数、printf 与报告生成
在本课中,您学习了如何编写生产级 awk 程序,从原始数据流生成格式化报告。以下是需要掌握的要点:
- 用户函数使用
function name(params, locals)声明。在局部变量参数前添加额外空格是 POSIX 惯例——不存在local关键字。 - printf 可以精确控制输出:宽度(
%10s)、对齐方式(%-10s)和精度(%8.2f)。请记得显式添加\n。 - 使用 BEGIN 输出页眉和分隔符,使用逐条记录规则累积数据,再使用 END 计算总数并打印完整报告。
- 在 awk 内部,将
printf输出重定向到文件(> file)或流水线(| "sort"),以拆分报告或对输出进行后处理。 - 输出 CSV 时,使用 csv_field() 辅助函数包裹字符串字段,以安全处理其中嵌入的逗号和引号。
- 用于转换单位(
human_bytes)、重复字符(repeat)或清理字符串(trim、csv_field)的函数,值得保存在个人 awk 函数库中——它们可以在不同项目间顺畅组合使用。
常见问题解答
「awk 函数、printf 格式化与报表生成」课时是免费的吗?
是的 — 「awk 函数、printf 格式化与报表生成」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。
「awk 函数、printf 格式化与报表生成」这节课中我会学到什么?
定义用户函数并使用 printf,从原始数据流生成精美的表格报表。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DevOps Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「awk 函数、printf 格式化与报表生成」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DevOps Bootcamp 课中编写并运行代码吗?
能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- awk 中的记录、字段和自定义分隔符
- 模式、范围与 BEGIN/END 块
- 使用 awk 数组进行聚合与分组
- awk 函数、printf 格式化与报表生成