Skip to content

Commit 8f9e862

Browse files
committed
Add GCC compatible preprocessing function
1 parent 9dd573e commit 8f9e862

File tree

3 files changed

+116
-8
lines changed

3 files changed

+116
-8
lines changed

cxxheaderparser/lexer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class LexError(CxxParseError):
1717
else:
1818
Protocol = object
1919

20-
_line_re = re.compile(r'^\#[\t ]*line (\d+) "(.*)"')
20+
_line_re = re.compile(r'^\#[\t ]*(line)? (\d+) "(.*)"')
2121
_multicomment_re = re.compile("\n[\\s]+\\*")
2222

2323

@@ -448,8 +448,8 @@ def t_PP_DIRECTIVE(self, t: LexToken):
448448
# handle line macros
449449
m = _line_re.match(t.value)
450450
if m:
451-
self.filename = m.group(2)
452-
self.line_offset = 1 + self.lex.lineno - int(m.group(1))
451+
self.filename = m.group(3)
452+
self.line_offset = 1 + self.lex.lineno - int(m.group(2))
453453
return None
454454
# ignore C++23 warning directive
455455
if t.value.startswith("#warning"):

cxxheaderparser/preprocessor.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,112 @@
11
"""
2-
Contains optional preprocessor support via pcpp
2+
Contains optional preprocessor support functions
33
"""
44

55
import io
66
import re
77
import os
8+
import subprocess
9+
import sys
810
import typing
11+
912
from .options import PreprocessorFunction
1013

1114

1215
class PreprocessorError(Exception):
1316
pass
1417

1518

19+
#
20+
# GCC preprocessor support
21+
#
22+
23+
24+
def _gcc_filter(fname: str, fp: typing.TextIO) -> str:
25+
new_output = io.StringIO()
26+
keep = True
27+
fname = fname.replace("\\", "\\\\")
28+
29+
for line in fp:
30+
if line.startswith("# "):
31+
last_quote = line.rfind('"')
32+
if last_quote != -1:
33+
keep = line[:last_quote].endswith(fname)
34+
35+
if keep:
36+
new_output.write(line)
37+
38+
new_output.seek(0)
39+
return new_output.read()
40+
41+
42+
def make_gcc_preprocessor(
43+
*,
44+
defines: typing.List[str] = [],
45+
include_paths: typing.List[str] = [],
46+
retain_all_content: bool = False,
47+
encoding: typing.Optional[str] = None,
48+
gcc_args: typing.List[str] = ["g++"],
49+
print_cmd: bool = True,
50+
) -> PreprocessorFunction:
51+
"""
52+
Creates a preprocessor function that uses g++ to preprocess the input text.
53+
54+
gcc is a high performance and accurate precompiler, but if an #include
55+
directive can't be resolved or other oddity exists in your input it will
56+
throw an error.
57+
58+
:param defines: list of #define macros specified as "key value"
59+
:param include_paths: list of directories to search for included files
60+
:param retain_all_content: If False, only the parsed file content will be retained
61+
:param encoding: If specified any include files are opened with this encoding
62+
:param gcc_args: This is the path to G++ and any extra args you might want
63+
:param print_cmd: Prints the gcc command as its executed
64+
65+
.. code-block:: python
66+
67+
pp = make_gcc_preprocessor()
68+
options = ParserOptions(preprocessor=pp)
69+
70+
parse_file(content, options=options)
71+
72+
"""
73+
74+
if not encoding:
75+
encoding = "utf-8"
76+
77+
def _preprocess_file(filename: str, content: str) -> str:
78+
cmd = gcc_args + ["-w", "-E", "-C"]
79+
80+
for p in include_paths:
81+
cmd.append(f"-I{p}")
82+
for d in defines:
83+
cmd.append(f"-D{d.replace(' ', '=')}")
84+
85+
kwargs = {"encoding": encoding}
86+
if filename == "<str>":
87+
cmd.append("-")
88+
filename = "<stdin>"
89+
kwargs["input"] = content
90+
else:
91+
cmd.append(filename)
92+
93+
if print_cmd:
94+
print("+", " ".join(cmd), file=sys.stderr)
95+
96+
result: str = subprocess.check_output(cmd, **kwargs) # type: ignore
97+
if not retain_all_content:
98+
result = _gcc_filter(filename, io.StringIO(result))
99+
100+
return result
101+
102+
return _preprocess_file
103+
104+
105+
#
106+
# PCPP preprocessor support (not installed by default)
107+
#
108+
109+
16110
try:
17111
import pcpp
18112
from pcpp import Preprocessor, OutputDirective, Action
@@ -41,7 +135,7 @@ def on_comment(self, *ignored):
41135
pcpp = None
42136

43137

44-
def _filter_self(fname: str, fp: typing.TextIO) -> str:
138+
def _pcpp_filter(fname: str, fp: typing.TextIO) -> str:
45139
# the output of pcpp includes the contents of all the included files, which
46140
# isn't what a typical user of cxxheaderparser would want, so we strip out
47141
# the line directives and any content that isn't in our original file
@@ -74,6 +168,13 @@ def make_pcpp_preprocessor(
74168
Creates a preprocessor function that uses pcpp (which must be installed
75169
separately) to preprocess the input text.
76170
171+
If missing #include files are encountered, this preprocessor will ignore the
172+
error. This preprocessor is pure python so it's very portable, and is a good
173+
choice if performance isn't critical.
174+
175+
:param defines: list of #define macros specified as "key value"
176+
:param include_paths: list of directories to search for included files
177+
:param retain_all_content: If False, only the parsed file content will be retained
77178
:param encoding: If specified any include files are opened with this encoding
78179
:param passthru_includes: If specified any #include directives that match the
79180
compiled regex pattern will be part of the output.
@@ -127,6 +228,6 @@ def _preprocess_file(filename: str, content: str) -> str:
127228
filename = filename.replace(os.sep, "/")
128229
break
129230

130-
return _filter_self(filename, fp)
231+
return _pcpp_filter(filename, fp)
131232

132233
return _preprocess_file

tests/test_preprocessor.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@
2626
)
2727

2828

29-
@pytest.fixture(params=["pcpp"])
29+
@pytest.fixture(params=["gcc", "pcpp"])
3030
def make_pp(request) -> typing.Callable[..., PreprocessorFunction]:
3131
param = request.param
32-
if param == "pcpp":
32+
if param == "gcc":
33+
gcc_path = shutil.which("g++")
34+
if not gcc_path:
35+
pytest.skip("g++ not found")
36+
37+
subprocess.run([gcc_path, "--version"])
38+
return preprocessor.make_gcc_preprocessor
39+
elif param == "pcpp":
3340
if preprocessor.pcpp is None:
3441
pytest.skip("pcpp not installed")
3542
return preprocessor.make_pcpp_preprocessor

0 commit comments

Comments
 (0)