
如果您瞥了一眼标题并想:“我不在乎——我不写 C 代码”,那么请稍等。虽然 C 确实有一个预处理器,而且众所周知,你可以用它做奇怪的事情,并且根据你的观点,可怕或美妙的事情,但实际上还有其他选项,你不必将它们与 C 一起使用程序。实际上,您可以将 C 预处理器用于几乎任何类型的文本文件。而且它并不是唯一可以以这种方式滥用的预处理器。例如,m4 预处理器非常复杂,大大未得到充分利用,并且可以处理 C 源代码或您想要发送给它的任何其他内容。
定义
我将预处理器定义为一个程序,它将其输入文件转换为输出文件,对可能嵌入在文件本身中的命令做出反应。大多数情况下,该输出会被发送到其他程序来完成“真正的”工作。这涵盖了 C 预处理器cpp
。它还涵盖了sed
等内容。老实说,您可以使用 C、 awk
、Python、Perl 或任何其他编程语言轻松创建自定义预处理器。您可以将许多其他标准程序视为预处理器,例如tr
。然而,最强大的功能之一是预处理复杂的输入文件,称为 m4。由于某种原因——也许是因为它的复杂性——你在野外看不到太多 m4。
什么预处理器?
如果您只使用过现代 C 编译器,您可能想知道预处理器在哪里。据您所知,普通系统现在只需一次即可完成整个编译。但是,如果您愿意,您的编译器应该提供一个cpp
可执行文件,在外部执行预处理器逻辑。对于 gcc (以及许多其他编译器),预处理器被命名为 — 毫不奇怪 — cpp
。预处理器有四个主要任务:
- 将一个字符串替换为另一个字符串,包括看起来像函数调用的“宏”。
- 评估表达式并根据表达式的值包含部分输入或排除它们。
- 删除评论。
- 读入其他文件。
当然,通常情况下,输入是 C 源代码,输出则发送给编译器,但不一定是这样。
一个简单的例子
假设您有某种配置文件,其中包含最初为英文的消息。该文件如下所示:
消息1:早上好 消息2:晚安 message3:猫是白色的
我们希望对其进行安排,以便我们可以轻松更改消息并构建新的配置文件。有多种方法可以做到这一点,每种方法都有一些优点和缺点。
假设您有一个名为langs
的文件:
#定义英语0 #define 西班牙语 1
显然,您可以在此处添加更多语言,并且数字是任意的,只要它们是唯一的即可。
现在,我们可以为最终的配置文件创建一个模板:
#include“语言” #ifndef 朗格 #define LANG 英语 #万一 #包括“xlat” 消息 1:GOOD_MORNING 消息2:晚安 消息3:猫(白色)
关于此文件有几点需要注意。首先,它包括我们的语言定义文件。然后它将LANG
定义为这些符号之一,除非其他东西已经定义了它。我们很快就会看到这可能是什么,但假设现在将LANG
设置为ENGLISH
。
xlat
的包含会使用我们选择的任何语言的正确字符串填充诸如GOODMORNING
之类的标签。 xlat
样子如下:
#如果语言==英语 #define WHITE 白色 #define GOOD_MORNING 早上好 #define GOOD_NIGHT 早上好 #define CAT(clr) 猫是 clr #万一 #if LANG==西班牙语 #define 白色白色 #define GOOD_MORNING 布宜诺斯艾利斯 #define GOOD_NIGHT 布埃纳斯诺奇斯 #define CAT(clr) El gato es clr #万一
请注意,早上好消息中包含 Unicode 字符。这是使用此类工具的一个小问题。编码将以C 风格转义字符的形式出现。根据您要使用输出的用途,这可能是可接受的,也可能是不可接受的。事实上,预处理器为编译器做了一些我们可能想要抑制的事情。
如果你只是运行:
.cpp模板
你得到:
# 0“模板” # 0 “<内置>” # 0 "<命令行>" # 1 “/usr/include/stdc-predef.h” 1 3 4 # 0 "<命令行>" 2 #1“模板” #1“语言”1 #2“模板”2 #1“xlat”1 #8“模板”2 消息1:早上好 消息2:晚安 message3:猫是白色的
我们想要的是在底部,确实如此,但是有很多东西可以帮助编译器生成错误消息和其他东西。
诀窍是在命令行上添加一些选项:
cpp -udef -P 模板
这些选项适用于 gcc 的预处理器。如果您使用其他东西,您可能必须做出自己的决定。
定制
如果您想要西班牙语版本,您只需编辑该文件即可。但您也可以告诉预处理器强制使用 LANG 符号,并且由于模板不会重新定义它,因此您将获得您选择的语言:
cpp -udef -P -D LANG=西班牙语模板
正如我提到的,在此之后 Unicode 字符会看起来很有趣,具体取决于您如何看待它。
其他方式
这不是本示例中使用预处理器的唯一方法。您可以检测语言,然后包含不同的文件(英语或西班牙语)以获得相同的结果。例如,这将具有许多小的独立文件的优点,您可以将其发送给不同的翻译人员。
可能还有许多其他方法可以做到这一点。预处理器就像一个多功能工具。几乎任何事情都有很多方法可以做。
类固醇预处理器
如果您真的想熟悉预处理器,请尝试m4
。它在思想上与 C 预处理器类似,但具有许多超能力。它不是 C 所特有的,因此您不需要做太多事情就可以让它处理您的文件。与 C 预处理器不同, m4
不关心行。例如,考虑以下输入:
你好! 定义(HACKADAY,1) 测试我们的宏: 黑客日 结束
如果您通过m4
运行它,您会注意到 Hello 和“Testing”行之间有一个奇怪的空行。为什么?因为宏定义只消耗到右括号为止的字符。其他所有内容仍然在文件中,包括末尾的换行符。如果您在定义后输入一些文本,那么没有问题,它将显示在输出中。
如果您想忽略该行的其余部分,可以使用dnl
(删除到新行),如下所示:
定义(HACKADAY,1)dnl
m4 中的参数使用美元符号表示法,与 shell 非常相似。引用也很奇怪,因为您使用反引号来打开并使用撇号来关闭。像这样:
定义(HACKADAY,`eval(10**$1)')
正如您所料,这允许您输入 HACKADAY(2) 并得到 100 作为结果 — 双星号表示求幂。
愉快的消遣
m4
的最佳功能之一是它具有至少十种不同的输出流。默认为流 0,其余的编号为 1 到 9。您可以轻松写入任何流,或写入超出范围的流(如 -1)以丢弃输入。最后,输出流按顺序放在一起。例如,假设您可以有一个宏来将项目添加到报表中。该报告有标题、正文和总计列。您可以将所有标头代码放入第一个流(或 m4 所说的“转移”)中。然后将主体代码放入转移2,将总代码放入转移3。
最后,生成的程序将包含所有标题,然后是所有正文项,最后是总计,您可以按照您认为方便的任何顺序编写它们。如果您想丢弃文本,则应转向负文件号。一些m4
程序(包括 GNU 程序)允许比标准数量更多的转移。
作为一个简单的示例,请考虑以下脚本:
dnl 这些评论将被丢弃 dnl 首先,我们要转向#1 dnl 然后我们将打印每个单词以及计数 dnl 递增计数 (_c) dnl 最后,我们将切换回 0 并输出计数 dnl 这样,“报告”的标题就会有计数 dnl 后跟我们想要计数的单词 转移(1)dnl 定义(_c,0)dnl 定义(WC,` 定义(`_c',incr(_c))dnl _c: $1')dnl 厕所(你好) 厕所(那里) WC(黑客日) 世界杯(2024) 转移(0)dnl _c 单词列表:
请注意,以dnl
开头的行本质上是注释。其余的内容很神秘,但其想法是定义一个宏来输出带有序列号的单词列表。标头包含总计数,当然,直到最后我们才知道。但由于标头放置在转移 0 中,其余部分放置在转移 1 中,因此一切都按正确的顺序出现。
关于m4
内容太多,无法在一篇文章中涵盖,但您可以自己阅读更多相关内容。老实说,如果您确实需要m4
的强大功能,也许您应该考虑 awk 或 Python。不过,您可能必须重新创建自己版本的转移系统,因此如果您确实需要该功能,也许m4
可以提供一些东西。
另一方面,也许尝试awk 。或者以糟糕的方式混合 awk、shell 脚本和 C 处理器。
原文: https://hackaday.com/2023/12/28/linux-fu-preprocessing-beyond-code/