AWK 实战总结2 -- 多行 record 以及 AWK 与系统的交互

AWK 实战总结2 -- 多行 record 以及 AWK 与系统的交互

[TOC]

本文介绍一种在开发高级编程语言时可以应用 AWK 语言作为工具的案例, 并顺便介绍 AWK 处理多行 record 时的用法, 浅析 AWK 与系统之间的交互通道.

场景介绍

在使用 C++ 编程时, 接口变化时, 我们可能需要一行行地去核对相关接口变量的变动, 不仅浪费时间, 也很容易眼睛看花犯错误. 如果能够通过数行代码来自动化替换, 方便并且容易排查问题. 但大多数情况下编辑器会帮我们解决一大部分内容, 例如补全功能. 或者从一开始设计接口时就不应该设计成松散的列举, 而是使用类等封装结构减少人眼校验的问题. 但是还是有一些 corner case. 我这里举出 2 个实际遇到的例子.

  1. ROS msg 的写入.
  2. 全局变量的初始化.

例如, 接口文件 source.h 内容如下, 其中接口变量 int i1 变为了 int i11.

1
2
3
4
5
6
7
8
#include <string>

using namespace std;

// interface changed, double d3 => double d33.
extern double d1; // meaning fo this variable
extern double d2;
extern double d33; // changed from double d3

用到这些变量的地方有两处需要更新.

target.cpp 中需要在每隔一段时间对接口变量进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "source.h"

using namespace std;

void init_extern()
{
// ANC
d1 = 0.0;
d2 = 0.0;
d3 = 0.0;
// ANC
}

void next_func()
{
//...
}

我们需要把这些接口变量放入 ROSDebugMsg.msg 文件中, 方便 dump 成 rosbag 排查接口变量在运行过程中的变化.

1
2
3
float64 d1
float64 d2
float64 d3

这里只用了 3 个变量, 如果这些变量有数十个, 几百个, 甚至更多呢? 仅仅靠人眼每次增删改是不现实的. 但是这种情况下, 编辑器没有什么办法帮我们. 因为要在运行期前完成功能, 也无法使用 C++ 程序自己去动态解析这些变化(基于模板元编程的反射机制). 再加上这种变量的出现大概率本来就是开发调试期间的临时做法(全局变量毕竟不是最终形态), 因此更灵活短小的脚本语言更方便使用. 这里采用 AWK 语言.

前提: 为了不让问题过于复杂化, 这里把所有的接口变量都设置为 double 类型. 对于更多类型可以考虑应用 AWK 的关联数组.

多行 record 的处理

AWK 的默认 RS\n, 即换行符. 也就是每个 record 就是一行. 但有时候我们需要将多行的文本当作 record 对象, 如何做呢?
可以参考 gwak 官网给出的建议.

这里整理如下:

  1. 修改 RS
    例如在 target.cpp 中预先埋下的锚点 // ANC 作为分隔符.

  2. 使用 ranger 匹配, 使用正则表达式匹配特定 2 行间的所有内容, 不需要预先埋点. 这种做法会把前后的符合正则表达式的两个语句也包含进来, 处理起来可能没那么规整.

1
awk '/void init_extern()/,/void next_func()/ {print}' target.cpp
  1. 对每行埋点, 这样只需要正常匹配, 把所有符合锚的行综合在一起变成多行. 这种思路在所需行不是整块分布而是零零散散分布时很有效. 但是需要较麻烦地添加看起来可能意义不明的注释. PS. 全局变量一般会在链接后初始化值为 0, 此处的初始化为 0 只是为了演示.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "source.h"

using namespace std;

void init_extern()
{
d1 = 0.0;// ANC
d2 = 0.0;// ANC
d3 = 0.0;// ANC
}

void next_func()
{
//...
}

本文采取第一种方式.

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

anchor_string="// ANC"
source_file="./source.h"
cpp_file="./target.cpp"
msg_file="./ROSDebugMsg.msg"

src_temp=$(mktemp -p .) || exit 0
target1_cpp=$(mktemp -p .) || exit 0
ROSDebugMsg1_msg=$(mktemp -p .) || exit 0
awk '/extern/ {print $3}' ${source_file} | awk -F";" '{print "double "$1" = 0.0;"}' >${src_temp}

cat ${src_temp}

echo "---------------"

awk 'BEGIN{RS="'"$anchor_string"'"}
NR==2{
$0="";
print "'"${anchor_string}"'";
while((getline line < "./'${src_temp}'") > 0)
{print line}
print "'"${anchor_string}"'";
close("./'${src_temp}'")
}
{print}' target.cpp >${target1_cpp}

cat ${target1_cpp}

echo "---------------"

awk '/extern/ {print $3}' ${source_file} | awk -F";" '
{
temp=$1
command = ("if ! grep -w " $1 " " 'msg_file' " ;then echo " $1 " ; fi" )
while((command | getline var) > 0)
{
if(temp == var)
print "float64 "var;
else
print var;
}
close(command)
}
' >${ROSDebugMsg1_msg}

cat ${ROSDebugMsg1_msg}

echo "---------------"

mv ${target1_cpp} target.cpp

mv ${ROSDebugMsg1_msg} ROSDebugMsg.msg

rm ${src_temp}

代码里没有注释, 这里稍微写一下.

  1. mktemp: 创建临时文件, 会更加安全.
  2. RS="'"$anchor_string"'": 将 bash 里的变量传给 AWK, 本来可以通过 "'$var'" 的形式, 但是本例中字符串变量里有空格, 因此需要再套一层 "".
  3. $0="";: 整块替换.
  4. print "'"${anchor_string}"'";: 2 个此语句, 对替换后的块前后再次加入锚点, 方便下次处理.
  5. while((getline line < "./'${src_temp}'") > 0): 逐行读入.
  6. close("./'${src_temp}'"): 关闭文件, 养成良好习惯.
  7. command = ("if ! grep -w " $1 " " "ROSDebugMsg.msg" " ;then echo " $1 " ; fi" )while((command | getline var) > 0): 此处应用了 AWK 与 Unix 其他命令交互的 getline 模式. 将 source.h 中的每行变量名取出, 并且通过 grep -w 精确匹配到 ROSDebugMsg.msg 里, 将匹配的变量直接从 grep 的输出中拿出来(已经是 float64 var 的格式了)到 var 中, 而不匹配的变量则会因为 if 的 false 进入 echo $1 的分支中将 source.h 中的变量名打印出来. 这是故意用复杂的思路解决问题, 更简单的做法是删除 ROSDebugMsg.msg 全部然后重新写入.
    PS. 除了使用 if-else 判断外, 对 grep 匹配成功与否的输出还可以使用条件语句: grep -w $1 ROSDebugMsg.msg || echo $1, 更加简洁.
  8. if(temp == var) print "float64 "var;: 由于之前已经将 source.h 中的变量名都保存下来了, 如果没有匹配到, grep -w 的输出 var 应该与 temp 相同, 因此手动加上 float64 前缀, 打印出来. 如果匹配成功 vartemp 不同, 一个是变量名, 例如 d33 而另一个是 float64 d3.

延伸

除了使用上面的 command | getline 方式进行 AWK 与系统其他命令之间的交互以外, 还可以使用 gawk 下的 |& operator 进行如下模式的操作:

1
2
print "some query" |& "db_server"
"db_server" |& getline

AWK 通过 print 将参数传递给系统命令 "db_server", 系统命令将输出结果再通过 getline 传递回 AWK. 我尝试使用 |& 进行实现同样的功能, 但是发现 grep 命令的表现一直报错, 说命令使用格式错误, 但是我用 wc echo 等命令测试正常, 因为不是 AWK 标准里的, 后面有精力再研究一下 gawk 的这项功能的具体原理.

getline 作为 AWK 接收输入的窗口总共有如下使用模式:

$$ \begin{array}{llc} \text { Variant } & \text { Effect } & \text { awk / gawk } \\ \text { getline } & \text { Sets \$0, NF, FNR, NR, and RT } & \text { awk } \\ \text { getline var } & \text { Sets var, FNR, NR, and RT } & \text { awk } \\ \text { getline < file } & \text { Sets \$0, NF, and RT } & \text { awk } \\ \text { getline var < file } & \text { Sets var and RT } & \text { awk } \\ \text { command | getline } & \text { Sets \$0, NF, and RT } & \text { awk } \\ \text { command | getline var } & \text { Sets var and RT } & \text { awk } \\ \text { command | \& getline } & \text { Sets \$0, NF, and RT } & \text { gawk } \\ \text { command | \& getline var } & \text { Sets var and RT } & \text { gawk } \end{array} $$

最后一列标记为 gawk 的为 gawk 拓展的功能, 不一定符合 POSIX 标准.

systemgetline 区别

最后稍微提及一下 system getline 的区别.

system 也可以作为调用系统里其他命令的接口, 例子如下:

1
2
3
END {
system("date | mail -s 'awk run done' root")
}

getline 在此方面不同的是:

  1. system 需要系统支持, 不是所有的系统都能运行 system.
  2. system 只会返回 command’s exit status, 而不是命令本身的输出结果. 因此只适合做开环的调用, “is useful for running large self-contained programs”.

更多信息可以参考 gawk 的 manual.

AWK 实战总结2 -- 多行 record 以及 AWK 与系统的交互

https://www.chuxin911.com/awk_practice_2_20220524/

作者

cx

发布于

2022-05-24

更新于

2023-01-10

许可协议