forked from 4hopp/proto2cpp
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathproto2cpp.py
executable file
·284 lines (259 loc) · 11.1 KB
/
proto2cpp.py
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#!/usr/bin/env python
##
# Doxygen filter for Google Protocol Buffers .proto files.
# This script converts .proto files into C++ style ones
# and prints the output to standard output.
#
# version 0.8-beta OSI
#
# How to enable this filter in Doxygen:
# 1. Generate Doxygen configuration file with command 'doxygen -g <filename>'
# e.g. doxygen -g doxyfile
# 2. In the Doxygen configuration file, find FILE_PATTERNS and add *.proto
# FILE_PATTERNS = *.proto
# 3. In the Doxygen configuration file, find EXTENSION_MAPPING and add proto=C++
# EXTENSION_MAPPING = proto=C++
# 4. In the Doxygen configuration file, find INPUT_FILTER and add this script
# INPUT_FILTER = "python proto2cpp.py"
# 5. Run Doxygen with the modified configuration
# doxygen doxyfile
#
#
# Version 0.8 2018 Bugfix regarding long comments, remove typo
# Version 0.7 2018 Bugfix and extensions have been made by Open Simulation Interface (OSI) Carsten Kuebler https://github.com/OpenSimulationInterface,
# Copyright (C) 2016 Regents of the University of California https://github.com/vgteam/vg
# Copyright (C) 2012-2015 Timo Marjoniemi https://sourceforge.net/p/proto2cpp/wiki/Home/
# All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
##
import os
import sys
import re
import fnmatch
import inspect
## Class for converting Google Protocol Buffers .proto files into C++ style output to enable Doxygen usage.
##
## The C++ style output is printed into standard output.<br />
## There are three different logging levels for the class:
## <ul><li>#logNone: do not log anything</li>
## <li>#logErrors: log errors only</li>
## <li>#logAll: log everything</li></ul>
## Logging level is determined by \c #logLevel.<br />
## Error logs are written to file determined by \c #errorLogFile.<br />
## Debug logs are written to file determined by \c #logFile.
#
class proto2cpp:
## Logging level: do not log anything.
logNone = 0
## Logging level: log errors only.
logErrors = 1
## Logging level: log everything.
logAll = 2
## Constructor
#
def __init__(self):
## Debug log file name.
self.logFile = "proto2cpp.log"
## Error log file name.
self.errorLogFile = "proto2cpp.error.log"
## Logging level.
self.logLevel = self.logNone
## Handles a file.
##
## If @p fileName has .proto suffix, it is processed through parseFile().
## Otherwise it is printed to stdout as is except for file \c proto2cpp.py without
## path since it's the script given to python for processing.
##
## @param fileName Name of the file to be handled.
#
def handleFile(self, fileName):
if fnmatch.fnmatch(filename, '*.proto'):
self.log('\nXXXXXXXXXX\nXX ' + filename + '\nXXXXXXXXXX\n\n')
# Open the file. Use try to detect whether or not we have an actual file.
if (sys.version_info >= (3, 0)):
try:
with open(filename, 'r', encoding='utf8') as inputFile:
self.parseFile(inputFile)
pass
except IOError as e:
self.logError('the file ' + filename + ' could not be opened for reading')
else:
# Python 2 code in this block
try:
with open(filename, 'r') as inputFile:
self.parseFile(inputFile)
pass
except IOError as e:
self.logError('the file ' + filename + ' could not be opened for reading')
elif not fnmatch.fnmatch(filename, os.path.basename(inspect.getfile(inspect.currentframe()))):
self.log('\nXXXXXXXXXX\nXX ' + filename + '\nXXXXXXXXXX\n\n')
if (sys.version_info > (3, 0)):
try:
with open(filename, 'r', encoding='utf8') as theFile:
output = ''
for theLine in theFile:
output += theLine
print(output)
self.log(output)
pass
except IOError as e:
self.logError('the file ' + filename + ' could not be opened for reading')
else:
# Python 2 code in this block
try:
with open(filename, 'r') as theFile:
output = ''
for theLine in theFile:
output += theLine
print(output)
self.log(output)
pass
except IOError as e:
self.logError('the file ' + filename + ' could not be opened for reading')
else:
self.log('\nXXXXXXXXXX\nXX ' + filename + ' --skipped--\nXXXXXXXXXX\n\n')
## Parser function.
##
## The function takes a .proto file object as input
## parameter and modifies the contents into C++ style.
## The modified data is printed into standard output.
##
## @param inputFile Input file object
#
def parseFile(self, inputFile):
# Go through the input file line by line.
isEnum = False
isPackage = False
isMultilineComment = False
# This variable is here as a workaround for not getting extra line breaks (each line
# ends with a line separator and print() method will add another one).
# We will be adding lines into this var and then print the var out at the end.
theOutput = ''
for line in inputFile:
# Search for comment ("//") and add one more slash character ("/") to the comment
# block to make Doxygen detect it.
matchComment = re.search("//", line)
# Search for semicolon and if one is found before comment, add a third slash character
# ("/") and a smaller than ("<") character to the comment to make Doxygen detect it.
matchSemicolon = re.search(";", line)
if matchSemicolon is not None and (matchComment is not None and matchSemicolon.start() < matchComment.start()):
comment = "///<" + line[matchComment.end():]
# Replace '.' in nested message references with '::'
# don't work for multi-nested references and generates problems with URLs and acronyms
#comment = re.sub(r'\s(\w+)\.(\w+)\s', r' \1::\2 ', comment)
line = line[:matchComment.start()]
elif matchComment is not None:
if isMultilineComment:
comment = " * " + line[matchComment.end():]
else:
comment = "/** " + line[matchComment.end():]
isMultilineComment = True
# replace '.' in nested message references with '::'
# don't work for multi-nested references and generates problems with URLs and acronyms
#comment = re.sub(r'\s(\w+)\.(\w+)\s', r' \1::\2 ', comment)
line = line[:matchComment.start()]
else:
comment = ""
# End multiline comment, if there is no comment or if there are some chars before the comment.
if (matchComment is None or len(line.strip())>0) and isMultilineComment:
theOutput += " */\n"
isMultilineComment = False
# line = line.replace(".", "::") but not in quoted strings (Necessary for import statement)
line = re.sub(r'\.(?=(?:[^"]*"[^"]*")*[^"]*$)',r'::',line)
# Search for " option ...;", remove it
line = re.sub(r'\boption\b[^;]+;', r'', line)
# Search for " package ", make a namespace
matchPackage = re.search(r"\bpackage\b", line)
if matchPackage is not None:
isPackage = True
# Convert to C++-style separator and block instead of statement
line = "namespace" + line[:matchPackage.start()] + line[matchPackage.end():].replace(";", " {")
# Search for " repeated " fields and make them ...
#matchRepeated = re.search(r"\brepeated\b", line)
#if matchRepeated is not None:
# # Convert
# line = re.sub(r'\brepeated\s+(\S+)', r' repeated \1', line)
# Search for "enum", start changing all semicolons (";") to commas (",").
matchEnum = re.search(r"\benum\b", line)
if matchEnum is not None:
isEnum = True
# Search semicolon if we have detected an enum, and replace semicolon with comma.
if isEnum is True and matchSemicolon is not None:
line = line.replace(";", ",")
# Search for a closing brace.
matchClosingBrace = re.search("}", line)
if isEnum is True and matchClosingBrace is not None:
line = line[:matchClosingBrace.start()] + "};" + line[matchClosingBrace.end():]
isEnum = False
elif isEnum is False and matchClosingBrace is not None:
# Message (to be struct) ends => add semicolon so that it'll
# be a proper C(++) struct and Doxygen will handle it correctly.
line = line[:matchClosingBrace.start()] + "};" + line[matchClosingBrace.end():]
# Replacements change start of comment...
matchMsg = re.search(r"\bmessage\b", line)
if matchMsg is not None:
line = line[:matchMsg.start()] + "struct" + line[matchMsg.end():]
# Replacements change start of comment...
matchExt = re.search(r"\bextend\b", line)
if matchExt is not None:
a_extend = line[matchExt.end():]
matchName = re.search(r"\b\w[\S:]*\b", a_extend)
if matchName is not None:
name = a_extend[matchName.start():matchName.end()]
name = re.sub(r'\w+::',r'',name)
a_extend = a_extend[:matchName.start()] + name + ": public " + a_extend[matchName.start():]
else:
a_extend = "_Dummy: public " + a_extend;
line = line[:matchExt.start()] + "struct " + a_extend
theOutput += (line.strip() + ' ' + comment.strip()).strip() + '\n'
if isPackage:
# Close the package namespace
theOutput += "}"
isPackage = False
# Now that we've got all lines in the string let's split the lines and print out
# one by one.
# This is a workaround to get rid of extra empty line at the end which print() method adds.
lines = theOutput.splitlines()
for line in lines:
if len(line) > 0:
print(line) # Add linebreak to generate documentation correct.
self.log(line + '\n')
## Writes @p string to log file.
##
## #logLevel must be #logAll or otherwise the logging is skipped.
##
## @param string String to be written to log file.
#
def log(self, string):
if self.logLevel >= self.logAll:
with open(self.logFile, 'a') as theFile:
theFile.write(string)
## Writes @p string to error log file.
##
## #logLevel must be #logError or #logAll or otherwise the logging is skipped.
##
## @param string String to be written to error log file.
#
def logError(self, string):
if self.logLevel >= self.logError:
with open(self.errorLogFile, 'a') as theFile:
theFile.write(string)
converter = proto2cpp()
# Doxygen will give us the file names
for filename in sys.argv[1:]:
converter.handleFile(filename)
# end of file