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