【问题标题】:How to show 'preprocessed' code ignoring includes with GCC如何显示“预处理”代码忽略包含在 GCC 中
【发布时间】:2019-07-20 08:02:53
【问题描述】:

我想知道是否可以使用 gcc 输出“预处理”代码,但“忽略”(不扩展)包括:

ES 我得到了这个主要的:

#include <stdio.h>
#define prn(s) printf("this is a macro for printing a string: %s\n", s);

int int(){
char str[5] = "test"; 
prn(str);
return 0;
}

我跑gcc -E main -o out.c

我明白了:

/*
all stdio stuff
*/

int int(){
char str[5] = "test";
printf("this is a macro for printing a string: %s\n", str);
return 0;
}

我只想输出:

#include <stdio.h>
int int(){
char str[5] = "test";
printf("this is a macro for printing a string: %s\n", str);
return 0;
}

或者,至少,只是

int int(){
char str[5] = "test";
printf("this is a macro for printing a string: %s\n", str);
return 0;
}

PS:如果可以扩展“本地”"" 包含而不扩展“全局”&lt;&gt; 包含,那就太好了

【问题讨论】:

  • 将包含在#ifndef XXXX ... #endif 中并传递-DXXXX-E 怎么样?
  • grep -v "#include" &lt; file.c &gt; noinclude.c ; gcc -E noinclude.c
  • 如果您使用标准头文件中的宏,这两种方法都会出现问题,而这些宏不会被扩展。
  • @bruno 可以在没有中间文件的情况下使用sed 完成......但我不会考虑它:)
  • 您可以随时注释掉系统包含文件。

标签: c linux gcc c-preprocessor


【解决方案1】:

当 cpp 扩展包含时,它会添加 # directives (linemarkers) 以将错误追溯到原始文件。

您可以添加一个后期处理步骤(它可以用任何脚本语言编写,如果您愿意,甚至可以用 C 语言编写)来仅解析线标记并过滤掉来自项目目录之外的文件的行;更好的是,其中一个标志 (3) 标记系统头文件(来自通过-isystem 提供的路径的东西,无论是由编译器驱动程序隐式还是显式地),所以这也是您可以利用的东西。

例如在 Python 3 中:

#!/usr/bin/env python3
import sys

skip = False
for l in sys.stdin:
    if not skip:
        sys.stdout.write(l)
    if l.startswith("# "):
        toks = l.strip().split(" ")
        linenum, filename = toks[1:3]
        flags = toks[3:]
        skip = "3" in flags

使用gcc -E foo.c | ./filter.py 我明白了

# 1 "foo.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "foo.c"
# 1 "/usr/include/stdio.h" 1 3 4



# 4 "foo.c"
int int(){
char str[5] = "test";
printf("this is a macro for printing a string: %s\n", str);;
return 0;
}

【讨论】:

    【解决方案2】:

    假设文件名为c.c

    gcc -E c.c | tail -n +`gcc -E c.c | grep -n -e "#*\"c.c\""  | tail -1 | awk -F: '{print $1}'`
    

    似乎# &lt;number&gt; "c.c" 标记了每个#include 之后的行

    当然你也可以把gcc -E c.c存到一个文件里不做两次

    优点是在执行gcc -E之前不修改源也不删除#include,这只是删除从上到最后由@产生的所有行987654329@ ...如果我是对的

    【讨论】:

    • 有效,只需要在第一个尾部添加-n作为参数
    • 我也喜欢它,因为它的语法简洁,易于记忆和适应
    • @DDS 对不起,对我来说“+”和“-n +”也是一样的,我还是编辑了我的答案。如果我的答案是 答案,请接受它以标记问题已解决 (What should I do when someone answers my question?)
    【解决方案3】:

    我同意 Matteo Italia 的评论,即如果您只是阻止 #include 指令被扩展,那么生成的代码将无法代表编译器实际看到的内容,因此它在故障排除中的用途有限。

    这里有一个解决这个问题的想法。在包含之前和之后添加变量声明。任何合理唯一的变量都可以。

    int begin_includes_tag;
    #include <stdio.h>
    ... other includes
    int end_includes_tag;
    

    那么你可以这样做:

    > gcc -E main -o out.c | sed '/begin_includes_tag/,/end_includes_tag/d'
    

    sed 命令将删除这些变量声明之间的所有内容。

    【讨论】:

    • 无需添加实际变量,即使包含 GUID 的两个 cmet 也可以。
    • @MatteoItalia 感谢您的想法。起初我用 cmets 尝试过,但很快意识到 cmets 只会被预处理器剥离。我不知道 GUID cmets 不会被剥离。
    • 呃抱歉不知道评论剥离发生在预处理器级别,那么请忽略我的评论!
    • 我会这样做,将“声明标记”放入#ifdef DEBUG,因为它很简单,而且我不需要对 SED 做很多事情(我不熟悉)因为它可以让我轻松地“取消标记”我想要处理的内容
    • @MatteoItalia,gnu cpp 程序可以在保持 cmets 与 -C 标志(或 -CC)的同时进行预处理,所以我觉得你的提示很有用。
    【解决方案4】:

    保护#includes 不被扩展,以文本方式运行预处理器,删除文本预处理器生成的# 1 "&lt;stdint&gt;" 等垃圾并重新公开受保护的#includes。

    这个shell函数可以做到:

    expand_cpp(){
         sed 's|^\([ \t]*#[ \t]*include\)|magic_fjdsa9f8j932j9\1|' "$@" \
         | cpp | sed 's|^magic_fjdsa9f8j932j9||; /^# [0-9]/d'
    }
    

    只要你把包含词放在一起,而不是做一些疯狂的事情,比如

    #i\
    ncl\
    u??/
    de <iostream>
    

    (您可以在上面看到 2 个反斜杠续行 + 1 个三元组(??/ == \ )反斜杠续行)。

    如果你愿意,你可以用同样的方式保护#ifs #ifdefs #ifndefs #endifs 和#elses。

    应用于您的示例

    example.c:

    #include <stdio.h>
    #define prn(s) printf("this is a macro for printing a string: %s\n", s);
    
    int int(){
    char str[5] = "test";
    prn(str);
    return 0;
    }
    

    expand_cpp &lt; example.cexpand_cpp example.c 一样,它会生成:

    #include <stdio.h>
    
    
    int int(){
    char str[5] = "test";
    printf("this is a macro for printing a string: %s\n", str);;
    return 0;
    }
    

    【讨论】:

      【解决方案5】:

      您可以使用-dI 显示#include 指令并对预处理器输出进行后处理。

      假设您的文件名是foo.c

      SOURCEFILE=foo.c
      gcc -E -dI "$SOURCEFILE" | awk '
          /^# [0-9]* "/ { if ($3 == "\"'"$SOURCEFILE"'\"") show=1; else show=0; }
          { if(show) print; }'
      

      或取消# line_number "file" 的所有$SOURCEFILE 行:

      SOURCEFILE=foo.c
      gcc -E -dI "$SOURCEFILE" | awk '
          /^# [0-9]* "/ { ignore = 1; if ($3 == "\"'"$SOURCEFILE"'\"") show=1; else show=0; }
          { if(ignore) ignore=0; else if(show) print; }'
      

      注意:AWK 脚本不适用于包含空格的文件名。要处理带空格的文件名,您可以修改 AWK 脚本以比较 $0 而不是 $3

      【讨论】:

        【解决方案6】:

        以前的许多答案都指向使用跟踪 # 指令的方向。

        它实际上是经典 Unix 中的单行代码(带有awk):

        gcc -E file.c | awk '/# [1-9][0-9]* "file.c"/ {skip=0; next} /# [1-9][0-9]* ".*"/ {skip=1} (skip&lt;1) {print}'

        【讨论】:

          【解决方案7】:

          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 指令中使用&lt;&gt;"",所以它的行为就像#include &lt;file&gt; 在源代码中一样。

          第一种方法

          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__ 是字符串文字。

          自动化

          以前的方法可行,但可能很乏味;

          1. 每个#include都需要手动从文件中删除,并且

          2. 它必须作为-imacros 的参数添加到gcc 调用中。

          第一部分很容易编写脚本:将grep -Gv '^#include[ ]*&lt;' &lt;./"$fname" 管道传输到 gcc。

          第二部分需要一些练习(至少没有 awk):

          2.1 从上一个 grep 命令中删除 -v 否定匹配:grep -G '^#include[ ]*&lt;' &lt;./"$fname"

          2.2 在xarg 中使用expr 之前的管道以从每个包含指令中提取标题名称:xargs -I {} -- expr "{}" : '#include[ ]*&lt;[ ]*\(.*\)[ ]*&gt;'

          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[ ]*&lt;' in 16-th line of preproc.sh`中删除&lt;,但要注意,标题的内容将被复制,并且代码可能会失败,如果标题包含初始化变量。在我的示例中,这是有意解决问题的。

          总结

          这里有很多好的答案,为什么还有另一个?因为这似乎是具有以下属性的独特解决方案:

          1. 本地包含已扩展
          2. 全局包含被丢弃
          3. 在本地或全局包含中定义的宏被扩展

          该方法足够通用,不仅可用于玩具示例,而且实际上可用于驻留在单个目录中的中小型项目。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2023-03-28
            • 1970-01-01
            • 1970-01-01
            • 2010-11-06
            • 2013-06-06
            • 1970-01-01
            • 2021-10-16
            相关资源
            最近更新 更多