TL;DR
将文件名分配给fname 并在shell 中运行以下命令。在整个过程中,fname 被假定为包含要处理的源文件的 sh 变量。
fname=file_to_process.c ;
grep -G '^#include' <./"$fname" ;
grep -Gv '^#include[ ]*<' <./"$fname" | gcc -x c - -E -o - $(grep -G '^#include[ ]*<' <./"$fname" | xargs -I {} -- expr "{}" : '#include[ ]*<[ ]*\(.*\)[ ]*>' | xargs -I {} printf '-imacros %s ' "{}" ) | grep -Ev '^([ ]*|#.*)$'
除了gcc 之外的所有内容都是纯 POSIX sh,没有 bashisms 或不可移植的选项。第一个 grep 用于输出 #include 指令。
GCC 的 -imacros
来自 gcc 文档:
-imacros file:和'-include'完全一样,除了扫描文件产生的任何输出都是
扔掉了。它定义的宏保持定义。这使您可以获取所有
来自标头的宏,不处理其声明
那么,-include 到底是什么?
-include file: 处理文件好像#include "file" 出现在主文件的第一行
源文件。但是,搜索文件的第一个目录是预处理器的
工作目录而不是包含主源文件的目录。如果
在那里找不到,在#include“...”的其余部分中搜索它
搜索链正常。
简单地说,因为你不能在-include 指令中使用<> 或"",所以它的行为就像#include <file> 在源代码中一样。
第一种方法
ANSI C 保证 assert 是宏,所以它非常适合简单的测试:
printf 'int main(){\nassert(1);\nreturn 0;}\n' | gcc -x c -E - -imacros assert.h。
选项-x c 和- 告诉gcc 从stdin 读取源文件并且使用的语言是C。输出不包含来自assert.h 的任何声明,但仍然有混乱,可以用grep:
printf 'int main(){\nassert(1);\nreturn 0;}\n' | gcc -x c -E - -imacros assert.h | grep -Ev '^([ ]*|#.*)$'
注意:一般情况下,gcc 不会扩展打算作为宏的标记,但缺少定义。尽管如此,assert 恰好完全展开:__extension__ 是编译器选项,__assert_fail 是函数,__PRETTY_FUNCTION__ 是字符串文字。
自动化
以前的方法可行,但可能很乏味;
-
每个#include都需要手动从文件中删除,并且
-
它必须作为-imacros 的参数添加到gcc 调用中。
第一部分很容易编写脚本:将grep -Gv '^#include[ ]*<' <./"$fname" 管道传输到 gcc。
第二部分需要一些练习(至少没有 awk):
2.1 从上一个 grep 命令中删除 -v 否定匹配:grep -G '^#include[ ]*<' <./"$fname"
2.2 在xarg 中使用expr 之前的管道以从每个包含指令中提取标题名称:xargs -I {} -- expr "{}" : '#include[ ]*<[ ]*\(.*\)[ ]*>'
2.3 再次通过管道连接到 xarg,然后 printf 使用 -imacros 前缀:xargs -I {} printf '-imacros %s ' "{}"
2.4 将所有命令替换为 "$()" 并放在 gcc 中。
完成。从我的答案开始,这就是你最终得到冗长命令的方式。
解决细微问题
这个方案还是有缺陷的;如果本地头文件本身包含全局头文件,则这些全局头文件将被扩展。解决此问题的一种方法是使用 grep+sed 从本地文件中传输所有全局包含,并将它们收集到每个 *.c 文件中。
printf '' > std ;
for header in *.h ; do
grep -G '^#include[ ]*<' <./$header >> std ;
sed -i '/#include[ ]*</d' $header ;
done;
for source in *.c ; do
cat std > tmp;
cat $source >> tmp;
mv -f tmp $source ;
done
现在可以在pwd 内的任何 *.c 文件上调用处理脚本,而无需担心来自全局包含的任何内容都会泄漏到其中。最后一个问题是重复。本地标头(包括其自身的本地包含)可能会重复,但这仅在标头不受保护时才会发生,并且通常每个标头都应始终受到保护。
最终版本和示例
为了展示这些脚本的实际效果,这里有一个小演示:
文件h1.h:
#ifndef H1H
#define H1H
#include <stdio.h>
#include <limits.h>
#define H1 printf("H1:%i\n", h1_int)
int h1_int=INT_MAX;
#endif
文件h2.h:
#ifndef H2H
#define H2H
#include <stdio.h>
#include "h1.h"
#define H2 printf("H2:%i\n", h2_int)
int h2_int;
#endif
文件main.c:
#include <assert.h>
#include "h1.h"
#include "h2.h"
int main(){
assert(1);
H1;
H2;
}
脚本的最终版本preproc.sh:
fname="$1"
printf '' > std ;
for source in *.[ch] ; do
grep -G '^#include[ ]*<' <./$source >> std ;
sed -i '/#include[ ]*</d' $source ;
sort -u std > std2;
mv -f std2 std;
done;
for source in *.c ; do
cat std > tmp;
cat $source >> tmp;
mv -f tmp $source ;
done
grep -G '^#include[ ]*<' <./"$fname" ;
grep -Gv '^#include[ ]*<' <./"$fname" | gcc -x c - -E -o - $(grep -G '^#include[ ]*<' <./"$fname" | xargs -I {} -- expr "{}" : '#include[ ]*<[ ]*\(.*\)[ ]*>' | xargs -I {} printf '-imacros %s ' "{}" ) | grep -Ev '^([ ]*|#.*)$'
调用./preproc.sh main.c的输出:
#include <assert.h>
#include <limits.h>
#include <stdio.h>
int h1_int=0x7fffffff;
int h2_int;
int main(){
((void) sizeof ((
1
) ? 1 : 0), __extension__ ({ if (
1
) ; else __assert_fail (
"1"
, "<stdin>", 4, __extension__ __PRETTY_FUNCTION__); }))
;
printf("H1:%i\n", h1_int);
printf("H2:%i\n", h2_int);
}
这应该总是编译。如果你真的想打印每个#include "file",然后从grep模式'^#include[ ]*<' in 16-th line of preproc.sh`中删除<,但要注意,标题的内容将被复制,并且代码可能会失败,如果标题包含初始化变量。在我的示例中,这是有意解决问题的。
总结
这里有很多好的答案,为什么还有另一个?因为这似乎是具有以下属性的独特解决方案:
- 本地包含已扩展
- 全局包含被丢弃
- 在本地或全局包含中定义的宏被扩展
该方法足够通用,不仅可用于玩具示例,而且实际上可用于驻留在单个目录中的中小型项目。