0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

awk 函数、printf 格式化与报表生成

定义用户函数并使用 printf,从原始数据流生成精美的表格报表。

awk 函数、printf 格式化与报表生成 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 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 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

「awk 函数、printf 格式化与报表生成」这节课中我会学到什么?

定义用户函数并使用 printf,从原始数据流生成精美的表格报表。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「awk 函数、printf 格式化与报表生成」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?

能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. awk 中的记录、字段和自定义分隔符
  2. 模式、范围与 BEGIN/END 块
  3. 使用 awk 数组进行聚合与分组
  4. awk 函数、printf 格式化与报表生成
← 返回 Linux Command Line & Bash Scripting Mastery