Skip to content

Commit e302fec

Browse files
author
Ladyada
committed
1 parent 4d057e6 commit e302fec

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

run-clang-format.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
#!/usr/bin/env python
2+
"""A wrapper script around clang-format, suitable for linting multiple files
3+
and to use for continuous integration.
4+
5+
This is an alternative API for the clang-format command line.
6+
It runs over multiple files and directories in parallel.
7+
A diff output is produced and a sensible exit code is returned.
8+
9+
"""
10+
11+
from __future__ import print_function, unicode_literals
12+
13+
import argparse
14+
import codecs
15+
import difflib
16+
import fnmatch
17+
import io
18+
import multiprocessing
19+
import os
20+
import signal
21+
import subprocess
22+
import sys
23+
import traceback
24+
25+
from functools import partial
26+
27+
DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx'
28+
29+
30+
class ExitStatus:
31+
SUCCESS = 0
32+
DIFF = 1
33+
TROUBLE = 2
34+
35+
36+
def list_files(files, recursive=False, extensions=None, exclude=None):
37+
if extensions is None:
38+
extensions = []
39+
if exclude is None:
40+
exclude = []
41+
42+
out = []
43+
for file in files:
44+
if recursive and os.path.isdir(file):
45+
for dirpath, dnames, fnames in os.walk(file):
46+
fpaths = [os.path.join(dirpath, fname) for fname in fnames]
47+
for pattern in exclude:
48+
# os.walk() supports trimming down the dnames list
49+
# by modifying it in-place,
50+
# to avoid unnecessary directory listings.
51+
dnames[:] = [
52+
x for x in dnames
53+
if
54+
not fnmatch.fnmatch(os.path.join(dirpath, x), pattern)
55+
]
56+
fpaths = [
57+
x for x in fpaths if not fnmatch.fnmatch(x, pattern)
58+
]
59+
for f in fpaths:
60+
ext = os.path.splitext(f)[1][1:]
61+
if ext in extensions:
62+
out.append(f)
63+
else:
64+
out.append(file)
65+
return out
66+
67+
68+
def make_diff(file, original, reformatted):
69+
return list(
70+
difflib.unified_diff(
71+
original,
72+
reformatted,
73+
fromfile='{}\t(original)'.format(file),
74+
tofile='{}\t(reformatted)'.format(file),
75+
n=3))
76+
77+
78+
class DiffError(Exception):
79+
def __init__(self, message, errs=None):
80+
super(DiffError, self).__init__(message)
81+
self.errs = errs or []
82+
83+
84+
class UnexpectedError(Exception):
85+
def __init__(self, message, exc=None):
86+
super(UnexpectedError, self).__init__(message)
87+
self.formatted_traceback = traceback.format_exc()
88+
self.exc = exc
89+
90+
91+
def run_clang_format_diff_wrapper(args, file):
92+
try:
93+
ret = run_clang_format_diff(args, file)
94+
return ret
95+
except DiffError:
96+
raise
97+
except Exception as e:
98+
raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__,
99+
e), e)
100+
101+
102+
def run_clang_format_diff(args, file):
103+
try:
104+
with io.open(file, 'r', encoding='utf-8') as f:
105+
original = f.readlines()
106+
except IOError as exc:
107+
raise DiffError(str(exc))
108+
invocation = [args.clang_format_executable, file]
109+
110+
# Use of utf-8 to decode the process output.
111+
#
112+
# Hopefully, this is the correct thing to do.
113+
#
114+
# It's done due to the following assumptions (which may be incorrect):
115+
# - clang-format will returns the bytes read from the files as-is,
116+
# without conversion, and it is already assumed that the files use utf-8.
117+
# - if the diagnostics were internationalized, they would use utf-8:
118+
# > Adding Translations to Clang
119+
# >
120+
# > Not possible yet!
121+
# > Diagnostic strings should be written in UTF-8,
122+
# > the client can translate to the relevant code page if needed.
123+
# > Each translation completely replaces the format string
124+
# > for the diagnostic.
125+
# > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation
126+
#
127+
# It's not pretty, due to Python 2 & 3 compatibility.
128+
encoding_py3 = {}
129+
if sys.version_info[0] >= 3:
130+
encoding_py3['encoding'] = 'utf-8'
131+
132+
try:
133+
proc = subprocess.Popen(
134+
invocation,
135+
stdout=subprocess.PIPE,
136+
stderr=subprocess.PIPE,
137+
universal_newlines=True,
138+
**encoding_py3)
139+
except OSError as exc:
140+
raise DiffError(str(exc))
141+
proc_stdout = proc.stdout
142+
proc_stderr = proc.stderr
143+
if sys.version_info[0] < 3:
144+
# make the pipes compatible with Python 3,
145+
# reading lines should output unicode
146+
encoding = 'utf-8'
147+
proc_stdout = codecs.getreader(encoding)(proc_stdout)
148+
proc_stderr = codecs.getreader(encoding)(proc_stderr)
149+
# hopefully the stderr pipe won't get full and block the process
150+
outs = list(proc_stdout.readlines())
151+
errs = list(proc_stderr.readlines())
152+
proc.wait()
153+
if proc.returncode:
154+
raise DiffError("clang-format exited with status {}: '{}'".format(
155+
proc.returncode, file), errs)
156+
return make_diff(file, original, outs), errs
157+
158+
159+
def bold_red(s):
160+
return '\x1b[1m\x1b[31m' + s + '\x1b[0m'
161+
162+
163+
def colorize(diff_lines):
164+
def bold(s):
165+
return '\x1b[1m' + s + '\x1b[0m'
166+
167+
def cyan(s):
168+
return '\x1b[36m' + s + '\x1b[0m'
169+
170+
def green(s):
171+
return '\x1b[32m' + s + '\x1b[0m'
172+
173+
def red(s):
174+
return '\x1b[31m' + s + '\x1b[0m'
175+
176+
for line in diff_lines:
177+
if line[:4] in ['--- ', '+++ ']:
178+
yield bold(line)
179+
elif line.startswith('@@ '):
180+
yield cyan(line)
181+
elif line.startswith('+'):
182+
yield green(line)
183+
elif line.startswith('-'):
184+
yield red(line)
185+
else:
186+
yield line
187+
188+
189+
def print_diff(diff_lines, use_color):
190+
if use_color:
191+
diff_lines = colorize(diff_lines)
192+
if sys.version_info[0] < 3:
193+
sys.stdout.writelines((l.encode('utf-8') for l in diff_lines))
194+
else:
195+
sys.stdout.writelines(diff_lines)
196+
197+
198+
def print_trouble(prog, message, use_colors):
199+
error_text = 'error:'
200+
if use_colors:
201+
error_text = bold_red(error_text)
202+
print("{}: {} {}".format(prog, error_text, message), file=sys.stderr)
203+
204+
205+
def main():
206+
parser = argparse.ArgumentParser(description=__doc__)
207+
parser.add_argument(
208+
'--clang-format-executable',
209+
metavar='EXECUTABLE',
210+
help='path to the clang-format executable',
211+
default='clang-format')
212+
parser.add_argument(
213+
'--extensions',
214+
help='comma separated list of file extensions (default: {})'.format(
215+
DEFAULT_EXTENSIONS),
216+
default=DEFAULT_EXTENSIONS)
217+
parser.add_argument(
218+
'-r',
219+
'--recursive',
220+
action='store_true',
221+
help='run recursively over directories')
222+
parser.add_argument('files', metavar='file', nargs='+')
223+
parser.add_argument(
224+
'-q',
225+
'--quiet',
226+
action='store_true')
227+
parser.add_argument(
228+
'-j',
229+
metavar='N',
230+
type=int,
231+
default=0,
232+
help='run N clang-format jobs in parallel'
233+
' (default number of cpus + 1)')
234+
parser.add_argument(
235+
'--color',
236+
default='auto',
237+
choices=['auto', 'always', 'never'],
238+
help='show colored diff (default: auto)')
239+
parser.add_argument(
240+
'-e',
241+
'--exclude',
242+
metavar='PATTERN',
243+
action='append',
244+
default=[],
245+
help='exclude paths matching the given glob-like pattern(s)'
246+
' from recursive search')
247+
248+
args = parser.parse_args()
249+
250+
# use default signal handling, like diff return SIGINT value on ^C
251+
# https://bugs.python.org/issue14229#msg156446
252+
signal.signal(signal.SIGINT, signal.SIG_DFL)
253+
try:
254+
signal.SIGPIPE
255+
except AttributeError:
256+
# compatibility, SIGPIPE does not exist on Windows
257+
pass
258+
else:
259+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
260+
261+
colored_stdout = False
262+
colored_stderr = False
263+
if args.color == 'always':
264+
colored_stdout = True
265+
colored_stderr = True
266+
elif args.color == 'auto':
267+
colored_stdout = sys.stdout.isatty()
268+
colored_stderr = sys.stderr.isatty()
269+
270+
retcode = ExitStatus.SUCCESS
271+
files = list_files(
272+
args.files,
273+
recursive=args.recursive,
274+
exclude=args.exclude,
275+
extensions=args.extensions.split(','))
276+
277+
if not files:
278+
return
279+
280+
njobs = args.j
281+
if njobs == 0:
282+
njobs = multiprocessing.cpu_count() + 1
283+
njobs = min(len(files), njobs)
284+
285+
if njobs == 1:
286+
# execute directly instead of in a pool,
287+
# less overhead, simpler stacktraces
288+
it = (run_clang_format_diff_wrapper(args, file) for file in files)
289+
pool = None
290+
else:
291+
pool = multiprocessing.Pool(njobs)
292+
it = pool.imap_unordered(
293+
partial(run_clang_format_diff_wrapper, args), files)
294+
while True:
295+
try:
296+
outs, errs = next(it)
297+
except StopIteration:
298+
break
299+
except DiffError as e:
300+
print_trouble(parser.prog, str(e), use_colors=colored_stderr)
301+
retcode = ExitStatus.TROUBLE
302+
sys.stderr.writelines(e.errs)
303+
except UnexpectedError as e:
304+
print_trouble(parser.prog, str(e), use_colors=colored_stderr)
305+
sys.stderr.write(e.formatted_traceback)
306+
retcode = ExitStatus.TROUBLE
307+
# stop at the first unexpected error,
308+
# something could be very wrong,
309+
# don't process all files unnecessarily
310+
if pool:
311+
pool.terminate()
312+
break
313+
else:
314+
sys.stderr.writelines(errs)
315+
if outs == []:
316+
continue
317+
if not args.quiet:
318+
print_diff(outs, use_color=colored_stdout)
319+
if retcode == ExitStatus.SUCCESS:
320+
retcode = ExitStatus.DIFF
321+
return retcode
322+
323+
324+
if __name__ == '__main__':
325+
sys.exit(main())

0 commit comments

Comments
 (0)