Linux awk命令全面详解

2024/02/02 Linux 共 56265 字,约 161 分钟

Linux awk命令全面详解

awk是一种强大的文本处理工具,它不仅是一个命令,更是一门完整的编程语言。awk特别擅长处理结构化数据,如表格、日志文件等。本文将详细介绍awk命令的各种功能和用法,帮助您掌握这一强大的文本处理工具。

1. 命令概述

awk是一个模式扫描和处理语言,它可以:

  • 从文件或标准输入读取文本
  • 根据指定的模式匹配行
  • 对匹配的行执行指定的操作
  • 输出结果到标准输出或文件

awk的名称来源于其创始人 Alfred Aho、Peter Weinberger 和 Brian Kernighan 的姓氏首字母。在Linux系统中,通常使用的是 GNU 版本的 awk,称为 gawk。

1.1 工作原理

awk的工作原理可以概括为:

  1. 按行读取输入(默认情况下)
  2. 将每一行分割成字段(默认以空格或制表符为分隔符)
  3. 根据指定的模式匹配行
  4. 对匹配的行执行相应的操作
  5. 处理完所有行后执行可选的 END 块

1.2 程序执行流程

gawk执行AWK程序的详细顺序如下:

  1. 首先,执行通过-v选项指定的所有变量赋值
  2. 然后,gawk将程序编译成内部形式
  3. 接着,执行BEGIN规则中的代码(如果有)
  4. 然后,读取ARGV数组中命名的每个文件(最多到ARGV[ARGC-1])
    • 如果命令行上没有指定文件,则读取标准输入
    • 如果ARGV中的元素为空字符串(“”),gawk会跳过它
    • 对于每个输入文件,如果存在BEGINFILE规则,则在处理文件内容前执行相关代码
    • 对于每个输入记录,gawk测试它是否匹配程序中的任何模式,对每个匹配的模式执行关联的操作
    • 处理完文件后,如果存在ENDFILE规则,则执行相关代码
  5. 最后,在所有输入用完后,执行END规则中的代码(如果有)

注意: 如果命令行上的文件名形式为var=val,它会被视为变量赋值,在BEGIN规则运行后执行。这对于动态分配控制输入如何分成字段和记录的变量值特别有用。

1.2 基本语法

awk命令的基本语法如下:

awk [选项] '程序' [文件...]

其中,程序的基本结构为:

/pattern/ { action }

或者更完整的结构:

BEGIN { action }
/pattern/ { action }
END { action }

1.3 准备测试数据

在开始学习awk命令之前,让我们创建一些测试数据文件,以便后续示例使用:

# 创建基本的测试文件
cat > awk_test.txt << 'EOF'
Alice 85 90 95
Bob 75 80 85
Charlie 90 95 100
David 60 65 70
EOF

# 创建CSV格式文件
cat > data.csv << 'EOF'
name,age,city,salary
Alice,28,New York,80000
Bob,32,Boston,95000
Charlie,45,Chicago,120000
David,36,San Francisco,110000
EOF

# 创建日志文件示例
cat > app.log << 'EOF'
2024-02-01 10:05:23 INFO User login: alice
2024-02-01 10:07:34 ERROR Database connection failed
2024-02-01 10:10:12 INFO User logout: alice
2024-02-01 11:15:45 INFO User login: bob
2024-02-01 14:30:22 WARNING Disk usage high (85%)
EOF

2. 常用参数详解

awk命令支持多种参数,这些参数可以控制awk的行为方式:

参数 长选项 描述
-f 脚本文件 --file=脚本文件 从指定文件读取awk程序
-F fs --field-separator=fs 指定字段分隔符
-v var=val --assign=var=val 在执行程序前定义变量
-b --characters-as-bytes 将每个字符视为单个字节,忽略区域设置信息
-c --traditional 以传统模式运行,与Brian Kernighan的awk行为一致
-C --copyright 显示版权信息
-d[文件] --dump-variables[=文件] 输出全局变量信息到指定文件,默认awkvars.out
-D[文件] --debug[=文件] 启用调试功能,可选指定调试命令文件
-e '程序文本' --source='程序文本' 直接在命令行指定程序
-E 文件 --exec=文件 类似于-f,但作为最后处理的选项,适用于#!脚本和CGI应用,禁用命令行变量赋值
-g --gen-pot 扫描并解析awk程序,生成GNU .pot格式文件,不执行程序
-h --help 显示帮助信息
-i 包含文件 --include=包含文件 加载awk源库,使用AWKPATH环境变量搜索,自动添加.awk后缀
-l 库 --load=库 从共享库加载gawk扩展,使用AWKLIBPATH环境变量搜索
-L[模式] --lint[=模式] 提供可疑或不可移植构造的警告。模式可以是fatal、invalid或no-ext
-M --bignum 强制使用任意精度算术,需要编译时支持GNU MPFR和GMP库
-n --non-decimal-data 识别输入数据中的八进制和十六进制值,使用时需谨慎!
-N --use-lc-numeric 使用区域设置的小数点字符解析输入数据
-o[文件] --pretty-print[=文件] 将格式化的程序输出到文件,默认awkprof.out,隐含–no-optimize
-O --optimize 启用默认优化,包括简单常量折叠,默认开启
-p[prof-file] --profile[=prof-file] 启动性能分析会话,发送分析数据到指定文件,默认awkprof.out,隐含–no-optimize
-P --posix 以POSIX兼容模式运行,有额外限制如不识别\x转义序列等
-r --re-interval 允许正则表达式中的区间表达式
-s --no-optimize 禁用gawk的默认优化
-S --sandbox 以沙箱模式运行,禁用system()函数、重定向等功能
-t --lint-old 提供与原始UNIX awk不兼容的警告
-V --version 显示gawk版本信息
--   标记选项结束,允许后续参数以”-“开头

注意事项:

  • 参数顺序可能会影响命令的执行结果
  • 多个参数可以组合使用
  • 对于复杂的程序,建议使用-f参数从文件读取
  • 使用--选项可以确保gawk正确处理以”-“开头的参数,这与大多数POSIX程序的参数解析惯例一致

2.1 gawk特定功能说明

GNU awk (gawk)提供了一些标准awk之外的高级功能:

  • 程序分析器:使用--profile选项可以收集程序执行的统计信息,帮助优化awk程序性能。虽然会使程序运行速度变慢,但会在执行完毕后自动生成分析报告到awkprof.out文件。报告包含每条语句的执行计数和用户定义函数的调用次数。

  • 集成调试器:通过--debug选项可以启动交互式调试会话。在调试模式下,gawk会加载awk源代码并提示输入调试命令。注意,调试器只能调试通过-f--include选项提供的awk程序源代码。

  • 命名空间支持:gawk支持命名空间功能,可以避免变量名和函数名冲突。使用@namespace指令可以定义命名空间。这对于大型程序和库的开发特别有用。

  • 扩展正则表达式:gawk提供了完整的正则表达式支持,包括区间表达式等高级功能。使用--re-interval选项可以启用正则表达式中的区间表达式。

  • POSIX兼容模式:通过--posix选项,gawk可以严格按照POSIX标准运行,有一些额外限制:不识别\x转义序列、不能在?:后换行、不识别func作为function的同义词等。

  • 沙箱模式--sandbox选项使gawk在沙箱模式下运行,禁用system()函数、getline输入重定向、printprintf输出重定向以及动态扩展加载。这有效阻止脚本访问除命令行指定文件外的本地资源,提高安全性。

  • 代码优化:gawk提供了代码优化功能,通过--optimize选项启用(默认开启),包括简单常量折叠等优化。使用--no-optimize可以禁用这些优化。

  • 程序格式化--pretty-print选项可以将awk程序输出为格式化、易读的版本,有助于理解和维护复杂程序。

  • 国际化支持:通过--gen-pot选项可以生成GNU .pot格式文件,用于awk程序的国际化和本地化。

  • 变量检查--dump-variables选项输出全局变量的列表、类型和最终值,有助于查找拼写错误和确保函数不会无意中使用全局变量。

  • 代码检查--lint系列选项提供对可疑或不可移植构造的警告,帮助开发更干净、更可移植的awk程序。

  • 环境变量支持:gawk支持以下重要的环境变量:
    • AWKPATH:指定查找通过-f--include选项指定的源文件的搜索路径。默认为”.:/usr/local/share/awk”
    • AWKLIBPATH:指定查找通过--load选项指定的扩展模块的搜索路径。默认为”/usr/local/lib/gawk”
  • 命令行目录处理:在gawk 4.0及以上版本中,命令行上的目录会产生警告但被跳过。使用--posix--traditional选项时,目录会被视为致命错误(符合POSIX标准)。

2.1.1 gawk高级功能示例

# 示例1:使用程序分析器
# 创建一个简单的awk程序文件
cat > analyze_program.awk << 'EOF'
BEGIN { print "Starting analysis" }
{ 
    sum += $2 
    if ($2 > max) max = $2
    if ($2 < min || NR == 1) min = $2
}
END { 
    print "Sum:", sum
    print "Avg:", sum/NR
    print "Max:", max
    print "Min:", min
}
EOF

# 使用分析器运行程序
awk --profile -f analyze_program.awk awk_test.txt

# 查看生成的分析报告
# cat awkprof.out

# 示例2:命名空间的使用
cat > namespace_example.awk << 'EOF'
@namespace "math";

function add(a, b) {
    return a + b
}

function multiply(a, b) {
    return a * b
}

@namespace "string";

function add(a, b) {
    return a b  # 字符串连接
}

@namespace "";

{ 
    # 访问不同命名空间的函数
    print "Math sum:", math::add($2, $3)
    print "String concat:", string::add($1, ": ")
}
EOF

# 运行命名空间示例
# awk -f namespace_example.awk awk_test.txt

# 示例3:使用调试器(交互式,这里仅展示命令)
# awk --debug -f analyze_program.awk awk_test.txt
# 调试命令示例:
# break BEGIN  # 在BEGIN块设置断点
# run          # 运行程序
# print sum    # 打印变量值
# next         # 执行下一条语句
# quit         # 退出调试器

# 示例4:沙箱模式的使用
# 创建一个包含潜在危险操作的脚本
cat > sandbox_test.awk << 'EOF'
{ 
    print "Processing:", $0
    # 尝试执行系统命令(在沙箱模式下会被禁止)
    # system("echo \\"This could be dangerous\\"")
    # 尝试读取其他文件(在沙箱模式下会被禁止)
    # getline < "/etc/passwd"
}
EOF

# 使用沙箱模式运行
# awk -S -f sandbox_test.awk awk_test.txt

# 示例5:使用lint选项检查程序问题
cat > lint_test.awk << 'EOF'
{ 
    # 可疑的构造:使用未初始化变量
    print "Total:", total + $2
    # 使用gawk扩展功能
    if (match($0, /[0-9]{2,3}/, arr)) {
        print "Found number:", arr[0]
    }
}
EOF

# 使用lint选项检查
# awk --lint -f lint_test.awk awk_test.txt
# 使用lint=fatal使警告成为错误
# awk --lint=fatal -f lint_test.awk awk_test.txt

# 示例6:使用--dump-variables检查变量
# 创建一个使用多个变量的程序
cat > var_dump_test.awk << 'EOF'
BEGIN {
    counter = 0
    threshold = 85
}
{ 
    counter++
    if ($2 > threshold) {
        high_scores++
    }
    total += $2
}
END {
    avg = total / counter
    print "Results calculated"
}
EOF

# 运行并导出变量信息
# awk --dump-variables=vars.out -f var_dump_test.awk awk_test.txt
# 查看变量信息
# cat vars.out

# 示例7:处理非十进制数据
cat > hex_oct_test.txt << 'EOF'
Decimal: 100
Hex: 0x64
Octal: 0144
EOF

# 启用非十进制数据识别
# awk -n '{ print $1, "value:", $2 + 0 }' hex_oct_test.txt

# 示例8:使用pretty-print格式化程序
# 创建一个格式混乱的程序
cat > messy_program.awk << 'EOF'
BEGIN{print "Start"} {if($2>80){print $1" passed"}else{print $1" failed"}} END{print "Done"}
EOF

# 格式化程序
# awk --pretty-print=formatted.awk -f messy_program.awk
# 查看格式化后的程序
# cat formatted.awk

# 示例9:生成国际化模板文件
cat > i18n_program.awk << 'EOF'
BEGIN {
    print "欢迎使用awk程序"
    print "This is an awk program"
}
{ 
    printf "处理第%d行: %s\n", NR, $0
}
END {
    print "程序执行完毕"
    print "Program completed"
}
EOF

# 生成.pot文件
# awk --gen-pot -f i18n_program.awk > program.pot
# 查看生成的模板文件
# cat program.pot


## 3. 基本使用方法

### 3.1 打印整个文件

最简单的`awk`命令是打印整个文件的内容:

```bash
# 打印整个文件内容
awk '{ print }' awk_test.txt

# 等价于
awk '{ print $0 }' awk_test.txt

这里的$0表示整行内容。

3.2 打印指定字段

awk会自动将每行分割成字段,默认以空格或制表符为分隔符,字段编号从1开始:

# 打印每行的第一个字段
awk '{ print $1 }' awk_test.txt

# 打印每行的第一个和第三个字段
awk '{ print $1, $3 }' awk_test.txt

# 使用不同的输出分隔符
awk '{ print $1 " - " $3 }' awk_test.txt

3.3 指定字段分隔符

使用-F参数可以指定不同的字段分隔符:

# 使用逗号作为分隔符
awk -F, '{ print $1, $4 }' data.csv

# 使用多个字符作为分隔符(使用正则表达式)
awk -F'[, ]' '{ print $1, $3 }' data.csv

# 使用多个可能的分隔符
awk -F'[:; ]' '{ print $1 }' /etc/passwd

3.4 使用BEGIN和END块

BEGIN块在处理任何输入行之前执行,END块在处理完所有输入行之后执行:

# 使用BEGIN块设置标题,END块计算总数
awk 'BEGIN { print "Name\tTotal" } { total = $2 + $3 + $4; print $1 "\t" total } END { print "-----------------\nDone processing" }' awk_test.txt

# 计算文件中的行数
awk 'END { print "Total lines: " NR }' awk_test.txt

4. 模式匹配

4.1 使用正则表达式匹配

# 匹配包含"Bob"的行
awk '/Bob/ { print }' awk_test.txt

# 匹配以"A"开头的行
awk '/^A/ { print }' awk_test.txt

# 匹配以数字结尾的行
awk '/[0-9]$/ { print }' awk_test.txt

4.2 使用比较运算符

# 打印第二个字段大于80的行
awk '$2 > 80 { print }' awk_test.txt

# 打印第一个字段等于"Alice"的行
awk '$1 == "Alice" { print }' awk_test.txt

# 打印第三个字段不等于90的行
awk '$3 != 90 { print }' awk_test.txt

4.3 使用逻辑运算符

# 逻辑与:打印第二个字段大于80且第三个字段小于95的行
awk '$2 > 80 && $3 < 95 { print }' awk_test.txt

# 逻辑或:打印第一个字段是"Alice"或"Bob"的行
awk '$1 == "Alice" || $1 == "Bob" { print }' awk_test.txt

# 逻辑非:打印第一个字段不是"David"的行
awk '$1 != "David" { print }' awk_test.txt

4.4 范围模式

# 匹配从包含"Alice"的行到包含"Charlie"的行
awk '/Alice/,/Charlie/ { print }' awk_test.txt

# 匹配从第1行到第3行
awk 'NR >= 1 && NR <= 3 { print }' awk_test.txt

5. 变量和函数

5.1 变量概述

awk变量是动态的,它们在首次使用时自动创建。变量的值可以是浮点数、字符串或两者兼具,具体取决于它们的使用方式。此外,gawk还允许变量具有正则表达式类型。

变量特点:

  • 无需声明,首次使用时自动创建
  • 支持数字、字符串和正则表达式类型
  • 支持一维数组,多维数组可通过模拟实现
  • gawk提供真正的数组的数组功能

5.1.1 变量类型转换

awk根据上下文自动转换变量类型,但也可以手动控制:

# 变量类型转换示例
awk 'BEGIN {
    # 未初始化变量
    print "Uninitialized variable:"  
    print "Numeric value:", uninitialized + 0  # 输出0
    print "String value:", uninitialized ""   # 输出空字符串
    
    # 强制类型转换
    num = "123.45"
    str = 678.90
    
    print "\nType conversion:"
    print "Force numeric:", num + 0      # 输出123.45
    print "Force string:", str ""       # 输出678.9
    
    # CONVFMT控制数字到字符串的转换
    print "\nCONVFMT effect:"
    CONVFMT = "%.2f"
    a = 12
    b = a ""  # 即使CONVFMT设置为%.2f,整数仍会转换为整数形式
    print "Integer conversion:", b  # 输出12
    
    c = 12.345
    d = c ""  # 使用CONVFMT格式
    print "Float conversion:", d  # 输出12.35
}'

5.1.2 命名空间

gawk提供简单的命名空间功能,帮助解决变量全局化的问题:

# 命名空间示例
awk 'BEGIN {
    # 使用默认命名空间(awk)
    var1 = "global variable"
    
    # 定义带命名空间的变量
    myns::var2 = "in my namespace"
    
    # 访问命名空间变量
    print "Default namespace variable:", var1
    print "myns namespace variable:", myns::var2
    
    # 切换当前命名空间
    @namespace "myns"
    
    # 现在直接定义的变量属于myns命名空间
    var3 = "in current namespace"
    
    # 访问不同命名空间的变量
    print "Current namespace variable:", var3
    print "Default namespace variable:", awk::var1
    
    # 大写变量始终属于awk命名空间
    UPPERCASE = "uppercase in awk namespace"
    print "UPPERCASE in awk namespace:", awk::UPPERCASE
}'

5.1.3 八进制和十六进制常量

可以在awk程序中使用C风格的八进制和十六进制常量:

# 八进制和十六进制常量示例
awk 'BEGIN {
    # 八进制常量
    octal = 011  # 十进制9
    # 十六进制常量
    hex = 0x11   # 十进制17
    
    print "Octal 011 = decimal", octal
    print "Hex 0x11 = decimal", hex
    print "Sum =", octal + hex
}'

5.1.4 字符串常量和转义序列

字符串常量用双引号括起来,支持多种转义序列:

# 字符串转义序列示例
awk 'BEGIN {
    # 基本转义序列
    print "Newline:\nTab:\tBackslash:\\"
    print "Alert(beep):\a"
    print "Backspace:\b"
    print "Form-feed:\f"
    print "Carriage return:\r"
    print "Vertical tab:\v"
    
    # 打印包含引号的字符串
    print "String with \"quotes\""
    
    # 十六进制转义序列(最多两位十六进制数字)
    print "\nHex escape sequence:"
    print "ESC character (\x1B):", "\x1B"
    print "ASCII 65 (A):", "\x41"
    
    # 八进制转义序列(1-3位八进制数字)
    print "\nOctal escape sequence:"
    print "ESC character (\033):", "\033"
    print "ASCII 65 (A):", "\101"
    
    # 字面字符转义
    print "\nLiteral character escape:"
    print "Backslash: \\c where c is any character"
}'

5.1.5 正则表达式常量

正则表达式常量用斜杠(/)括起来,gawk还支持强类型正则表达式常量:

# 正则表达式常量示例
awk 'BEGIN {
    # 基本正则表达式匹配
    print "Basic regex matching:"
    text = "Hello, World!"
    if (text ~ /World/) {
        print "Text contains 'World'"
    }
    
    # 正则表达式中的转义序列
    print "\nRegex with escape sequences:"
    whitespace = "Line1\nLine2\tLine3"
    gsub(/[ \t\f\n\r\v]+/, ", ", whitespace)
    print "Normalized whitespace:", whitespace
    
    # gawk强类型正则表达式常量(使用@前缀)
    print "\ngawk strongly typed regex constants:"
    
    # 创建强类型正则表达式变量
    number_regex = @/[0-9]+/
    word_regex = @/[a-zA-Z]+/
    
    # 使用强类型正则表达式
    test_text = "abc123def"
    
    if (test_text ~ number_regex) {
        print "Text contains numbers"
    }
    
    if (test_text ~ word_regex) {
        print "Text contains words"
    }
    
    # 将正则表达式作为参数传递给函数
    function check_pattern(str, pattern) {
        if (str ~ pattern) {
            return "Match found"
        } else {
            return "No match"
        }
    }
    
    print "Check with number pattern:", check_pattern("42 is the answer", number_regex)
    print "Check with word pattern:", check_pattern("Only words here", word_regex)
}'

5.1.6 正则表达式语法详解

以下是AWK中正则表达式的完整语法参考:

[abc...]   字符列表:匹配abc...中的任意字符。可以使用破折号分隔字符来表示字符范围。要在列表中包含字面意义的破折号,请将其放在最前面或最后面。

        [^abc...]  否定字符列表:匹配除了abc...之外的任意字符。
     
        r1|r2      选择:匹配r1或r2。
     
        r1r2       连接:匹配r1,然后匹配r2。
     
        r+         匹配一个或多个r。
    
        r*         匹配零个或多个r。
     
        r?         匹配零个或一个r。
     
        (r)        分组:匹配r。
     
        r{n}
        r{n,}
        r{n,m}     大括号内的一个或两个数字表示区间表达式。如果大括号内有一个数字,则前面的正则表达式r重复n次。如果有两个由逗号分隔的数字,则r重复n到m次。如果有一个数字后跟逗号,则r至少重复n次。
     
        \y         匹配单词开头或结尾的空字符串。
     
        \B         匹配单词内部的空字符串。
     
        \\         匹配单词开头的空字符串。
     
        \\>        匹配单词结尾的空字符串。
     
        \s         匹配任意空白字符。
     
        \S         匹配任意非空白字符。
     
        \w         匹配任意单词构成字符(字母、数字或下划线)。
     
        \W         匹配任意非单词构成字符。
     
        \`         匹配缓冲区(字符串)开头的空字符串。
     
        \'         匹配缓冲区结尾的空字符串。

POSIX字符类

[:alnum:]  字母数字字符。

[:alpha:]  字母字符。

[:blank:]  空格或制表符。

[:cntrl:]  控制字符。

[:digit:]  数字字符。

[:graph:]  既可打印又可见的字符。

[:lower:]  小写字母字符。

[:print:]  可打印字符。

[:punct:]  标点符号字符。

[:space:]  空白字符。

[:upper:]  大写字母字符。

[:xdigit:] 十六进制数字字符。

排序符号和等价类(多字节字符集支持):

  • [[.ch.]] - 排序符号,匹配多字符排序元素
  • [[=e=]] - 等价类,匹配具有相同排序权重的所有字符

正则表达式模式匹配控制

  • 默认模式:支持完整POSIX正则表达式和GNU扩展
  • --posix:仅支持POSIX正则表达式,GNU操作符被视为普通字符
  • --traditional:使用传统awk的正则表达式语法

6. 模式和动作

AWK是一种面向行的语言,由模式和动作组成。模式在前,动作用花括号({})括起来。模式或动作可以缺失,但不能同时缺失。

6.1 基本概念

# 基本模式-动作结构
# 模式缺失:对每一行执行动作
awk '{ print $1 }' awk_test.txt

# 动作缺失:等同于 { print }
awk '/Alice/' awk_test.txt

# 完整的模式-动作
awk '$2 > 80 { print $1 " passed" }' awk_test.txt

# 语句分隔符和行继续符
awk 'BEGIN { print "Start"; count=0 } { count++ } END { print "Total:", count }' awk_test.txt

# 使用反斜杠进行行继续
awk 'BEGIN { \
    print "Multi-line\nstatement" \
}'

6.2 特殊模式

6.2.1 BEGIN和END模式

# BEGIN和END模式示例
awk 'BEGIN {
    print "Student Grades Report"
    print "====================="
    sum = 0
    count = 0
} {
    # 处理每一行
    sum += $2
    count++
    print $1 ": " $2
} END {
    print "====================="
    print "Average: " sum / count
}' awk_test.txt

6.2.2 BEGINFILE和ENDFILE模式(gawk特有)

# BEGINFILE和ENDFILE模式示例
cat > file1.txt << 'EOF'
Line 1 in file1
Line 2 in file1
EOF

cat > file2.txt << 'EOF'
Line 1 in file2
Line 2 in file2
EOF

# 使用BEGINFILE和ENDFILE处理多个文件
awk 'BEGINFILE {
    print "\nProcessing file: " FILENAME
    # 检查文件是否成功打开
    if (ERRNO) {
        print "Error opening file: " ERRNO
        nextfile  # 跳过无法打开的文件
    }
    file_count++
} {
    line_count++
} ENDFILE {
    print "Lines in " FILENAME ": " FNR
} END {
    print "\nTotal files processed: " file_count
    print "Total lines processed: " line_count
}' file1.txt file2.txt nonexistent.txt

6.3 模式类型

6.3.1 正则表达式模式

# 正则表达式模式示例
awk '/^A/' awk_test.txt  # 匹配以A开头的行
awk '/9$/' awk_test.txt  # 匹配以9结尾的行
awk '/[0-9]{2,3}/' awk_test.txt  # 匹配包含2-3位数字的行

# 使用变量作为正则表达式
awk 'BEGIN { pattern = "Bob"; } $0 ~ pattern { print }' awk_test.txt

6.3.2 关系表达式模式

# 关系表达式模式示例
awk '$2 > 80' awk_test.txt  # 第二个字段大于80
awk '$1 == "Alice"' awk_test.txt  # 第一个字段等于Alice
awk 'NF >= 3' awk_test.txt  # 字段数大于等于3
awk '$0 !~ /error/i' app.log  # 不包含error(忽略大小写)

6.3.3 逻辑运算符组合

# 逻辑运算符组合模式
awk '$2 > 80 && $3 > 85' awk_test.txt  # 逻辑与:第二个字段>80且第三个字段>85
awk '$1 == "Alice" || $1 == "Bob"' awk_test.txt  # 逻辑或:名字是Alice或Bob
awk '!($1 ~ /^D/)' awk_test.txt  # 逻辑非:不以D开头

# 使用括号改变优先级
awk '$2 > 80 && ($3 > 85 || $4 > 90)' awk_test.txt

6.3.4 三元运算符模式

# 三元运算符模式
awk '$2 >= 90 ? "Excellent" : ($2 >= 80 ? "Good" : "Average") { print $1 ": " $2 ": " $0 }' awk_test.txt

6.3.5 范围模式

# 范围模式示例
# 匹配从包含Alice的行到包含Charlie的行
awk '/Alice/,/Charlie/' awk_test.txt

# 使用数值范围
awk 'NR >= 2 && NR <= 4' awk_test.txt

# 注意:范围模式不能与其他模式组合
# 下面的写法是错误的:awk '/Alice/,/Charlie/ && $2 > 80' awk_test.txt

# 可以使用变量来模拟复杂的范围条件
awk 'BEGIN { in_range = 0 } 
     /Alice/ { in_range = 1 }
     in_range && $2 > 80 { print }
     /Charlie/ { in_range = 0 }' awk_test.txt

6.2 记录和字段

6.2.1 记录

记录(Record)通常由换行符分隔。你可以通过设置内置变量RS来控制记录的分隔方式:

  • 如果RS是单个字符,则该字符作为记录分隔符
  • 如果RS是正则表达式,则匹配该正则表达式的文本作为记录分隔符
  • 如果RS设置为空字符串,则记录由空行分隔
  • RS为空字符串时,换行符始终作为字段分隔符,无论FS的值是什么

在兼容模式下,RS值的字符串中只有第一个字符用于分隔记录。

6.2.2 字段

当读取每条输入记录时,gawk会使用FS变量的值作为字段分隔符将记录分割成字段:

  • 如果FS是单个字符,则该字符作为字段分隔符
  • 如果FS是空字符串,则每个字符单独成为一个字段
  • 否则,FS被视为完整的正则表达式
  • 特殊情况:如果FS是单个空格,则字段由连续的空格、制表符或换行符分隔

注意: IGNORECASE变量的值也会影响当FS为正则表达式时字段的分割方式,以及当RS为正则表达式时记录的分隔方式。

除了FS外,gawk还提供了两种其他方式来定义字段:

  1. FIELDWIDTHS变量:设置为以空格分隔的数字列表,每个字段具有固定宽度。每个字段宽度前可选择性地加上冒号分隔的值,指定字段开始前要跳过的字符数。

  2. FPAT变量:设置为表示正则表达式的字符串,每个字段由匹配该正则表达式的文本组成。在这种情况下,正则表达式描述的是字段本身,而不是分隔字段的文本。

6.2.3 字段操作

输入记录中的每个字段可以通过其位置引用:$1$2等。$0表示整个记录,包括前导和尾随空白。

  • 字段引用不必使用常量:n = 5; print $n打印输入记录中的第五个字段
  • NF变量设置为输入记录中的字段总数
  • 引用不存在的字段(即$NF之后的字段)会产生空字符串
  • 对不存在的字段赋值(例如$(NF+2) = 5)会增加NF的值,创建任何中间字段(值为空字符串),并导致$0的值被重新计算,字段之间用OFS的值分隔
  • 引用负编号的字段会导致致命错误
  • 减少NF会导致超过新值的字段值丢失,$0的值被重新计算,字段之间用OFS的值分隔
  • 对现有字段赋值会导致在引用$0时重建整个记录
  • $0赋值会导致记录被重新分割,为字段创建新值

6.3 内置变量

awk提供了许多内置变量:

| 变量 | 描述 | |——|——| | $0 | 整行内容 | | $1, $2, ... | 各个字段的值 | | ARGC | 命令行参数的数量(不包括gawk的选项或程序源代码) | | ARGIND | 当前正在处理的文件在ARGV中的索引 | | ARGV | 命令行参数的数组,索引从0到ARGC-1。动态更改ARGV的内容可以控制用于数据的文件 | | BINMODE | 在非POSIX系统上,指定所有文件I/O使用”二进制”模式。数值1、2或3分别指定输入文件、输出文件或所有文件应使用二进制I/O | | CONVFMT | 数字的转换格式,默认为”%.6g” | | ENVIRON | 包含当前环境值的数组,索引为环境变量,每个元素为该变量的值(例如ENVIRON[“HOME”]可能为”/home/user”) | | ERRNO | 当在getline重定向、读取或close()过程中发生系统错误时,设置为描述错误的字符串。在非英语环境中可能会被翻译。如果ERRNO中的字符串对应于errno(3)变量中的系统错误,则可以在PROCINFO[“errno”]中找到数值。对于非系统错误,PROCINFO[“errno”]将为零 | | FNR | 当前输入文件中的记录号 | | FILENAME | 当前文件名。如果命令行上未指定文件,则FILENAME的值为”-“。但是,FILENAME在BEGIN规则内未定义(除非由getline设置) | | FIELDWIDTHS | 以空格分隔的字段宽度列表。设置后,gawk会将输入解析为固定宽度的字段,而不是使用FS变量的值作为字段分隔符。每个字段宽度前可选择性地加上冒号分隔的值,指定字段开始前要跳过的字符数 | | FPAT | 描述记录中字段内容的正则表达式。设置后,gawk会将输入解析为字段,其中字段与正则表达式匹配,而不是使用FS的值作为字段分隔符 | | FS | 输入字段分隔符,默认为空格 | | FUNCTAB | 一个数组,其索引和对应值是程序中所有用户定义或扩展函数的名称。注意:不能对FUNCTAB数组使用delete语句 | | IGNORECASE | 控制所有正则表达式和字符串操作的大小写敏感性。如果IGNORECASE具有非零值,则字符串比较和规则中的模式匹配、使用FS和FPAT进行字段分割、使用RS进行记录分隔、使用~和!~进行正则表达式匹配,以及gensub()、gsub()、index()、match()、patsplit()、split()和sub()内置函数在执行正则表达式操作时都忽略大小写。注意:数组下标不受影响,但asort()和asorti()函数会受影响 | | LINT | 从AWK程序内动态控制–lint选项。当为true时,gawk打印lint警告;当为false时,不打印。–lint选项允许的值也可以赋给LINT,具有相同的效果。任何其他true值都只打印警告 | | NF | 当前输入记录中的字段数 | | NR | 到目前为止看到的输入记录总数 | | OFMT | 数字的输出格式,默认为”%.6g” | | OFS | 输出字段分隔符,默认为空格 | | ORS | 输出记录分隔符,默认为换行符 | | PREC | 任意精度浮点数的工作精度,默认为53 | | PROCINFO | 此数组的元素提供对运行中的AWK程序信息的访问。在某些系统上,数组中可能有元素”group1”到”groupn”(n为进程拥有的补充组数量)。使用in运算符测试这些元素。保证可用的元素包括:

  • PROCINFO[“argv”]: gawk在C语言级别接收到的命令行参数,下标从0开始
  • PROCINFO[“egid”]: getegid(2)系统调用的值
  • PROCINFO[“errno”]: 当ERRNO设置为关联错误消息时,errno(3)的值
  • PROCINFO[“euid”]: geteuid(2)系统调用的值
  • PROCINFO[“FS”]: 字段分割使用FS时为”FS”,使用FPAT时为”FPAT”,使用FIELDWIDTHS时为”FIELDWIDTHS”,或API输入解析器字段分割时为”API”
  • PROCINFO[“gid”]: getgid(2)系统调用的值
  • PROCINFO[“identifiers”]: 子数组,索引为AWK程序文本中使用的所有标识符名称。这些值表示gawk在完成程序解析后对标识符的了解,程序运行时不会更新。每个标识符的值为以下之一:
    • “array”: 标识符是数组
    • “builtin”: 标识符是内置函数
    • “extension”: 标识符是通过@load或–load加载的扩展函数
    • “scalar”: 标识符是标量
    • “untyped”: 标识符是未类型化的(可以用作标量或数组,gawk尚不知道)
    • “user”: 标识符是用户定义的函数
  • PROCINFO[“pgrpid”]: getpgrp(2)系统调用的值
  • PROCINFO[“pid”]: getpid(2)系统调用的值
  • PROCINFO[“platform”]: 指示gawk编译平台的字符串。它是以下之一:
    • “djgpp”, “mingw”: 使用DJGPP或MinGW的Microsoft Windows
    • “os2”: OS/2
    • “posix”: GNU/Linux、Cygwin、Mac OS X和传统Unix系统
    • “vms”: OpenVMS或Vax/VMS
  • PROCINFO[“ppid”]: getppid(2)系统调用的值
  • PROCINFO[“strftime”]: strftime()的默认时间格式字符串。更改其值会影响strftime()调用时格式化时间值的方式
  • PROCINFO[“uid”]: getuid(2)系统调用的值
  • PROCINFO[“version”]: gawk的版本
  • PROCINFO[“api_major”]: 扩展API的主版本号(当加载动态扩展可用时)
  • PROCINFO[“api_minor”]: 扩展API的次版本号(当加载动态扩展可用时)
  • PROCINFO[“gmp_version”]: GNU GMP库的版本(当编译了MPFR支持时)
  • PROCINFO[“mpfr_version”]: GNU MPFR库的版本(当编译了MPFR支持时)
  • PROCINFO[“prec_max”]: GNU MPFR库支持的任意精度浮点数的最大精度(当编译了MPFR支持时)
  • PROCINFO[“prec_min”]: GNU MPFR库允许的任意精度浮点数的最小精度(当编译了MPFR支持时)
  • PROCINFO[“NONFATAL”]: 如果存在,则所有重定向的I/O错误变为非致命
  • PROCINFO[“name”, “NONFATAL”]: 使name的I/O错误变为非致命
  • PROCINFO[“command”, “pty”]: 使用伪终端与command进行双向通信,而不是设置两个单向管道
  • PROCINFO[“input”, “READ_TIMEOUT”]: 从input读取数据的超时时间(毫秒),input是重定向字符串或文件名
  • PROCINFO[“input”, “RETRY”]: 如果在从input读取数据时发生可能重试的I/O错误,则getline返回-2而不是-1
  • PROCINFO[“sorted_in”]: 控制for循环中数组元素的遍历顺序。支持的值包括”@ind_str_asc”、”@ind_num_asc”等,也可以是自定义比较函数名称 | RS | 输入记录分隔符,默认为换行符 | | ROUNDMODE | 任意精度算术的舍入模式,默认为”N”(IEEE-754 roundTiesToEven模式)。接受的值包括:”A”/”a”(舍入远离零)、”D”/”d”(向负无穷舍入)、”N”/”n”(偶数舍入)、”U”/”u”(向正无穷舍入)、”Z”/”z”(向零舍入) | | RT | 记录终止符。gawk将RT设置为由RS指定的字符或正则表达式匹配的输入文本 | | RSTART | match()匹配的第一个字符的索引;无匹配时为0(字符索引从1开始) | | RLENGTH | match()匹配的字符串长度;无匹配时为-1 | | SUBSEP | 用于分隔数组元素中多个下标的字符串,默认为”\034” | | SYMTAB | 一个数组,其索引是程序中所有当前定义的全局变量和数组的名称。可用于间接访问读取或修改变量的值 | | TEXTDOMAIN | AWK程序的文本域;用于查找程序字符串的本地化翻译 |
# 打印行号和字段数
awk '{ print "Line", NR, ":", NF, "fields" }' awk_test.txt

# 使用FS和OFS变量
awk 'BEGIN { FS=","; OFS=" - " } { print $1, $2, $3 }' data.csv

# 使用FIELDWIDTHS处理固定宽度字段
# 创建一个固定宽度格式的测试文件
cat > fixed_width.txt << 'EOF'
John    28   Engineer
Alice   32   Designer
Bob     45   Manager
EOF

# 使用FIELDWIDTHS分隔固定宽度字段
awk 'BEGIN { FIELDWIDTHS="10 5 10" } { print "Name:", $1, "Age:", $2, "Job:", $3 }' fixed_width.txt

# 使用FPAT处理复杂格式(匹配字段而不是分隔符)
# 创建一个复杂格式的测试文件
cat > complex_data.txt << 'EOF'
{name:"John", age:28, role:"Engineer"}
{name:"Alice", age:32, role:"Designer"}
{name:"Bob", age:45, role:"Manager"}
EOF

# 使用FPAT提取字段内容
awk 'BEGIN { FPAT="[a-zA-Z]+" } { print "Name:", $2, "Role:", $6 }' complex_data.txt

# 使用ENVIRON数组访问环境变量
awk 'BEGIN {
    # 访问HOME环境变量
    print "Home directory:", ENVIRON["HOME"]
    # 访问PATH环境变量
    print "PATH:", ENVIRON["PATH"]
    # 访问当前用户环境变量(根据不同系统可能有所不同)
    print "Current user:", ENVIRON["USER"] || ENVIRON["USERNAME"]
    
    # 遍历所有环境变量
    print "\nAll environment variables:"
    for (var in ENVIRON) {
        print var ": " ENVIRON[var]
    }
}'

# 使用PROCINFO数组获取进程信息
awk 'BEGIN {
    print "Process ID:", PROCINFO["pid"]
    print "Parent Process ID:", PROCINFO["ppid"]
    print "Process Group ID:", PROCINFO["pgrpid"]
    print "Platform:", PROCINFO["platform"]
    print "Current effective user ID:", PROCINFO["euid"]
    print "Current effective group ID:", PROCINFO["egid"]
    print "Current real group ID:", PROCINFO["gid"]
    print "Current real user ID:", PROCINFO["uid"]
    
    # 获取gawk版本信息
    print "\nGAWK Version:", PROCINFO["version"]
    
    # 检查扩展API版本(如果可用)
    if ("api_major" in PROCINFO) {
        print "Extension API Version:", PROCINFO["api_major"] "." PROCINFO["api_minor"]
    }
    
    # 检查GMP/MPFR版本(如果可用)
    if ("gmp_version" in PROCINFO) {
        print "GMP Version:", PROCINFO["gmp_version"]
    }
    if ("mpfr_version" in PROCINFO) {
        print "MPFR Version:", PROCINFO["mpfr_version"]
    }
    
    # 获取命令行参数
    print "\nCommand line arguments:"
    for (i=0; i < length(PROCINFO["argv"]); i++) {
        print "Arg", i, ":", PROCINFO["argv"][i]
    }
    
    # 检查平台类型
    print "\nPlatform type check:"
    if (PROCINFO["platform"] == "posix") {
        print "Running on POSIX compatible system (Linux, Unix, macOS, Cygwin)"
    } else if (PROCINFO["platform"] ~ /^(djgpp|mingw)$/) {
        print "Running on Microsoft Windows"
    } else if (PROCINFO["platform"] == "os2") {
        print "Running on OS/2"
    } else if (PROCINFO["platform"] == "vms") {
        print "Running on OpenVMS or Vax/VMS"
    }
    
    # 检查当前使用的字段分隔模式
    print "\nField splitting mode:"
    print "Current field splitting in use:", PROCINFO["FS"]
}'

# 演示PROCINFO["identifiers"]子数组的使用
awk 'BEGIN {
    # 定义一些变量和函数以便在identifiers中显示
    scalar_var = 10
    array_var["index"] = 20
    
    function user_func() {}
    
    print "\nIdentifiers information:"
    # 遍历identifiers子数组
    for (id in PROCINFO["identifiers"]) {
        # 只显示我们定义的标识符类型
        if (PROCINFO["identifiers"][id] ~ /^(array|scalar|user)$/) {
            print "Identifier:", id, "Type:", PROCINFO["identifiers"][id]
        }
    }
}'

# 使用ERRNO处理文件操作错误
awk 'BEGIN {
    # 尝试打开一个不存在的文件
    if ((getline < "nonexistent_file.txt") == -1) {
        print "Error reading file:", ERRNO
        print "Error number:", PROCINFO["errno"]
    }
}'

# 演示PROCINFO["FS"]在不同字段分隔模式下的变化
# 1. 默认FS模式
awk 'BEGIN {
    print "Default field splitting mode:", PROCINFO["FS"]
}
END {
    print "After processing with default FS:", PROCINFO["FS"]
}' /dev/null

# 2. 使用FPAT模式
awk 'BEGIN {
    FPAT = "[a-zA-Z]++"
    print "FPAT field splitting mode:", PROCINFO["FS"]
}
END {
    print "After processing with FPAT:", PROCINFO["FS"]
}' /dev/null

# 3. 使用FIELDWIDTHS模式
awk 'BEGIN {
    FIELDWIDTHS = "3 2 4"
    print "FIELDWIDTHS field splitting mode:", PROCINFO["FS"]
}
END {
    print "After processing with FIELDWIDTHS:", PROCINFO["FS"]
}' /dev/null

# 演示PROCINFO["sorted_in"]控制数组遍历顺序
awk 'BEGIN {
    # 创建一个关联数组
    data["banana"] = 5
    data["apple"] = 10
    data["cherry"] = 3
    data["date"] = 7
    
    print "\nDefault traversal order:"
    for (key in data) {
        print key, "=", data[key]
    }
    
    print "\nSorted by index (ascending):"
    PROCINFO["sorted_in"] = "@ind_str_asc"
    for (key in data) {
        print key, "=", data[key]
    }
    
    print "\nSorted by value (descending):"
    PROCINFO["sorted_in"] = "@val_num_desc"
    for (key in data) {
        print key, "=", data[key]
    }
    
    # 演示自定义比较函数
    print "\nCustom sort (by value length):"
    function by_value_length(i1, v1, i2, v2) {
        if (v1 < v2) return -1
        if (v1 > v2) return 1
        return 0
    }
    PROCINFO["sorted_in"] = "by_value_length"
    for (key in data) {
        print key, "=", data[key]
    }
}'

# 演示SYMTAB数组间接访问变量
awk 'BEGIN {
    # 定义一些变量
    my_var = 42
    another_var = "Hello, World!"
    
    print "\nUsing SYMTAB for indirect access:"
    print "my_var value:", SYMTAB["my_var"]
    print "another_var value:", SYMTAB["another_var"]
    
    # 修改变量值
    SYMTAB["my_var"] = 100
    print "Modified my_var value:", my_var
    
    # 列出所有定义的变量
    print "\nAll defined variables:"
    for (var in SYMTAB) {
        if (typeof(SYMTAB[var]) != "array") {
            print var, "=", SYMTAB[var]
        }
    }
}'

# 演示ROUNDMODE控制舍入模式
awk 'BEGIN {
    # 设置高精度
    PREC = 53
    
    # 测试不同的舍入模式
    test_num = 1.25
    
    print "\nTesting different rounding modes:"
    
    ROUNDMODE = "N"  # 默认的偶数舍入
    print "ROUNDMODE=N (even):", int(test_num + 0.5)
    
    ROUNDMODE = "Z"  # 向零舍入
    print "ROUNDMODE=Z (toward zero):", int(test_num)
    
    ROUNDMODE = "U"  # 向正无穷舍入
    print "ROUNDMODE=U (up):", int(test_num + 1)
    
    ROUNDMODE = "D"  # 向负无穷舍入
    print "ROUNDMODE=D (down):", int(test_num)
}'

# 演示RT变量(记录终止符)
echo "Line1;Line2;Line3" | awk 'BEGIN { RS = ";" } {
    print "Record:", $0, "RT:", RT
}'

# 演示RSTART和RLENGTH(正则表达式匹配)
awk 'BEGIN {
    text = "Hello, World! This is a test."
    pattern = "World"
    
    if (match(text, pattern)) {
        print "Pattern found at position:", RSTART
        print "Pattern length:", RLENGTH
        print "Matched text:", substr(text, RSTART, RLENGTH)
    }
}'

# 演示SUBSEP(多维数组分隔符)
awk 'BEGIN {
    # 创建多维数组
    multidim["first", "second", "third"] = "value"
    
    # 显示SUBSEP的默认值
    print "SUBSEP default value is a control character (^\\034)", ""
    print "Accessing multidimensional array:" 
    print multidim["first", "second", "third"]
    
    # 手动构建键来访问数组
    key = "first" SUBSEP "second" SUBSEP "third"
    print "Using constructed key:", multidim[key]
}'

# 演示TEXTDOMAIN(本地化)
awk 'BEGIN {
    # 设置文本域
    TEXTDOMAIN = "myapp"
    print "Text domain set to:", TEXTDOMAIN
    # 注意:实际本地化需要相应的.mo文件
}'

# 使用IGNORECASE忽略大小写
# 创建一个测试文件
cat > mixed_case.txt << 'EOF'
Apple banana ORANGE Mango
GrapE PEAR Kiwi
EOF

# 不使用IGNORECASE(区分大小写)
awk '/apple/ { print "Found with case sensitivity:", $0 }' mixed_case.txt

# 使用IGNORECASE(忽略大小写)
awk 'BEGIN { IGNORECASE = 1 } /apple/ { print "Found with IGNORECASE:", $0 }' mixed_case.txt

# 使用LINT控制警告输出
awk 'BEGIN {
    # 启用lint警告
    LINT = 1
    # 尝试使用未初始化的变量作为数组下标(会产生警告)
    print "Trying to use uninitialized variable as array index"
    arr[undefined_var] = 1
}'

# 使用OFMT控制数字输出格式
awk 'BEGIN {
    num = 123.456789
    print "Default format:", num
    
    # 设置自定义输出格式
    OFMT = "%.2f"
    print "Custom format (2 decimal places):", num
    
    OFMT = "%08.2f"
    print "Zero-padded format:", num
}'

# 使用FUNCTAB访问函数信息(GNU awk特有)
awk 'function my_function() { return "Hello" }
BEGIN {
    print "Available functions:"
    for (func in FUNCTAB) {
        print func
    }
    print "\nIs my_function defined?", "my_function" in FUNCTAB ? "Yes" : "No"
}'

6.4 自定义变量

# 直接在程序中定义变量
awk 'BEGIN { count=0 } { count++ } END { print "Total lines:", count }' awk_test.txt

# 使用-v参数在命令行定义变量
awk -v threshold=85 '$2 > threshold { print $1 " scored above threshold" }' awk_test.txt

6.5 数学函数

# 使用数学函数
awk 'BEGIN { print "Square root of 25:", sqrt(25) }'
awk 'BEGIN { print "Sine of 90 degrees:", sin(3.14159/2) }'
awk 'BEGIN { print "Random number:", rand() }'

# 计算平均值
awk '{ sum += $2 } END { print "Average:", sum/NR }' awk_test.txt

6.6 字符串函数

# 字符串长度
awk '{ print $1, "length:", length($1) }' awk_test.txt

# 字符串匹配
awk 'BEGIN { if (match("Hello World", "World")) print "Found at position:", RSTART }'

# 字符串截取
awk 'BEGIN { print substr("Hello World", 7, 5) }'  # 输出"World"

# 字符串替换
awk 'BEGIN { print gensub("World", "Linux", "g", "Hello World") }'

6. 数组

6.1 数组基本操作

awk支持关联数组,可以使用字符串作为索引:

# 基本数组操作
awk 'BEGIN {
    # 创建数组
    fruits["apple"] = "red"
    fruits["banana"] = "yellow"
    fruits["grape"] = "purple"
    
    # 访问数组元素
    print "Apple is", fruits["apple"]
    
    # 遍历数组
    for (fruit in fruits) {
        print fruit, "is", fruits[fruit]
    }
}'

# 使用in运算符检查数组索引
awk 'BEGIN {
    arr["key"] = "value"
    if ("key" in arr) {
        print "Key exists: ", arr["key"]
    }
    # 删除数组元素
    delete arr["key"]
    if (! ("key" in arr)) {
        print "Key no longer exists"
    }
}

### 6.3 多维数组

#### 6.3.1 模拟多维数组

传统的awk通过SUBSEP分隔符模拟多维数组,SUBSEP默认为ASCII控制字符\034(^\):

```bash
# 模拟多维数组(使用SUBSEP)
awk 'BEGIN {
    # 创建多维数组
    i = "A"; j = "B"; k = "C"
    x[i, j, k] = "hello, world\n"
    
    # 直接访问
    print x[i, j, k]
    
    # 检查多维索引是否存在
    if ((i, j, k) in x) {
        print "Multidimensional index exists"
    }
    
    # 使用SUBSEP构建键
    key = i SUBSEP j SUBSEP k
    print "Using constructed key:", x[key]
    
    # 显示SUBSEP的值
    print "SUBSEP value is a control character (^\\034)"
}'

#### 6.3.2 GAWK真正的多维数组

gawk支持真正的多维数组(数组的数组),不需要矩形结构:

```bash
# gawk真正的多维数组示例
awk 'BEGIN {
    # 创建真正的多维数组
    a[1] = 5
    a[2][1] = 6
    a[2][2] = 7
    
    # 访问多维数组元素
    print "a[1]:", a[1]
    print "a[2][1]:", a[2][1]
    print "a[2][2]:", a[2][2]
    
    # 遍历多维数组
    print "\nTraversing a[2]:"
    for (i in a[2]) {
        print "a[2][" i "]:", a[2][i]
    }
}'

# 演示数组的数组作为函数参数
awk 'BEGIN {
    # 创建子数组
    subarr[1] = "one"
    subarr[2] = "two"
    
    # 将子数组赋值给父数组
    parent[1] = subarr
    
    # 注意:需要先创建子数组元素再删除以确保正确类型
    delete parent[2][1]  # 确保parent[2]是数组类型
    
    # 在父数组中添加新的子数组元素
    parent[2][3] = "three"
    
    print "parent[1][1]:", parent[1][1]
    print "parent[2][3]:", parent[2][3]
}

6.2 数组应用示例

# 统计单词出现次数
cat > words.txt << 'EOF'
apple banana apple orange banana apple
orange mango apple banana
EOF

awk '{ 
    for (i=1; i<=NF; i++) {
        count[$i]++
    }
} 
END { 
    print "Word counts:"
    for (word in count) {
        print word ":", count[word]
    }
}' words.txt

# 统计日志中每个级别的消息数量
awk '{
    level = $3
    count[level]++
} 
END {
    print "Log level counts:"
    for (l in count) {
        print l ":", count[l]
    }
}' app.log

7. 控制结构

7.1 正则表达式模式匹配控制

传统UNIX awk使用标准的正则表达式匹配。GNU操作符不被视为特殊字符,并且区间表达式不可用。由八进制和十六进制转义序列描述的字符被视为字面量,即使它们表示正则表达式元字符。

# 使用--re-interval选项启用区间表达式
awk --re-interval '/[0-9]{2,3}/' file.txt

–re-interval

允许在正则表达式中使用区间表达式,即使已提供了–traditional选项。

7.2 运算符

AWK中的运算符按优先级从高到低排列如下:

(...)       分组

$           字段引用

++ --       递增和递减,前缀和后缀

^           幂运算(也可以使用**,以及**=作为赋值运算符)

+ - !       一元加号、一元减号和逻辑非

* / %       乘法、除法和取模

+ -         加法和减法

空格        字符串连接

|   |&      getline、print和printf的管道I/O

< > <= >= == !=
            常规关系运算符

~ !~        正则表达式匹配、否定匹配。注意:不要在~或!~的左侧使用常量正则表达式(/foo/)。只在右侧使用。表达式/foo/ ~ exp与((($0 ~ /foo/) ~ exp))具有相同的含义。这通常不是您想要的。

in          数组成员资格

&&          逻辑与

||          逻辑或

?:          C条件表达式。形式为expr1 ? expr2 : expr3。如果expr1为真,表达式的值为expr2,否则为expr3。只计算expr2和expr3中的一个。

= += -= *= /= %= ^=
            赋值。支持绝对赋值(var = value)和运算符赋值(其他形式)

7.3 控制语句

控制语句如下:

if (condition) statement [ else statement ]
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
for (var in array) statement
break
continue
delete array[index]
delete array
exit [ expression ]
{ statements }
switch (expression) {
case value|regex : statement
...
[ default: statement ]
}

7.4 I/O语句

输入/输出语句如下:

close(file [, how])   关闭文件、管道或协同进程。可选的how参数只应在关闭到协同进程的双向管道的一端时使用。它必须是字符串值,"to""from"。

getline               从下一个输入记录设置$0;设置NF、NR、FNR、RT。

getline <file         从文件的下一个记录设置$0;设置NF、RT。

getline var           从下一个输入记录设置var;设置NR、FNR、RT。

getline var <file     从文件的下一个记录设置var;设置RT。

command | getline [var]
                      运行命令,将输出通过管道传输到$0或var,并设置RT。

command |& getline [var]
                      运行命令作为协同进程,将输出通过管道传输到$0或var,并设置RT。协同进程是gawk的扩展。(命令也可以是套接字。)

next                  停止处理当前输入记录。读取下一个输入记录,并从AWK程序的第一个模式开始重新处理。到达输入数据末尾时,执行任何END规则。

nextfile              停止处理当前输入文件。下一个读取的输入记录来自下一个输入文件。更新FILENAME和ARGIND,将FNR重置为1,并从AWK程序的第一个模式开始重新处理。到达输入数据末尾时,执行任何ENDFILE和END规则。

print                 打印当前记录。输出记录以ORS的值结束。

print expr-list       打印表达式。每个表达式由OFS的值分隔。输出记录以ORS的值结束。

print expr-list >file 将表达式打印到文件。每个表达式由OFS的值分隔。输出记录以ORS的值结束。

printf fmt, expr-list 格式化并打印。

printf fmt, expr-list >file
                      格式化并打印到文件。

system(cmd-line)      执行命令cmd-line,并返回退出状态。

fflush([file])        刷新与打开的输出文件或管道文件相关联的任何缓冲区。如果file缺失或是空字符串,则刷新所有打开的输出文件和管道。

print和printf还允许其他输出重定向:

print ... >> file       # 将输出追加到文件
print ... | command     # 将输出写入管道
print ... |& command    # 将数据发送到协同进程或套接字

printf格式说明

AWK版本的printf语句和sprintf()函数接受以下格式说明符:

基本格式转换符:

  • %a, %A: 以[-]0xh.hhhhp+-dd形式的十六进制浮点数(C99格式)。%A使用大写字母。
  • %c: 单个字符。如果参数是数字,则将其视为字符打印;否则,假设参数是字符串,只打印第一个字符。
  • %d, %i: 十进制数字(整数部分)。
  • %e, %E: 以[-]d.dddddde[+-]dd形式的浮点数。%E使用E而不是e。
  • %f, %F: 以[-]ddd.dddddd形式的浮点数。如果系统库支持,%F也可用,对于特殊的”非数字”和”无穷大”值使用大写字母。
  • %g, %G: 使用%e或%f转换,取较短者,去除无意义的零。%G使用%E代替%e。
  • %o: 无符号八进制数(也是整数)。
  • %u: 无符号十进制数(同样是整数)。
  • %s: 字符串。
  • %x, %X: 无符号十六进制数(整数)。%X使用ABCDEF而不是abcdef。
  • %%: 单个%字符,不需要参数转换。

可选修饰符(位于%和控制字母之间):

  • count$: 在此时使用第count个参数。这称为位置指定符,主要用于翻译后的格式字符串。
  • -: 表达式应在其字段内左对齐。
  • space: 对于数值转换,正数前添加空格,负数前添加减号。
  • +: 始终为数值转换提供符号,即使要格式化的数据是正数。+会覆盖空格修饰符。
  • #: 对某些控制字母使用”备用形式”。对于%o,提供前导零。对于%x和%X,对非零结果提供前导0x或0X。对于%e、%E、%f和%F,结果始终包含小数点。对于%g和%G,不从结果中删除尾随零。
  • 0: 前导0表示输出应使用零而不是空格填充。这仅适用于数值输出格式。
  • ': 单引号字符指示gawk在十进制数中插入区域设置的千位分隔符字符,并在浮点格式中使用区域设置的小数点字符。
  • width: 字段应填充到此宽度。默认使用空格填充,使用0标志时使用零填充。
  • .prec: 指定打印时使用的精度。对于%e、%E、%f和%F格式,这指定要打印到小数点右侧的位数。对于%g和%G格式,它指定最大有效数字位数。对于%s格式,它指定应该打印的字符串的最大字符数。

支持ISO C printf()例程的动态宽度和精度功能。格式字符串中宽度或精度规范位置的*会使它们的值从printf或sprintf()的参数列表中获取。

特殊文件名称

当从print或printf重定向到文件,或通过getline从文件读取时,gawk会识别某些特殊的文件名。这些文件名允许访问从gawk父进程(通常是shell)继承的打开文件描述符。这些文件名也可以在命令行上用于命名数据文件。

标准文件描述符相关文件名:

  • /dev/stdin: 标准输入
  • /dev/stdout: 标准输出
  • /dev/stderr: 标准错误输出
  • /dev/fd/n: 与打开的文件描述符n关联的文件

这些在错误消息输出时特别有用,例如:

print "You blew it!" > "/dev/stderr"

而不是使用:

print "You blew it!" | "cat 1>&2"
**网络连接相关文件名(用于 &协同进程运算符):**

TCP/IP连接:

  • /inet/tcp/lport/rhost/rport: 在本地端口lport与远程主机rhost的远程端口rport之间建立TCP/IP连接
  • /inet4/tcp/lport/rhost/rport: 强制使用IPv4连接
  • /inet6/tcp/lport/rhost/rport: 强制使用IPv6连接

UDP/IP连接:

  • /inet/udp/lport/rhost/rport: 使用UDP/IP而不是TCP/IP
  • /inet4/udp/lport/rhost/rport: 强制使用IPv4的UDP连接
  • /inet6/udp/lport/rhost/rport: 强制使用IPv6的UDP连接
其中,lport设为0表示让系统选择端口。这些特殊文件名只能与 &双向I/O运算符一起使用。

7.5 if-else语句示例

# 使用if-else语句
awk '{
    total = $2 + $3 + $4
    if (total >= 270) {
        grade = "A"
    } else if (total >= 240) {
        grade = "B"
    } else if (total >= 210) {
        grade = "C"
    } else {
        grade = "F"
    }
    print $1, "Total:", total, "Grade:", grade
}' awk_test.txt

6.7 循环结构

# for循环
awk 'BEGIN { 
    for (i=1; i<=5; i++) {
        print "Count:", i
    }
}'

# while循环
awk 'BEGIN { 
    i=1
    while (i<=5) {
        print "Count:", i
        i++
    }
}'

# do-while循环
awk 'BEGIN { 
    i=1
    do {
        print "Count:", i
        i++
    } while (i<=5)
}'

7.3 switch语句

GNU awk支持switch语句:

# switch语句
awk '{
    switch ($1) {
        case "Alice":
            print "Hello Alice"
            break
        case "Bob":
            print "Hello Bob"
            break
        default:
            print "Hello Stranger"
    }
}' awk_test.txt

7.6 数学函数

AWK提供了以下内置算术函数:

  • atan2(y, x): 返回y/x的反正切值,单位为弧度
  • cos(expr): 返回expr的余弦值,expr以弧度为单位
  • exp(expr): 指数函数
  • int(expr): 将expr截断为整数
  • log(expr): 自然对数函数
  • rand(): 返回0到1之间的随机数N,满足0 ≤ N < 1
  • sin(expr): 返回expr的正弦值,expr以弧度为单位
  • sqrt(expr): 返回expr的平方根
  • srand([expr]): 使用expr作为随机数生成器的新种子。如果未提供expr,则使用当前时间。返回随机数生成器的前一个种子

7.7 字符串函数

GNU awk提供了以下内置字符串函数:

7.8 时间函数

由于AWK程序的主要用途之一是处理包含时间戳信息的日志文件,gawk提供了以下用于获取时间戳和格式化时间的函数:

  • mktime(datespec [, utc-flag]): 将datespec转换为与systime()返回的形式相同的时间戳,并返回结果。datespec是一个格式为YYYY MM DD HH MM SS[ DST]的字符串。字符串内容是六个或七个数字,分别表示包含世纪的完整年份、1到12的月份、1到31的日期、0到23的小时、0到59的分钟、0到60的秒以及可选的夏令时标志。这些数字的值不必在指定的范围内;例如,-1小时表示午夜前1小时。假定使用从零点开始的公历,其中第0年在第1年之前,第-1年在第0年之前。如果存在utc-flag且为非零或非空,则假定时间在UTC时区;否则,假定时间在本地时区。如果DST夏令时标志为正,则假定时间为夏令时;如果为零,则假定为标准时间;如果为负(默认值),mktime()将尝试确定指定时间是否处于夏令时。如果datespec不包含足够的元素或者生成的时间超出范围,mktime()返回-1。

  • strftime([format [, timestamp[, utc-flag]]]): 根据format中的规范格式化timestamp。如果存在utc-flag且为非零或非空,则结果为UTC时间,否则为本地时间。timestamp应该与systime()返回的形式相同。如果缺少timestamp,则使用当前时间。如果缺少format,则使用相当于date(1)输出的默认格式。默认格式可在PROCINFO[“strftime”]中找到。

  • systime(): 返回自纪元(1970-01-01 00:00:00 UTC)以来的当前时间,以秒为单位。

  • asort(s [, d [, how] ]): 返回源数组s中的元素数量。使用gawk的常规规则对s的内容进行排序,并将排序后的值的索引替换为从1开始的连续整数。如果指定了可选的目标数组d,则首先将s复制到d,然后对d进行排序,保留源数组s的索引不变。可选字符串how控制排序方向和比较模式。

  • asorti(s [, d [, how] ]): 返回源数组s中的元素数量。行为与asort()相同,不同之处在于数组索引用于排序,而不是数组值。完成后,数组按数字索引,值是原始索引的值。原始值将丢失;因此,如果希望保留原始值,请提供第二个数组。

  • gensub(r, s, h [, t]): 在目标字符串t中搜索正则表达式r的匹配项。如果h是以下划线g或G开头的字符串,则替换r的所有匹配项为s。否则,h是一个数字,表示要替换r的第几个匹配项。如果未提供t,则使用$0。在替换文本s中,序列\n(其中n是1到9之间的数字)可用于表示与第n个带括号的子表达式匹配的文本。序列\0表示整个匹配的文本,字符&也是如此。与sub()和gsub()不同,修改后的字符串作为函数结果返回,原始目标字符串不变。

  • gsub(r, s [, t]): 对于字符串t中匹配正则表达式r的每个子字符串,替换为字符串s,并返回替换次数。如果未提供t,则使用$0。替换文本中的&会被实际匹配的文本替换。使用\&获取字面上的&(这必须键入为”\&”)。

  • index(s, t): 返回字符串t在字符串s中的索引,如果t不存在则返回0。(这意味着字符索引从1开始。)对t使用正则表达式常量是致命错误。

  • length([s]): 返回字符串s的长度,如果未提供s,则返回$0的长度。作为非标准扩展,对于数组参数,length()返回数组中的元素数量。

  • match(s, r [, a]): 返回正则表达式r在s中出现的位置,如果r不存在则返回0,并设置RSTART和RLENGTH的值。请注意,参数顺序与~运算符相同:str ~ re。如果提供了数组a,则清除a并存储捕获组信息。数组索引0包含整个匹配的文本,索引1到n包含各个捕获组的内容。

  • split(s, a [, r [, seps]]): 使用分隔符正则表达式r将字符串s分割成数组a的元素,并返回元素数量。如果未提供r,则使用FS的值。split()会忽略空字段。如果提供了可选参数seps,它必须是一个数组,该数组将填充分隔符的字符串。

  • sprintf(format, expr-list): 根据format中的指令格式化expr-list中的表达式,并返回生成的字符串而不打印。这与printf语句的工作方式相同,但结果是返回而不是打印。

  • sub(r, s [, t]): 对于字符串t中第一个匹配正则表达式r的子字符串,替换为字符串s,并返回替换次数(0或1)。如果未提供t,则使用$0。替换文本中的&会被实际匹配的文本替换。

  • substr(s, i [, n]): 返回字符串s中从索引i开始的子字符串。如果提供了n,则返回最多n个字符;否则,返回从i到字符串末尾的所有字符。在awk中,字符串索引从1开始。

  • tolower(str): 返回字符串str的副本,其中所有大写字符都转换为相应的小写字符。非字母字符保持不变。

  • toupper(str): 返回字符串str的副本,其中所有小写字符都转换为相应的大写字符。非字母字符保持不变。

  • strtonum(str): 检查str并返回其数值。如果str以前导0开头,则将其视为八进制数。如果str以0x或0X开头,则将其视为十六进制数。否则,假定为十进制数。

  • patsplit(s, a [, r [, seps]]): 根据正则表达式r将字符串s分割成数组a,同时将分隔符保存在seps数组中,并返回字段数量。元素值是s中匹配r的部分。seps[i]的值是出现在a[i]之后的可能为空的分隔符。seps[0]的值是可能为空的前导分隔符。如果省略r,则使用FPAT代替。数组a和seps首先被清除。分割行为与使用FPAT的字段分割完全相同。

Gawk支持多字节处理。这意味着index()、length()、substr()和match()都以字符而不是字节为单位工作。

7.9 位操作函数

Gawk提供以下位操作函数。它们的工作方式是将双精度浮点值转换为uintmax_t整数,执行操作,然后将结果转换回浮点数。

注意: 向这些函数传递负数会导致致命错误。

  • and(v1, v2 [, …]): 返回参数列表中提供的值的按位与。至少需要两个参数。

  • compl(val): 返回val的按位取反。

  • lshift(val, count): 返回val左移count位后的值。

  • or(v1, v2 [, …]): 返回参数列表中提供的值的按位或。至少需要两个参数。

  • rshift(val, count): 返回val右移count位后的值。

  • xor(v1, v2 [, …]): 返回参数列表中提供的值的按位异或。至少需要两个参数。

7.10 类型函数

以下函数提供有关其参数的类型相关信息:

  • isarray(x): 如果x是数组则返回true,否则返回false。此函数主要用于多维数组的元素和函数参数。

  • typeof(x): 返回一个字符串,指示x的类型。字符串将是”array”、”number”、”regexp”、”string”、”strnum”、”unassigned”或”undefined”之一。

7.11 国际化函数

以下函数可用于在AWK程序中在运行时翻译字符串:

  • bindtextdomain(directory [, domain]): 指定gawk查找.gmo文件的目录,以防它们不会或不能放在”标准”位置(例如,在测试期间)。它返回domain被”绑定”的目录。 默认域是TEXTDOMAIN的值。如果directory是空字符串(“”),则bindtextdomain()返回给定域的当前绑定。

  • dcgettext(string [, domain [, category]]): 返回文本域domain中字符串string的翻译,用于locale类别category。domain的默认值是TEXTDOMAIN的当前值。category的默认值是”LC_MESSAGES”。 如果您提供category的值,它必须是一个字符串,等于GAWK: Effective AWK Programming中描述的已知locale类别之一。您还必须提供一个文本域。如果要使用当前域,请使用TEXTDOMAIN。

  • dcngettext(string1, string2, number [, domain [, category]]): 返回文本域domain中字符串string1和string2翻译的复数形式,用于locale类别category,对应于数字number。domain的默认值是TEXTDOMAIN的当前值。category的默认值是”LC_MESSAGES”。 如果您提供category的值,它必须是一个字符串,等于GAWK: Effective AWK Programming中描述的已知locale类别之一。您还必须提供一个文本域。如果要使用当前域,请使用TEXTDOMAIN。

7.12 用户定义函数

AWK中的函数定义如下:

function name(parameter list) { statements }

函数在从模式或操作中的表达式调用时执行。函数调用中提供的实际参数用于实例化函数声明中的形式参数。数组通过引用传递,其他变量通过值传递。

由于函数最初不是AWK语言的一部分,局部变量的规定相当笨拙:它们被声明为参数列表中的额外参数。惯例是通过在参数列表中添加额外空格来分隔局部变量和实际参数。例如:

function f(p, q,     a, b)   # a和b是局部变量
{
    ...
}

/abc/     { ... ; f(1, 2) ; ... }

函数调用中的左括号必须紧跟在函数名称之后,中间不能有任何空格。这避免了与连接运算符的语法歧义。此限制不适用于上面列出的内置函数。

函数可以相互调用,也可以递归调用。用作局部变量的函数参数在函数调用时初始化为空字符串和数字零。

使用return expr从函数返回值。如果未提供值,或者函数通过”掉出”末尾返回,则返回值未定义。

作为gawk扩展,函数可以间接调用。为此,将要调用的函数名称(作为字符串)分配给变量。然后使用该变量,就好像它是函数名称一样,并在其前面加上@符号,如下所示:

function myfunc()
{
    print "myfunc called"
    ...
}

{    ...
    the_func = "myfunc"
    @the_func()    # 通过the_func调用myfunc
    ...
}

从版本4.1.2开始,这适用于用户定义函数、内置函数和扩展函数。

7.13 函数检查与动态加载

函数检查

如果提供了--lint选项,gawk会在解析时而不是运行时警告对未定义函数的调用。在运行时调用未定义函数是一个致命错误。

关键字func可以用来代替function,尽管这种用法已被弃用。

动态加载新函数

您可以使用@load语句动态地将用C或C++编写的新函数添加到正在运行的gawk解释器中。完整的详细信息超出了本文档的范围;请参阅GAWK: Effective AWK Programming。

7.14 信号处理

Gawk分析器接受两个信号:

  • SIGUSR1:导致它将分析和函数调用堆栈转储到分析文件中,该文件是awkprof.out或通过--profile选项指定的任何文件。然后它继续运行。
  • SIGHUP:导致gawk转储分析和函数调用堆栈,然后退出。

7.15 国际化

字符串常量是用双引号括起来的字符序列。在非英语环境中,可以将AWK程序中的字符串标记为需要翻译为本地自然语言。这样的字符串在AWK程序中用前导下划线(”_“)标记。例如:

gawk 'BEGIN { print "hello, world" }'

始终打印”hello, world”。但是:

gawk 'BEGIN { print _"hello, world" }'

在法国可能会打印”bonjour, monde”。

生产和运行可本地化的AWK程序涉及几个步骤:

  1. 添加BEGIN操作来为TEXTDOMAIN变量赋值,以将文本域设置为与您的程序关联的名称:
BEGIN { TEXTDOMAIN = "myprog" }

这允许gawk找到与您的程序关联的.gmo文件。如果没有此步骤,gawk将使用messages文本域,该文本域可能不包含您程序的翻译。

  1. 用前导下划线标记所有应该翻译的字符串。

  2. 如果需要,在程序中适当地使用dcgettext()和/或bindtextdomain()函数。

  3. 运行gawk --gen-pot -f myprog.awk > myprog.pot为您的程序生成.pot文件。

  4. 提供适当的翻译,并构建和安装相应的.gmo文件。

7.16 POSIX兼容性

Gawk的主要目标是与POSIX标准以及Brian Kernighan最新版本的awk兼容。为此,gawk包含以下用户可见的功能,这些功能在AWK书中没有描述,但属于Brian Kernighan版本的awk,并且在POSIX标准中:

  • 书中指出命令行变量赋值发生在awk否则会打开参数作为文件时,这是在BEGIN规则执行之后。但是,在早期实现中,当这样的赋值出现在任何文件名之前时,赋值会在BEGIN规则运行之前发生。应用程序依赖于这个”特性”。当awk被更改为与其文档匹配时,添加了-v选项用于在程序执行前赋值变量,以适应依赖于旧行为的应用程序。

  • 在处理参数时,gawk使用特殊选项”–“来表示参数的结束。在兼容模式下,它会警告但忽略未定义的选项。在正常操作中,这些参数会传递给AWK程序进行处理。

  • AWK书没有定义srand()的返回值。POSIX标准规定它返回它正在使用的种子,以允许跟踪随机数序列。因此,gawk中的srand()也返回其当前种子。

  • 其他功能包括:使用多个-f选项(来自MKS awk);ENVIRON数组;\a和\v转义序列(最初在gawk中完成并反馈回Bell Laboratories版本);tolower()和toupper()内置函数(来自Bell Laboratories版本);以及printf中的ISO C转换规范(首先在Bell Laboratories版本中完成)。

7.17 历史特性

gawk支持历史AWK实现的一个特性:可以不仅不带参数调用length()内置函数,甚至可以不带括号!因此:

a = length     # Holy Algol 60, Batman!

与以下任一相同:

a = length()
a = length($0)

使用此功能是不良做法,如果在命令行上指定了--lint,gawk会发出关于其使用的警告。

7.18 GNU扩展

Gawk有太多的扩展超出了POSIX awk。以下是gawk的一些特性,它们在POSIX awk中不可用:

  • 对于通过-f选项命名的文件,不会执行路径搜索。因此,AWKPATH环境变量不是特殊的。

  • 没有进行文件包含的工具(gawk的@include机制)。

  • 没有动态添加用C编写的新函数的工具(gawk的@load机制)。

  • \x转义序列。

  • 在?和:之后继续行的能力。

  • AWK程序中的八进制和十六进制常量。

  • ARGIND、BINMODE、ERRNO、LINT、PREC、ROUNDMODE、RT和TEXTDOMAIN变量不是特殊的。

  • IGNORECASE变量及其副作用不可用。

  • FIELDWIDTHS变量和固定宽度字段分割。

  • FPAT变量和基于字段值的字段分割。

  • FUNCTAB、SYMTAB和PROCINFO数组不可用。

  • 将RS用作正则表达式的能力。

  • I/O重定向中可用的特殊文件名不被识别。

  • 用于创建协处理的 &运算符。
  • BEGINFILE和ENDFILE特殊模式不可用。

  • 使用空字符串作为FS的值和split()的第三个参数来分割单个字符的能力。

  • split()的可选第四个参数,用于接收分隔符文本。

  • close()函数的可选第二个参数。

  • match()函数的可选第三个参数。

  • 在printf和sprintf()中使用位置说明符的能力。

  • 将数组传递给length()的能力。

  • and()、asort()、asorti()、bindtextdomain()、compl()、dcgettext()、dcngettext()、gensub()、lshift()、mktime()、or()、patsplit()、rshift()、strftime()、strtonum()、systime()和xor()函数。

  • 可本地化字符串。

  • 非致命I/O。

  • 可重试I/O。

AWK书没有定义close()函数的返回值。当关闭输出文件或管道时,gawk的close()返回fclose(3)或pclose(3)的值,分别。当关闭输入管道时,它返回进程的退出状态。如果命名的文件、管道或协处理没有通过重定向打开,则返回值为-1。

当使用–traditional选项调用gawk时,如果-F选项的fs参数是”t”,那么FS被设置为制表符。请注意,键入gawk -F\t … 只会导致shell引用”t”,而不会将”\t”传递给-F选项。由于这是一个相当难看的特殊情况,因此它不是默认行为。如果指定了–posix,这种行为也不会发生。要真正获得制表符作为字段分隔符,最好使用单引号:gawk -F’\t’ …。

7.19 环境变量

AWKPATH环境变量可用于提供gawk在查找通过-f、–file、-i和–include选项以及@include指令命名的文件时搜索的目录列表。如果初始搜索失败,则在将.awk附加到文件名后再次搜索路径。

7.20 退出状态

如果使用值调用exit语句,则gawk以给定的数值退出。

否则,如果执行过程中没有问题,gawk以C常量EXIT_SUCCESS的值退出。这通常为零。

如果发生错误,gawk以C常量EXIT_FAILURE的值退出。这通常为1。

如果gawk由于致命错误而退出,退出状态为2。在非POSIX系统上,此值可能映射到EXIT_FAILURE。

7.21 版本信息

本手册页记录的是gawk,版本5.1。

7.22 作者

原始版本的UNIX awk由Bell Laboratories的Alfred Aho、Peter Weinberger和Brian Kernighan设计和实现。Brian Kernighan继续维护和增强它。

自由软件基金会的Paul Rubin和Jay Fenlason编写了gawk,使其与第七版UNIX中分发的原始awk版本兼容。John Woods贡献了许多错误修复。David Trueman在Arnold Robbins的贡献下,使gawk与新版本的UNIX awk兼容。Arnold Robbins是当前的维护者。

有关对gawk及其文档的完整贡献者列表,请参阅GAWK: Effective AWK Programming。

有关维护者和当前支持的端口的最新信息,请参阅gawk发行版中的README文件。

7.23 Bug报告

如果您在gawk中发现bug,请发送电子邮件至bug-gawk@gnu.org。请包括您的操作系统及其修订版本、gawk的版本(来自gawk –version)、用于编译它的C编译器,以及尽可能小的测试程序和数据来重现问题。

发送bug报告之前,请执行以下操作。首先,验证您是否拥有最新版本的gawk。每个版本都会修复许多bug(通常是微妙的bug),如果您的版本过时,问题可能已经解决。其次,请查看将环境变量LC_ALL设置为LC_ALL=C是否会导致行为符合您的预期。如果是这样,这是一个区域设置问题,可能是也可能不是真正的bug。最后,请仔细阅读本手册页和参考手册,确保您认为的bug确实是bug,而不仅仅是语言中的怪癖。

无论您做什么,都不要在comp.lang.awk中发布bug报告。虽然gawk开发人员偶尔会阅读这个新闻组,但在那里发布bug报告是一种不可靠的报告bug的方式。同样,不要使用网络论坛(如Stack Overflow)来报告bug。相反,请使用上面给出的电子邮件地址。真的。

如果您使用基于GNU/Linux或BSD的系统,您可能希望向您的发行版供应商提交bug报告。这很好,但也请发送一份副本到官方电子邮件地址,因为不能保证bug报告会转发给gawk维护者。

7.24 已知问题

鉴于命令行变量赋值功能,-F选项不是必需的;它仅保留用于向后兼容性。

8. 实用示例

8.1 处理CSV文件

# 打印CSV文件中特定列
awk -F, '{ print $1, "lives in", $3, "and earns", $4 }' data.csv

# 过滤CSV文件中符合条件的行
awk -F, '$4 > 100000 { print $1, "has a high salary" }' data.csv

# 计算CSV文件中数值列的总和
awk -F, 'NR>1 { sum += $4 } END { print "Total salary:", sum }' data.csv

8.2 日志文件分析

# 提取日志中的错误信息
awk '/ERROR/ { print }' app.log

# 统计每种日志级别的出现次数
awk '{ level[$3]++ } END { for (l in level) print l ":" level[l] }' app.log

# 提取特定时间段的日志
awk '$1 " " $2 >= "2024-02-01 11:00:00" && $1 " " $2 <= "2024-02-01 15:00:00" { print }' app.log

8.3 文本转换

# 将空格分隔的文件转换为CSV格式
awk 'BEGIN { OFS="," } { print $1, $2, $3, $4 }' awk_test.txt

# 格式化输出(对齐列)
awk '{ printf "%-10s %5d %5d %5d\n", $1, $2, $3, $4 }' awk_test.txt

# 将文本转换为大写
awk '{ print toupper($0) }' awk_test.txt

8.4 数据统计

# 计算第二列的总和、平均值、最大值和最小值
awk '{
    sum += $2
    if (NR == 1 || $2 > max) max = $2
    if (NR == 1 || $2 < min) min = $2
} 
END {
    print "Sum:", sum
    print "Average:", sum/NR
    print "Max:", max
    print "Min:", min
}' awk_test.txt

# 计算每行的总和和平均值
awk '{
    sum = 0
    for (i=2; i<=NF; i++) sum += $i
    avg = sum / (NF-1)
    print $1, "Total:", sum, "Average:", avg
}' awk_test.txt

9. 高级技巧

9.1 使用awk脚本文件

对于复杂的处理任务,可以将awk程序保存到文件中:

# 创建awk脚本文件
cat > analyze.awk << 'EOF'
BEGIN {
    print "Student Analysis Report"
    print "====================="
    print "Name\tTotal\tAverage"
    print "---------------------"
}
{
    total = $2 + $3 + $4
    avg = total / 3
    print $1 "\t" total "\t" avg
    all_total += total
    count++
}
END {
    print "---------------------"
    print "Class Average:\t" all_total/count
}
EOF

# 运行awk脚本
awk -f analyze.awk awk_test.txt

9.2 多文件处理

# 处理多个文件
awk '{
    print FILENAME ":" NR ":" $0
}' awk_test.txt data.csv

# 在多文件处理中使用FNR(每个文件的行号)
awk '{
    print FILENAME ":" FNR ":" $0
}' awk_test.txt data.csv

9.3 与其他命令结合使用

# 与sort命令结合
awk '{ print $2, $1 }' awk_test.txt | sort -nr

# 与grep命令结合
ps aux | grep awk | awk '{ print $1, $2, $11 }'

# 读取find命令的输出
find /etc -name "*.conf" -type f -size +1k | xargs ls -lh | awk '{ print $5, $9 }'

9.4 自定义函数

# 定义和使用自定义函数
awk 'function calculate_average(num1, num2, num3) {
    return (num1 + num2 + num3) / 3
}
{
    avg = calculate_average($2, $3, $4)
    print $1 "\'s average:", avg
}' awk_test.txt

10. 常见陷阱与解决方案

10.1 字段分隔符问题

问题:默认的字段分隔符可能无法正确处理某些格式的文件。

解决方案

  • 使用-F参数明确指定字段分隔符
  • 对于复杂的分隔符模式,使用正则表达式
# 处理包含多个空格或制表符的文件
awk -F'[[:space:]]+' '{ print $1, $2 }' file.txt

# 处理混合分隔符
awk -F'[,;\t ]+' '{ print $1, $2 }' mixed_separators.txt

10.2 引号和转义问题

问题:在shell中使用awk时,引号和特殊字符可能会导致问题,特别是转义字符如制表符\t

解决方案

  • 使用单引号包围awk程序
  • 对于需要在程序中使用单引号的情况,可以使用转义或变量
  • 在awk程序内部,使用双引号包围转义字符
# 正确处理单引号
awk '\''{ print "It\'s a test" }'\'' file.txt

# 或者使用变量
awk -v quote="'" '{ print "It" quote "s a test" }' file.txt

# 正确处理制表符
awk '{print $1"\t"$3}' file.txt

关于转义字符和字符串的特殊说明

在awk中,无论是转义字符还是普通字符串,都必须使用双引号括起来才能被正确处理。这是因为:

  1. awk的字符串处理规则
    • 双引号内的字符串会被awk解释转义序列和作为字符串常量
    • 无引号或单引号内的转义字符会被视为普通字符
    • 无引号的普通文本会被awk解释为变量名而非字符串常量
  2. 制表符示例
    # 错误示例:直接在awk程序中使用\t(无引号包围)
    awk '{print $1\t$3}' file.txt
    # 错误信息:反斜杠不是行的最后一个字符,syntax error
       
    # 正确示例:使用双引号包围\t
    awk '{print $1"\t"$3}' file.txt
    # 正确输出:用制表符分隔的字段
    
  3. 字符串常量示例
    # 错误示例:不使用双引号括起字符串
    awk '{print hello awk}' awk.txt
    # 错误:hello和awk被解释为变量名,而这些变量未定义,结果为空
       
    # 正确示例:使用双引号括起字符串
    awk '{print "hello world"}' awk.txt
    # 正确输出:hello world(每行都打印)
    

总结:在awk的print语句中,无论是特殊字符(如\t、\n)还是普通字符串,都必须使用双引号括起来。无引号的文本会被awk解释为变量引用,而不是字符串常量,这可能导致意外的行为或错误。

原因分析

  • 当在awk程序中直接使用\t(无引号包围)时,awk会将其视为两个独立的字符:反斜杠\和字母t
  • 只有当转义序列位于双引号内时,awk才会将\t解释为一个制表符字符
  • 这与许多编程语言中的字符串处理规则一致

10.3 性能优化

问题:处理大型文件时,awk可能会变慢。

解决方案

  • 尽可能使用简单的模式
  • 避免在循环中进行复杂的计算
  • 对于非常大的文件,可以先用其他工具过滤
# 先过滤再处理,提高性能
grep "ERROR" large.log | awk '{ print $1, $2 }'

# 使用next跳过不需要处理的行
awk '$1 == "skip" { next } { print }' large_file.txt

10. 常见问题与注意事项

10.1 awk中的引号使用规则

在使用awk命令时,引号的选择对命令的正确执行至关重要。下面通过实际案例详细解释awk中引号的正确使用方法。

问题现象

# 错误示例1:使用双引号
$ awk "{print $1, $3}" awk.txt 
awk: 命令行:1: {print , }
awk: 命令行:1:        ^ syntax error
awk: 命令行:1: {print , }
awk: 命令行:1:          ^ syntax error

# 错误示例2:不使用引号
$ awk {print $1, $3} awk.txt 
awk: 命令行:1: {print
awk: 命令行:1:       ^ 未预期的新行或字符串结束

# 正确示例:使用单引号
$ awk '{print $1, $3}' awk.txt 
nihao awk2
nihao awk5
nihao awk8

原因分析

  1. shell解析与变量替换
    • 在shell中,双引号内的$符号会被解释为shell变量引用,而不是awk的字段引用
    • 当使用"{print $1, $3}"时,shell会尝试将$1$3作为shell变量展开,通常它们是空值
    • 这导致实际传递给awk的命令变成了{print , },这是一个语法错误
  2. 无引号的问题
    • 不使用引号时,shell会将{print视为一个独立的命令,而不是awk程序的一部分
    • 这导致shell解析失败,报出”未预期的新行或字符串结束”错误
  3. 单引号的优势
    • 单引号内的所有内容(包括$符号)都会被shell原样传递给awk
    • 当使用'{print $1, $3}'时,awk正确接收到$1$3作为字段引用

其他引号使用场景

  1. 需要引用shell变量的情况
    • 当需要在awk程序中使用shell变量时,可以在单引号中嵌入双引号或打破单引号 ```bash

      方法1:打破单引号嵌入shell变量

      threshold=80 awk ‘NR > 1 && $2 > ‘“$threshold”’ { print }’ data.csv

    方法2:使用-v选项传递变量

    awk -v threshold=”$threshold” ‘NR > 1 && $2 > threshold { print }’ data.csv ```

  2. 包含单引号的awk程序
    • 当awk程序本身需要使用单引号时,可以使用转义或字符串拼接 ```bash

      使用转义

      awk ‘{ print “It's a test” }’ input.txt

    使用字符串拼接

    awk ‘{ print “It””’”’”"”s a test” }’ input.txt ```

最佳实践

  1. 默认使用单引号:编写awk程序时,默认使用单引号括起整个程序
  2. 避免双引号:除非确实需要在awk程序中引用shell变量,否则不要使用双引号
  3. 使用-v选项:当需要将shell变量传递给awk时,优先使用-v选项
  4. 程序文件:对于复杂的awk程序,建议将其保存到文件中,然后使用-f选项调用

通过正确理解和使用引号规则,可以避免在使用awk命令时遇到的常见语法错误,确保命令能够按照预期工作。

11. 总结

awk是一个功能强大的文本处理工具,它结合了命令行工具的便捷性和编程语言的灵活性。掌握awk命令的关键在于:

  1. 理解其工作原理(逐行处理和字段分割)
  2. 熟练掌握模式匹配和操作语法
  3. 灵活运用变量、数组和控制结构
  4. 学习各种内置函数和自定义函数的使用
  5. 掌握与其他命令的组合使用技巧

通过本文介绍的各种技巧和示例,您应该能够在日常工作中充分利用awk命令,提高文本处理和数据分析的效率。无论是日志分析、数据转换还是统计计算,awk都能成为您得力的助手。

12. 参考链接

您可以参考我们的Linux文本处理工具精通指南获取更多Linux文本处理工具的详细介绍。

13. 另请参阅

  • egrep(1), sed(1), getpid(2), getppid(2), getpgrp(2), getuid(2), geteuid(2), getgid(2), getegid(2), getgroups(2), printf(3), strftime(3), usleep(3)

  • 《The AWK Programming Language》,Alfred V. Aho、Brian W. Kernighan、Peter J. Weinberger 著,Addison-Wesley出版社,1988年。ISBN 0-201-07981-X。

  • 《GAWK: Effective AWK Programming》第5.1版,随gawk源代码一起分发。本文档的当前版本可在网上查阅:https://www.gnu.org/software/gawk/manual

  • GNU gettext 文档,可在网上查阅:https://www.gnu.org/software/gettext

14. 示例

打印并排序所有用户的登录名:

BEGIN     { FS = ":" }
     { print $1 | "sort" }

统计文件行数:

     { nlines++ }
END  { print nlines }

在每行前加上其在文件中的行号:

# 使用NR变量打印全局行号
awk '{print NR, $0}' awk.txt
# 输出示例:
# 1 nihao awk1 awk2 awk3
# 2 nihao awk4 awk5 awk6
# 3 nihao awk7 awk8 awk9

# 使用FNR变量打印每个文件的行号(多文件处理时有用)
awk '{print FNR, $0}' awk.txt

限定行号打印特定内容:

# 打印第1行的第1个和第3个字段
awk 'NR==1{print $1,$3}' awk.txt
# 输出示例:nihao awk2

# 打印第2行的行号和前两个字段(注意行号和$1之间没有空格)
awk 'NR==2{print NR $1,$2}' awk.txt
# 输出示例:2nihao awk4

# 打印第3行的行号和前两个字段
awk 'NR==3{print NR,$1,$2}' awk.txt
# 输出示例:3 nihao awk7

使用-F和-v FS选项指定分隔符:

# 默认使用空格分隔,整个行作为$1
awk '{print $1}' passwd.txt  # passwd.txt内容: root:x:0:0:root:/root:/bin/bash
# 输出示例:root:x:0:0:root:/root:/bin/bash

# 使用-F选项设置冒号为分隔符
awk -F: '{print $2}' passwd.txt
# 输出示例:x

# -F选项后可以有空格
awk -F : '{print $2}' passwd.txt
# 输出示例:x

# -F选项可以使用引号包围分隔符
awk -F ":" '{print $2}' passwd.txt
# 输出示例:x

# 使用-v FS选项设置字段分隔符(更灵活的方式)
awk -v FS=":" '{print $1,$7}' passwd.txt
# 输出示例:root /bin/bash

# 在输出中使用分隔符变量
awk -v FS=":" '{print $1 FS $7}' passwd.txt
# 输出示例:root:/bin/bash

# 字段连接(无分隔符)
awk -v FS=":" '{print $1 $7}' passwd.txt
# 输出示例:root/bin/bash

注意:-F和-v FS选项混用的优先级

# 当同时使用-F和-v FS选项时,-F选项的优先级更高
awk -v FS="/" -F ":" '{print $1,$2}' passwd.txt
# 输出示例:root x (使用了冒号作为分隔符)

# 单独使用-v FS选项
awk -v FS="/" '{print $1,$2}' passwd.txt
# 输出示例:root:x:0:0:root: root: (使用了斜杠作为分隔符)

当需要明确指定分隔符时,建议只使用一种方法,避免混用导致的混淆。如果需要在程序中动态修改分隔符,可以使用-v FS选项。

FS和OFS设置的注意事项及示例:

# 使用-v选项设置OFS(输出字段分隔符)
awk -F ":" -v OFS="~~" '{print $1,$7}' passwd.txt
# 输出示例:root~~/bin/bash

# 在BEGIN块中设置OFS和FS(注意:OFS值需要加引号)
awk -F ':' 'BEGIN{OFS="~~~";FS=":"}{print $1,$3,$7}' passwd.txt
# 输出示例:root~~~0~~~/bin/bash

# 错误示例:OFS值没有加引号会导致语法错误
# awk -F ':' 'BEGIN{OFS=~~~;FS=":"}{print $1,$3,$7}' passwd.txt
# 错误:syntax error

# 错误示例:Begin不是大写会导致设置无效
awk -F: 'Begin{OFS=",";FS=":"}{print $1,$3,$7}' passwd.txt
# 输出示例:root 0 /bin/bash (OFS设置无效,使用默认空格)

# 正确示例:BEGIN必须大写
awk -F: 'BEGIN{OFS=",";FS=":"}{print $1,$3,$7}' passwd.txt
# 输出示例:root,0,/bin/bash

# 可以只设置OFS,使用-F设置的FS
awk -F: 'BEGIN{OFS=","}{print $1,$3,$7}' passwd.txt
# 输出示例:root,0,/bin/bash

# 在BEGIN块中同时设置FS和OFS,不使用-F选项
awk 'BEGIN{OFS=",";FS=":"}{print $1,$3,$7}' passwd.txt
# 输出示例:root,0,/bin/bash

# 重要提示:FS必须是源文件中存在的字符,否则无法正确分割
# 下面示例使用空格作为分隔符,但passwd.txt实际使用冒号分隔
awk 'BEGIN{OFS=",";FS=" "}{print $1,$3,$7}' passwd.txt
# 输出示例:root:x:0:0:root:/root:/bin/bash,, (只识别到第一个字段)

注意事项:

  1. FS必须是源文件中实际存在的字符,否则无法正确分割字段
  2. BEGIN必须使用大写形式,否则会被视为普通模式而非特殊块
  3. -F选项后面可以跟双引号、单引号或无引号的分隔符
  4. 在BEGIN块中设置OFS时,值必须用引号括起来

RS(记录分隔符)的作用及示例:

# 生成1到7的序列,每个数字占一行
seq 7
# 输出示例:
# 1
# 2
# 3
# 4
# 5
# 6
# 7

# 设置RS为空字符串,将所有行作为一个记录处理
seq 7 | awk 'BEGIN{RS=""}{print $1,$2,$3,$4,$5,$6,$7}'
# 输出示例:1 2 3 4 5 6 7

RS的工作原理:

  • RS是awk中的记录分隔符变量,默认值是换行符(\n)
  • 当RS设置为空字符串(”“)时,awk会将连续的空行作为记录分隔符
  • 在没有空行的输入中(如seq命令的输出),所有行将被合并成一个记录
  • 此时,原有的每行内容会被视为该记录中的一个字段(由默认的FS分隔符分隔)
  • 这就解释了为什么垂直排列的数字会变成水平排列 - 它们从多个记录变成了一个记录中的多个字段

应用场景:

  • 处理段落格式的文本(每个段落由空行分隔)
  • 需要将多行内容合并为一行进行处理
  • 对文本块而非单行进行操作

OFS(输出字段分隔符)用法及列范围访问行为:

# 创建测试文件awk.txt
cat awk.txt
# 输出示例:
# nihao awk1 awk2 awk3 
# nihao awk4 awk5 awk6 
# nihao awk7 awk8 awk9 

# 使用-F":"(冒号作为输入分隔符,但文件中没有冒号),OFS="|"(管道作为输出分隔符)
# 打印行号和整行内容
awk -F ":" -v OFS="|" '{print NR,$0}' awk.txt
# 输出:
# 1|nihao awk1 awk2 awk3 
# 2|nihao awk4 awk5 awk6 
# 3|nihao awk7 awk8 awk9 

# 打印行号和$1(由于没有冒号,整行被视为$1)
awk -F ":" -v OFS="|" '{print NR,$1}' awk.txt
# 输出与上面相同,因为$1就是整行内容

# 打印行号和$2(由于没有冒号分隔,$2为空)
awk -F ":" -v OFS="|" '{print NR,$2}' awk.txt
# 输出:
# 1| 
# 2| 
# 3| 

重要原理解释:

  1. 关于超出列范围的访问行为
    • 当awk找不到指定的字段(如$2、$3等)时,会返回空字符串
    • 在上面的例子中,由于设置了-F":"但文件中没有冒号,每行都被视为一个整体字段$1
    • 因此访问$2、$3等字段时会返回空值
    • 行号NR总是正确显示的,而空字段会输出OFS分隔符和空内容
  2. 正确指定分隔符的情况
# 使用空格作为分隔符(默认也是空格)
awk -F " " -v OFS="|" '{print NR,$1,$2,$3,$4}' awk.txt
# 输出:
# 1|nihao|awk1|awk2|awk3 
# 2|nihao|awk4|awk5|awk6 
# 3|nihao|awk7|awk8|awk9 

OFS的用法总结:

  1. 使用-v选项设置OFS
    • 通过-v OFS="分隔符"可以在命令行中直接设置输出字段分隔符
    • 这比在BEGIN块中设置更灵活,特别是在需要从命令行参数获取分隔符时
  2. OFS的作用时机
    • OFS只在使用print命令输出多个字段时生效
    • 当打印$0(整行)时,OFS不会影响原始输入的格式
    • 只有在明确打印多个字段(如$1,$2,$3)时,OFS才会被用作字段间的分隔符
  3. OFS与FS的关系
    • FS(输入字段分隔符)决定如何分割输入行
    • OFS(输出字段分隔符)决定如何连接输出字段
    • 这两个设置是独立的,可以设置为不同的值
  4. 特殊情况处理
    • 当FS设置的分隔符在输入中不存在时,整行被视为一个字段
    • 访问不存在的字段(如超出范围的$N)会返回空字符串
    • 打印空字符串时,OFS仍然会被正确插入

printf格式化输出详解:

printf命令提供了更精确的输出格式化控制,比print命令更灵活。以下是各种格式化选项的示例和说明:

# 使用printf打印单个字段(字符串格式)
awk '{printf "%s\n",$1}' awk.txt
# 输出:
# nihao 
# nihao 
# nihao 

# 打印第二个字段(字符串格式)
awk '{printf "%s\n",$2}' awk.txt
# 输出:
# awk1 
# awk4 
# awk7 

# 注意:printf需要明确指定格式说明符和转义序列
# 下面的例子只有一个格式说明符%s,但提供了两个参数$1和$2
# 此时只有第一个参数$1被使用
awk '{printf "%s\n",$1,$2}' awk.txt
# 输出与第一个例子相同,只有$1被打印

printf格式说明符与多个参数:

# 使用两个格式说明符打印两个字段
awk '{printf "%s\n%s",$1,$2}' awk.txt
# 输出:
# nihao 
# awk1nihao 
# awk4nihao 
# (注意:没有最后一个换行符)

# 在一个格式字符串中组合多个字段,使用连字符连接
awk '{printf "%s-%s\n",$1,$2}' awk.txt
# 输出:
# nihao-awk1 
# nihao-awk4 
# nihao-awk7 

# 添加行号NR(整数格式)
awk '{printf "%d-%s-%s\n",NR,$1,$2}' awk.txt
# 输出:
# 1-nihao-awk1 
# 2-nihao-awk4 
# 3-nihao-awk7 

数值格式化选项:

# 使用浮点数格式打印行号
awk '{printf "%f-%s-%s\n",NR,$1,$2}' awk.txt
# 输出:
# 1.000000-nihao-awk1 
# 2.000000-nihao-awk4 
# 3.000000-nihao-awk7 

# 控制浮点数精度(4位宽度,2位小数)
awk '{printf "%4.2f-%s-%s\n",NR,$1,$2}' awk.txt
# 输出:
# 1.00-nihao-awk1 
# 2.00-nihao-awk4 
# 3.00-nihao-awk7 

# 使用左对齐格式(8个字符宽度,左对齐)
awk '{printf "%-8s%s\n",NR,$1}' awk.txt
# 输出:
# 1       nihao 
# 2       nihao 
# 3       nihao 

printf格式化输出总结:

  1. 格式说明符的作用
    • %s - 字符串格式
    • %d - 十进制整数格式
    • %f - 浮点数格式,可以指定精度如%4.2f
    • 格式说明符的数量必须与参数数量匹配,或参数数量是格式说明符数量的整数倍
  2. 宽度和对齐控制
    • 正数宽度(如%8s)- 右对齐,不足部分左侧补空格
    • 负数宽度(如%-8s)- 左对齐,不足部分右侧补空格
    • 数值宽度可以与精度结合(如%4.2f)- 总宽度4位,其中小数2位
  3. 与print的主要区别
    • printf不会自动添加换行符,需要显式使用\n
    • printf需要显式指定格式说明符
    • printf提供更精确的输出控制,适合需要严格格式的场景
  4. 常见转义序列
    • \n - 换行符
    • \t - 制表符
    • \\ - 反斜杠本身
    • \" - 双引号

print命令与文本标签结合使用:

print命令可以轻松地将文本标签与字段值结合输出,使结果更具可读性。以下是几个实用示例:

# 在字段前添加文本标签(英文标签)
awk '{print "first coloum",$1,"second coloum", $2}' awk.txt
# 输出:
# first coloum nihao second coloum awk1 
# first coloum nihao second coloum awk4 
# first coloum nihao second coloum awk7 

# 在标签后添加冒号增强可读性
awk '{print "first coloum:",$1,"second coloum:", $2}' awk.txt
# 输出:
# first coloum: nihao second coloum: awk1 
# first coloum: nihao second coloum: awk4 
# first coloum: nihao second coloum: awk7 

# 使用中文标签并结合END块显示统计信息
awk '{print "第一列:",$1,"第二列:", $2}END{printf "-----------\n行数总计:%2d\n",NR}' awk.txt
# 输出:
# 第一列: nihao 第二列: awk1 
# 第一列: nihao 第二列: awk4 
# 第一列: nihao 第二列: awk7 
# ----------- 
# 行数总计: 3 

print命令与文本标签结合使用总结:

  1. 基本语法:print命令可以直接将文本字符串与字段值混合输出,字符串需要用引号包围
  2. 标签格式化技巧:可以在标签后添加冒号、空格等符号增强可读性
  3. 与控制结构结合:可以在BEGIN或END块中结合print或printf命令显示统计信息
  4. 与printf配合使用:在需要格式化输出统计信息时,printf比print提供更精确的控制

printf多参数格式化输出的错误案例与正确用法:

当在printf中使用多个参数和格式说明符时,需要确保格式字符串和参数的正确匹配。以下是一个常见错误案例及其分析:

# 错误示例:格式字符串和参数不匹配
awk '{print "第一列:",$1,"第二列:", $2}END{printf "-----------\n行数总计:%2d\n","\n列数总计:"%2d,NF}' awk.txt
# 输出(只有行数信息,没有列数信息):
# 第一列: nihao 第二列: awk1 
# 第一列: nihao 第二列: awk4 
# 第一列: nihao 第二列: awk7 
# ----------- 
# 行数总计: 3 

错误原因分析:

  • 格式字符串被错误地分割成了多个部分:"-----------\n行数总计:%2d\n""\n列数总计:"%2d
  • 格式说明符和参数的数量及顺序不匹配
  • 字符串和格式说明符的拼接方式不正确

正确用法示例:

# 正确示例1:在一个格式字符串中包含所有格式说明符
awk '{print "第一列:",$1,"第二列:", $2}END{printf "-----------\n行数总计:%2d\n列数总计:%2d\n",NR,NF}' awk.txt
# 输出:
# 第一列: nihao 第二列: awk1 
# 第一列: nihao 第二列: awk4 
# 第一列: nihao 第二列: awk7 
# ----------- 
# 行数总计: 3 
# 列数总计: 2 

# 正确示例2:使用多个printf语句分别格式化输出
awk '{print "第一列:",$1,"第二列:", $2}END{printf "-----------\n"; printf "行数总计:%2d\n",NR; printf "列数总计:%2d\n",NF}' awk.txt
# 输出:
# 第一列: nihao 第二列: awk1 
# 第一列: nihao 第二列: awk4 
# 第一列: nihao 第二列: awk7 
# ----------- 
# 行数总计: 3 
# 列数总计: 2 

# 正确示例3:在数据处理块中计算每行的列数,并在END块中显示
awk '{print "第一列:",$1,"第二列:", $2; max_nf=(NF>max_nf)?NF:max_nf}END{printf "-----------\n行数总计:%2d\n最大列数:%2d\n",NR,max_nf}' awk.txt
# 输出:
# 第一列: nihao 第二列: awk1 
# 第一列: nihao 第二列: awk4 
# 第一列: nihao 第二列: awk7 
# ----------- 
# 行数总计: 3 
# 最大列数: 2 

多参数格式化输出注意事项:

  1. 格式字符串完整性:所有格式说明符必须包含在同一个格式字符串中
  2. 参数数量匹配:格式说明符的数量必须与后续提供的参数数量匹配
  3. 参数顺序对应:参数的顺序必须与格式说明符的顺序一一对应
  4. 字符串连接:在格式字符串内部可以使用转义序列和普通字符进行连接,而不是在格式字符串外部
  5. 多行输出:可以使用\n在格式字符串中创建换行,或使用多个printf语句实现复杂的格式化输出

NR变量在不同阶段的值与print/printf处理差异:

NR(Number of Record)变量在awk命令执行的不同阶段具有不同的值,同时print和printf命令处理格式字符串的方式也有明显差异。以下是详细说明和示例:

# 示例1:NR变量在BEGIN块、命令块和END块中的值
# 以及print与printf处理格式字符串的差异
awk -F: 'BEGIN{printf "BEGIN中的NR值:%2d\n",NR}NR==11{print "命令中的NR值:%2d\n",NR}END{printf "END中的NR值:%2d\n",NR}' /etc/passwd
# 输出:
# BEGIN中的NR值: 0  # BEGIN块中NR为0
# 命令中的NR值:%2d   # print命令直接打印格式字符串
#  11               # print命令单独打印NR的值
# END中的NR值:50    # END块中NR为总记录数

# 示例2:使用printf正确格式化输出NR值
awk -F: 'BEGIN{printf "BEGIN中的NR值:%2d\n",NR}NR==11{printf "命令中的NR值:%2d\n",NR}END{printf "END中的NR值:%2d\n",NR}' /etc/passwd
# 输出:
# BEGIN中的NR值: 0  # BEGIN块中NR为0
# 命令中的NR值:11   # printf正确解析格式说明符
# END中的NR值:50    # END块中NR为总记录数

关键要点说明:

  1. NR变量在不同阶段的值
    • BEGIN块:NR初始值为0(在处理任何输入行之前)
    • 命令块(main块):NR从1开始递增,每处理一行输入,NR增加1
    • END块:NR保持为处理的总行数(最终值)
  2. print与printf处理格式字符串的差异
    • print命令:不会解析格式说明符(如%d),而是直接将其作为普通文本输出 它会将逗号分隔的参数用OFS(默认空格)连接后输出
    • printf命令:会正确解析格式说明符,并将对应的参数按照指定格式输出 需要显式添加\n来实现换行
  3. awk命令执行的优先级顺序
    • BEGIN块:最先执行,且仅执行一次
    • 命令块(main块):对每一行输入执行一次
    • END块:最后执行,且仅执行一次
  4. 格式字符串使用建议
    • 当需要格式化输出(如数字格式化、宽度控制等)时,必须使用printf命令
    • 当只需要简单输出内容时,可以使用更简洁的print命令
    • 在同一个程序中,可以根据需要灵活组合使用print和printf
  5. 常见错误避免
    • 不要在print命令中使用格式说明符,它们会被当作普通文本
    • 确保printf命令的格式说明符数量与参数数量匹配
    • 记住在printf中显式添加换行符\n

print与printf的详细对比及适用场景:

print和printf命令各有特点,适用于不同的输出场景。以下是它们的详细对比和使用示例:

# 示例1:print自动添加换行符,printf需要显式添加
# 使用print命令(自动添加换行)
awk '{print $1"\t"$2}' awk.txt
# 输出(每行自动换行):
# nihao   awk1
# nihao   awk4
# nihao   awk7

# 使用printf命令(需要显式添加换行符)
awk '{printf $1"\t"$2}' awk.txt
# 输出(所有内容在一行):
# nihao   awk1nihao       awk4nihao       awk7

# 使用printf命令(显式添加换行符)
awk '{printf $1"\t"$2"\n"}' awk.txt
# 输出(每行换行,与print类似):
# nihao   awk1
# nihao   awk4
# nihao   awk7

关键区别与适用场景:

  1. 换行处理
    • print命令:自动在输出末尾添加换行符\n
    • printf命令:不会自动添加换行符,需要显式使用\n
    • 适用场景:需要自动换行时使用print,需要精确控制换行位置时使用printf
  2. 参数处理
    • print命令:使用逗号分隔参数时,会在参数之间插入OFS(默认空格)
    • printf命令:需要使用格式说明符或字符串连接来控制参数之间的分隔
    • 适用场景:简单分隔输出用print,复杂格式化输出用printf
  3. 格式化能力
    • print命令:格式化能力有限,主要依靠字符串连接和OFS
    • printf命令:提供强大的格式化控制(宽度、精度、对齐方式等)
    • 适用场景:需要格式化数字、对齐文本等精确控制时使用printf

表格格式化输出综合案例:

以下是一个完整的表格格式化输出案例,展示如何使用awk的BEGIN块、主命令块和END块协同工作,创建格式化的表格输出:

# 使用awk格式化输出/etc/passwd文件的用户名和Shell类型,创建美观的表格
head /etc/passwd | awk -F":" 'BEGIN{printf "------------------------\n%-6s|%12s|\n","用户名","Shell 类型"}{printf "------------------------\n%-9s|%14s|\n",$1,$7}END{printf "总行数:%2d\n",NR}'

输出结果:

------------------------ 
用户名   |    Shell 类型|
------------------------ 
root     |     /bin/bash|
------------------------ 
bin      | /sbin/nologin|
------------------------ 
daemon   | /sbin/nologin|
------------------------ 
adm      | /sbin/nologin|
------------------------ 
lp       | /sbin/nologin|
------------------------ 
sync     |     /bin/sync|
------------------------ 
shutdown |/sbin/shutdown|
------------------------ 
halt     |    /sbin/halt|
------------------------ 
mail     | /sbin/nologin|
------------------------ 
operator | /sbin/nologin|
总行数:10 

案例解析:

  1. 字段分隔符设置:使用-F":"将字段分隔符设置为冒号,适配/etc/passwd文件格式

  2. BEGIN块表头定义
    • 输出表格顶部分隔线
    • 使用左对齐格式%-6s%12s定义表头”用户名”和”Shell 类型”
    • 表头与数据列使用竖线|分隔,增强可读性
  3. 主命令块数据格式化
    • 每行数据前输出分隔线
    • 使用%-9s(左对齐9字符宽度)格式化用户名
    • 使用%14s(右对齐14字符宽度)格式化Shell路径
    • 保持表格结构的一致性和美观性
  4. END块统计信息
    • 使用NR变量获取总行数
    • 输出简洁的统计信息”总行数:10”
  5. 格式化技巧
    • 使用负号前缀(如%-6s)实现左对齐
    • 精确控制字段宽度确保表格对齐
    • 使用分隔线和边框字符增强表格可视化效果
    • 结合BEGIN/主命令/END块实现完整的表格结构
  6. 表头和表尾处理
    • 通常在BEGIN块中用printf创建表头(可精确控制格式)
    • 在main块中根据需要使用print或printf输出数据行
    • 在END块中用printf创建表尾和统计信息

awk语句分隔符使用规则:

在awk中,语句分隔符的使用有特定规则,以下是详细说明:

# 示例2:分号和逗号作为分隔符的区别
# 使用分号分隔多个语句
awk 'BEGIN{printf "表头\n"; total=0}{print $1,$2; total++}END{printf "总行数:%d\n",total}' awk.txt
# 输出:
# 表头
# nihao awk1
# nihao awk4
# nihao awk7
# 总行数:3

# 使用逗号分隔print的参数(不是语句分隔符)
awk '{print "字段1:",$1,"字段2:",$2}' awk.txt
# 输出(逗号被OFS替换):
# 字段1: nihao 字段2: awk1
# 字段1: nihao 字段2: awk4
# 字段1: nihao 字段2: awk7

语句分隔符规则:

  1. 分号(;)
    • 用于分隔同一代码块中的多个语句
    • 在BEGIN、main和END块中,多条语句之间必须用分号分隔
    • 分号可以放在行尾,也可以在同一行分隔多个语句
    • 最后一条语句的分号通常可以省略,但添加分号是良好的编程习惯
  2. 逗号(,)
    • 在print命令中,逗号用于分隔要输出的多个参数
    • 逗号在输出时会被替换为OFS(默认是空格)
    • 逗号不是语句分隔符,不能用于分隔不同的命令或语句
  3. 花括号内的语句分隔
    • 同一花括号内的多个语句必须用分号分隔
    • 即使语句分布在不同行,分号仍然是必需的
    • 空语句(多余的分号)通常会被忽略,但应避免使用

自定义变量vs内置变量NR:

在awk中,可以使用自定义变量来跟踪计数或存储值,也可以直接使用内置变量NR。以下是它们的对比:

# 示例3:使用自定义变量total vs 直接使用NR
# 使用自定义变量total
awk 'BEGIN{total=0}{print $1,$2; total++}END{printf "总行数:%2d\n",total}' awk.txt
# 输出:
# nihao awk1
# nihao awk4
# nihao awk7
# 总行数: 3

# 直接使用NR
awk '{print $1,$2}END{printf "总行数:%2d\n",NR}' awk.txt
# 输出(结果相同):
# nihao awk1
# nihao awk4
# nihao awk7
# 总行数: 3

自定义变量与内置变量NR的对比:

  1. 功能对比
    • NR:awk内置变量,自动跟踪当前处理的记录(行)号
    • 自定义变量:需要手动初始化和更新,但提供更大的灵活性
  2. 适用场景
    • 使用NR的场景
      • 简单的行计数需求
      • 需要跟踪原始输入的行号
      • 代码简洁性要求高的场景
    • 使用自定义变量的场景
      • 需要特定的计数逻辑(如条件计数)
      • 需要在处理过程中修改计数值
      • 需要多个计数器跟踪不同的统计维度
      • 增强代码可读性(如使用有意义的变量名)
  3. 灵活性对比
    • NR是只读变量(虽然可以修改,但不建议),值由awk自动维护
    • 自定义变量可以完全控制初始化值、更新逻辑和作用域
    • 对于复杂的统计需求,自定义变量提供更大的灵活性
  4. 性能考虑
    • NR是内置变量,使用它通常不会增加额外的性能开销
    • 自定义变量需要额外的内存和赋值操作,但对于大多数场景影响微小
    • 在性能关键的场景中,简单的行计数直接使用NR可能更高效
  5. 基本语法
    • print "文本标签", $字段 - 使用逗号分隔文本和字段
    • print会自动在逗号分隔的参数之间插入OFS(默认是空格)
  6. 标签格式化技巧
    • 在标签后添加冒号、空格等标点符号增强可读性
    • 支持中英文等多语言标签
    • 可以在一行中组合多个文本标签和字段
  7. 与控制结构结合
    • 可以在main块中使用print输出处理结果
    • 在END块中可以使用printf进行格式化统计输出
    • NF变量表示当前记录的字段数
  8. 与printf的配合
    • 通常在main块中使用print进行简单输出
    • 在END块中使用printf进行精确的统计信息格式化
    • 两者结合可以实现灵活的输出需求

提取URL中的域名信息并统计:

# 假设domain.txt文件包含URL列表
cat domain.txt
# 输出示例:
# http://www.example.org/index.html
# http://www.example.org/1.html
# http://api.example.org/index.html
# http://upload.example.org/index.html
# http://img.example.org/3.html
# http://search.example.org/2.html

# 使用正则表达式作为分隔符提取域名(/+表示一个或多个斜杠)
awk -F "/+" '{print $2}' domain.txt
# 输出示例:
# www.example.org
# www.example.org
# api.example.org
# upload.example.org
# img.example.org
# search.example.org

# 使用单个斜杠作为分隔符(需要使用$3获取域名)
awk -F "/" '{print $3}' domain.txt
# 输出示例同上

# 统计域名出现次数(结合awk和uniq)
awk -F "/" '{print $3}' domain.txt | uniq -c
# 输出示例:
# 2 www.example.org
# 1 api.example.org
# 1 upload.example.org
# 1 img.example.org
# 1 search.example.org

# 使用字符类正则表达式作为分隔符并添加行号
# 在这个表达式中:
# 1. [] 是字符类,表示匹配括号内的任意一个字符
# 2. / 是要匹配的字符(斜杠)
# 3. + 是量词,表示匹配前面的字符一个或多个
# 所以 [/]+ 表示匹配一个或多个连续的斜杠
# 直接使用 "/+" 也可以,因为 / 不是正则表达式中的特殊字符,不需要转义
awk -F "[/]+" '{print NR,$2}' domain.txt
# 输出示例:
# 1 www.example.org
# 2 www.example.org
# 3 api.example.org
# 4 upload.example.org
# 5 img.example.org
# 6 search.example.org

这个示例展示了如何使用正则表达式作为分隔符从URL中提取域名,并结合其他命令进行统计分析。

提取/etc/fstab中的UUID挂载信息:

# 首先使用grep过滤出包含UUID的行,然后用awk提取UUID和文件系统类型
grep "^UUID" /etc/fstab | awk '{print $1, $3}'
# 输出示例:UUID=4e0fed15-9f25-41c0-8a61-fd528964dd3f xfs

# 提取所有挂载点及其文件系统类型
awk '!/^#/ && $0 {print $2, $3}' /etc/fstab
# 输出示例:/	xfs
#          /boot	xfs
#          /home	xfs
#          none	swap

# 格式化输出fstab关键信息,使用制表符分隔
awk '!/^#/ && $0 {printf "挂载点: %-20s 文件系统: %-10s 选项: %s\n", $2, $3, $4}' /etc/fstab

这个示例展示了如何结合grep和awk命令高效地处理系统配置文件,提取关键信息进行分析或报告。

{ print FNR, $0 }

连接文件并添加行号(一个变体):

{ print NR, $0 }

为特定数据行运行外部命令:

tail -f access_log |
awk '/myhome.html/ { system("nmap " $1 ">> logdir/myhome.html") }'

15. 致谢

Brian Kernighan在测试和调试过程中提供了宝贵的帮助。我们向他表示感谢。

16. 复制许可

版权所有 © 1989, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2001, 2002, 2003, 2004, 2005, 2007, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020,自由软件基金会。

允许制作和分发本手册页的逐字副本,前提是保留版权声明和本许可声明的所有副本。

允许在逐字复制的条件下复制和分发本手册页的修改版本,前提是整个衍生作品在与本声明相同的许可声明下分发。

允许将本手册页翻译成另一种语言进行复制和分发,条件同上,但本许可声明可以使用基金会批准的翻译版本。

文档信息

Search

    Table of Contents