-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathkeywordify.py
284 lines (243 loc) · 9.79 KB
/
keywordify.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
# This script generates a keywords.txt file for the Arduino IDE by scraping
# through the codebase, and picking keywords using the following scheme
# that seems to be widely accepted by the Arduino community:
#
# KEYWORD1 for Classes, Datatypes, and C++ keywords
# KEYWORD2 for Methods and functions
# LITERAL1 for constants
#
# Author: Cody Burrows
# Date: 2019-07-08
import argparse
import datetime
import os
import re
# path to the variants\clearcore\Teknic directory
REL_PATH_TEKNIC = os.path.dirname(os.path.abspath(__file__)) + '\\Teknic\\libClearCore\\'
# path to the pins_arduino header file
REL_PATH_PINS_ARDUINO = os.path.dirname(os.path.abspath(__file__)) + '\\variants\\clearcore\\pins_arduino.h'
# I love escaping escapes
PATTERN_PUBLIC_FUNC = re.compile(r'(?!return|operator\b)\b[\w\*&]+ [\w]+\([^(){}]*\)? ?(override ?)?(;|{)?')
PATTERN_PUBLIC_ENUM = re.compile('(typedef )?enum[^{}()]+{')
PATTERN_ENUM_NAME = re.compile('[A-Z0-9_]+n?[A-Z0-9_]*( += (-)?(0x)?([0-9 <>UL]+|[A-Z0-9_]+n?[A-Z0-9_]*)( [+-] [0-9]+)?)?' +
'((,)?(\s*)\n|(,)?(\s*)//((\s*)(\S*)(\s*))*\n)')
# AL 9-24-2019 updated PATTERN_ENUM_NAME to allow inline comments after an enum,
# (\s*) allows optionally any amount of whitespace.
PATTERN_DEFINE_DIRECTIVE = re.compile('(#define)(\s*)[A-Z0-9_]*(\s*)(\w|\W)+')
PATTERN_GLOBAL_INSTANCE = re.compile(r'extern [A-Za-z][&A-Za-z0-9]* *&? *(\*const )?(?P<name>[A-Za-z][A-Za-z0-9]*);')
# relative path of keywords.txt file
keywords = "keywords.txt"
# Argument setup
parser = argparse.ArgumentParser(description='Generate keywords.txt from the ClearCore source files.')
parser.add_argument('-S', action='store_true', help='silent mode, no permission request to remove the file')
parser.add_argument('-p', action='store', dest="p", default="", help="relative path to the keywords.txt file")
# print error and die
def error(errString):
print('Error: ' + errString)
exit()
# Prompt user for y/n
def confirmYN(msg):
ans = str(input(msg + ' (y/n) ')).lower().strip()
return ans[0] == 'y'
# write a line out to the keywords.txt file
def writeline(f, line):
f.write(line + '\n')
# find all public functions in the given file
def publicFunctions(f):
public = False
inComment = False
matches = set()
openedFile = open(f, 'r')
for line in openedFile:
if 'public:' in line:
public = True
elif 'protected:' in line:
public = False
elif 'private:' in line:
public = False
if public:
# Detect beginning of a multi-line comment.
if not inComment:
if '/*' in line:
inComment = True
# Detect end of a multi-line comment.
if inComment:
if '*/' in line:
inComment = False
x = re.search(PATTERN_PUBLIC_FUNC, line)
# "Match objects always have a boolean value of True" https://docs.python.org/2/library/re.html
if x and not inComment:
wholeMatch = x.string[x.start():x.end()]
name = wholeMatch.split('(')[0] # gets func name with return ty
name = name.split(' ')[1] # gets rid of return type
matches.add(name)
#print('\tFound: ' + name)
openedFile.close()
return matches
# find all of the enum names in the given file
def publicEnums(f):
public = False
inEnum = False
inComment = False
matches = set()
openedFile = open(f, 'r')
for line in openedFile:
if 'public:' in line:
public = True
inEnum = False
elif 'protected:' in line:
public = False
inEnum = False
elif 'private:' in line:
public = False
inEnum = False
if public:
if inEnum:
# Detect beginning of a multi-line comment.
if not inComment:
if '/*' in line:
inComment = True
# Detect end of a multi-line comment.
if inComment:
if '*/' in line:
inComment = False
# Detect end of an enum.
if '}' in line:
inEnum = False
else:
inEnum = re.search(PATTERN_PUBLIC_ENUM, line) is not None
if inEnum and not inComment:
x = re.search(PATTERN_ENUM_NAME, line)
if x:
wholeMatch = x.string[x.start():x.end()]
if '=' in wholeMatch:
name = wholeMatch.split('=')[0].strip()
elif ',' in wholeMatch:
name = wholeMatch.split(',')[0].strip()
else:
name = wholeMatch.split(' ')[0].strip()
#print(f'Match: {wholeMatch}')
#print(f'Found: {name}')
matches.add(name)
openedFile.close()
return matches
# find all #defines and declare the first two values as literals
# ex: #define IO3 CLEARCORE_PIN_IO3 -> declare IO3 and CLEARCORE_PIN_IO3
def pinDefines(f):
define = False
matches = set()
with open(f) as fp:
for line in fp:
if PATTERN_DEFINE_DIRECTIVE.match(line):
# this is a hack for pins_arduino.h and not how a standard
# define would be handled
pinDirective = line.split()[1].strip()
pinEnum = line.split()[2].strip()
#print('\tFound: ' + pinDirective)
matches.add(pinDirective)
#print('\tFound: ' + pinEnum)
matches.add(pinEnum)
return matches
# find global connector instances
def globalInstances(f):
matches = set()
with open(f) as fp:
for line in fp:
if PATTERN_GLOBAL_INSTANCE.match(line):
global_instance = PATTERN_GLOBAL_INSTANCE.match(line).group('name')
matches.add(global_instance)
return matches
# start "running" the script
print('Running keywordify ' + datetime.date.today().strftime("%Y-%m-%d"))
# parse arguments
args = parser.parse_args()
# make sure we're starting in the right directory
inc_found = os.path.exists(REL_PATH_TEKNIC + 'inc')
src_found = os.path.exists(REL_PATH_TEKNIC + 'src')
pins_h_found = os.path.exists(REL_PATH_PINS_ARDUINO)
if not inc_found:
error('Couldn\'t find ' + REL_PATH_TEKNIC + 'inc!')
elif not src_found:
error('Couldn\'t find ' + REL_PATH_TEKNIC + 'src!')
elif not pins_h_found:
error("Couldn't find " + REL_PATH_PINS_ARDUINO + "!")
else:
print('Found ' + REL_PATH_TEKNIC + 'inc and\n' + REL_PATH_TEKNIC + 'src\n')
# update the keywords.txt path with an optionally specified location
keywords = args.p + keywords
# check to see if keywords.txt already exists
if os.path.exists(keywords):
# the '-S' flag indicates silence, don't ask
if not args.S:
if not confirmYN('keywords.txt already exists. Overwrite?'):
exit()
os.remove(keywords)
# fall through to start scraping
keyword1s = set()
keyword2s = set()
literal1s = set()
REL_PATH_INC = REL_PATH_TEKNIC + 'inc'
REL_PATH_SRC = REL_PATH_TEKNIC + 'src'
# get all of the classes in ...\Teknic\src (KEYWORD1)
print('Searching for Classes...')
for (_, _, f) in os.walk(REL_PATH_SRC):
for filename in f:
if filename.endswith('.cpp'):
name = os.path.splitext(filename)[0]
#print('\tFound: ' + name)
keyword1s.add(name)
# add in Serial0
keyword1s.add("Serial0")
# get all of the global connector instances (KEYWORD1)
print('Searching for global instances...')
for (_,_,f) in os.walk(REL_PATH_INC):
for filename in f:
if filename == "ClearCore.h":
keyword1s = keyword1s.union(globalInstances(REL_PATH_INC + '\\' + filename))
# get all of the public functions (KEYWORD2)
print('Searching for public functions...')
for(_, _, f) in os.walk(REL_PATH_INC):
for filename in f:
if filename.endswith('.h'):
#print('\nLooking at file: ' + filename + '\n')
keyword2s = keyword2s.union(publicFunctions(REL_PATH_INC +
'\\' + filename))
# get all of the constants (LITERAL1)
print('Searching for constants...')
for(_, _, f) in os.walk(REL_PATH_INC):
for filename in f:
if filename.endswith('.h'):
#print('\nLooking at file: ' + filename + '\n')
literal1s = literal1s.union(publicEnums(REL_PATH_INC +
'\\' + filename))
# get all of the pin mappings (LITERAL1)
print('Looking at pin mapping...')
literal1s = literal1s.union(pinDefines(REL_PATH_PINS_ARDUINO))
# done collecting names
# write out to file
# write a timestamp to the file
keywords_txt = open(keywords, 'w+')
timestamp = datetime.datetime.now()
# write out KEYWORD1 entries
writeline(keywords_txt, '# Classes, Datatypes, and C++ keywords')
# sort alphabetically
keyword1sSorted = sorted(keyword1s)
for fname in keyword1sSorted:
writeline(keywords_txt, fname + '\t' + 'KEYWORD1')
# write out KEYWORD2 entries
keyword2s = keyword2s - keyword1s
# sort alphabetically
keyword2sSorted = sorted(keyword2s)
writeline(keywords_txt, '\n# Public methods and functions')
for fname in keyword2sSorted:
writeline(keywords_txt, fname + '\t' + 'KEYWORD2')
# write out LITERAL1 entries (sorted alphabetically)
literal1s = literal1s - keyword2s - keyword1s
# sort alphabetically
literal1sSorted = sorted(literal1s)
writeline(keywords_txt, '\n# Public Constants')
for fname in literal1sSorted:
writeline(keywords_txt, fname + '\t' + 'LITERAL1')
writeline(keywords_txt, '\n# keywordify done')
keywords_txt.close()
print('keywordify done.')