1#!/usr/bin/env python 2"""A wrapper script around clang-format, suitable for linting multiple files 3and to use for continuous integration. 4This is an alternative API for the clang-format command line. 5It runs over multiple files and directories in parallel. 6A diff output is produced and a sensible exit code is returned. 7 8NOTE: pulled from https://github.com/Sarcasm/run-clang-format, which is 9licensed under the MIT license. 10""" 11 12from __future__ import print_function, unicode_literals 13 14import argparse 15import codecs 16import difflib 17import fnmatch 18import io 19import multiprocessing 20import os 21import signal 22import subprocess 23import sys 24import traceback 25 26from functools import partial 27 28try: 29 from subprocess import DEVNULL # py3k 30except ImportError: 31 DEVNULL = open(os.devnull, "wb") 32 33 34DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' 35 36 37class ExitStatus: 38 SUCCESS = 0 39 DIFF = 1 40 TROUBLE = 2 41 42 43def 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 75def 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 85class DiffError(Exception): 86 def __init__(self, message, errs=None): 87 super(DiffError, self).__init__(message) 88 self.errs = errs or [] 89 90 91class 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 98def 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 109def 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 174def bold_red(s): 175 return '\x1b[1m\x1b[31m' + s + '\x1b[0m' 176 177 178def 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 204def 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 213def 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 220def 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 355if __name__ == '__main__': 356 sys.exit(main())