style.py revision 11322:01b16bab6675
1#! /usr/bin/env python 2# Copyright (c) 2014 ARM Limited 3# All rights reserved 4# 5# The license below extends only to copyright in the software and shall 6# not be construed as granting a license to any other intellectual 7# property including but not limited to intellectual property relating 8# to a hardware implementation of the functionality of the software 9# licensed hereunder. You may use the software subject to the license 10# terms below provided that you ensure that this notice is replicated 11# unmodified and in its entirety in all distributions of the software, 12# modified or unmodified, in source code or in binary form. 13# 14# Copyright (c) 2006 The Regents of The University of Michigan 15# Copyright (c) 2007,2011 The Hewlett-Packard Development Company 16# Copyright (c) 2016 Advanced Micro Devices, Inc. 17# All rights reserved. 18# 19# Redistribution and use in source and binary forms, with or without 20# modification, are permitted provided that the following conditions are 21# met: redistributions of source code must retain the above copyright 22# notice, this list of conditions and the following disclaimer; 23# redistributions in binary form must reproduce the above copyright 24# notice, this list of conditions and the following disclaimer in the 25# documentation and/or other materials provided with the distribution; 26# neither the name of the copyright holders nor the names of its 27# contributors may be used to endorse or promote products derived from 28# this software without specific prior written permission. 29# 30# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 31# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 32# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 33# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 34# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 35# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 36# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 37# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 38# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 39# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 40# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 41# 42# Authors: Nathan Binkert 43# Steve Reinhardt 44 45import heapq 46import os 47import re 48import sys 49 50from os.path import dirname, join as joinpath 51from itertools import count 52from mercurial import bdiff, mdiff, commands 53 54current_dir = dirname(__file__) 55sys.path.insert(0, current_dir) 56sys.path.insert(1, joinpath(dirname(current_dir), 'src', 'python')) 57 58from m5.util import neg_inf, pos_inf, Region, Regions 59import sort_includes 60from file_types import lang_type 61 62all_regions = Regions(Region(neg_inf, pos_inf)) 63 64tabsize = 8 65lead = re.compile(r'^([ \t]+)') 66trail = re.compile(r'([ \t]+)$') 67any_control = re.compile(r'\b(if|while|for)([ \t]*)\(') 68 69format_types = set(('C', 'C++')) 70 71 72def re_ignore(expr): 73 """Helper function to create regular expression ignore file 74 matcher functions""" 75 76 rex = re.compile(expr) 77 def match_re(fname): 78 return rex.match(fname) 79 return match_re 80 81# This list contains a list of functions that are called to determine 82# if a file should be excluded from the style matching rules or 83# not. The functions are called with the file name relative to the 84# repository root (without a leading slash) as their argument. A file 85# is excluded if any function in the list returns true. 86style_ignores = [ 87 # Ignore external projects as they are unlikely to follow the gem5 88 # coding convention. 89 re_ignore("^ext/"), 90] 91 92def check_ignores(fname): 93 """Check if a file name matches any of the ignore rules""" 94 95 for rule in style_ignores: 96 if rule(fname): 97 return True 98 99 return False 100 101 102def modified_regions(old_data, new_data): 103 regions = Regions() 104 beg = None 105 for pbeg, pend, fbeg, fend in bdiff.blocks(old_data, new_data): 106 if beg is not None and beg != fbeg: 107 regions.append(beg, fbeg) 108 beg = fend 109 return regions 110 111def modregions(wctx, fname): 112 fctx = wctx.filectx(fname) 113 pctx = fctx.parents() 114 115 file_data = fctx.data() 116 lines = mdiff.splitnewlines(file_data) 117 if len(pctx) in (1, 2): 118 mod_regions = modified_regions(pctx[0].data(), file_data) 119 if len(pctx) == 2: 120 m2 = modified_regions(pctx[1].data(), file_data) 121 # only the lines that are new in both 122 mod_regions &= m2 123 else: 124 mod_regions = Regions() 125 mod_regions.append(0, len(lines)) 126 127 return mod_regions 128 129class UserInterface(object): 130 def __init__(self, verbose=False): 131 self.verbose = verbose 132 133 def prompt(self, prompt, results, default): 134 while True: 135 result = self.do_prompt(prompt, results, default) 136 if result in results: 137 return result 138 139class MercurialUI(UserInterface): 140 def __init__(self, ui, *args, **kwargs): 141 super(MercurialUI, self).__init__(*args, **kwargs) 142 self.ui = ui 143 144 def do_prompt(self, prompt, results, default): 145 return self.ui.prompt(prompt, default=default) 146 147 def write(self, string): 148 self.ui.write(string) 149 150class StdioUI(UserInterface): 151 def do_prompt(self, prompt, results, default): 152 return raw_input(prompt) or default 153 154 def write(self, string): 155 sys.stdout.write(string) 156 157 158class Verifier(object): 159 """Base class for style verifier objects 160 161 Subclasses must define these class attributes: 162 languages = set of strings identifying applicable languages 163 test_name = long descriptive name of test, will be used in 164 messages such as "error in <foo>" or "invalid <foo>" 165 opt_name = short name used to generate command-line options to 166 control the test (--fix-<foo>, --ignore-<foo>, etc.) 167 """ 168 169 def __init__(self, ui, repo, opts): 170 self.ui = ui 171 self.repo = repo 172 # opt_name must be defined as a class attribute of derived classes. 173 # Check test-specific opts first as these have precedence. 174 self.opt_fix = opts.get('fix_' + self.opt_name, False) 175 self.opt_ignore = opts.get('ignore_' + self.opt_name, False) 176 self.opt_skip = opts.get('skip_' + self.opt_name, False) 177 # If no test-specific opts were set, then set based on "-all" opts. 178 if not (self.opt_fix or self.opt_ignore or self.opt_skip): 179 self.opt_fix = opts.get('fix_all', False) 180 self.opt_ignore = opts.get('ignore_all', False) 181 self.opt_skip = opts.get('skip_all', False) 182 183 def __getattr__(self, attr): 184 if attr in ('prompt', 'write'): 185 return getattr(self.ui, attr) 186 187 if attr == 'wctx': 188 try: 189 wctx = repo.workingctx() 190 except: 191 from mercurial import context 192 wctx = context.workingctx(repo) 193 self.wctx = wctx 194 return wctx 195 196 raise AttributeError 197 198 def open(self, filename, mode): 199 filename = self.repo.wjoin(filename) 200 201 try: 202 f = file(filename, mode) 203 except OSError, msg: 204 print 'could not open file %s: %s' % (filename, msg) 205 return None 206 207 return f 208 209 def skip(self, filename): 210 filename = self.repo.wjoin(filename) 211 212 # We never want to handle symlinks, so always skip them: If the location 213 # pointed to is a directory, skip it. If the location is a file inside 214 # the gem5 directory, it will be checked as a file, so symlink can be 215 # skipped. If the location is a file outside gem5, we don't want to 216 # check it anyway. 217 if os.path.islink(filename): 218 return True 219 return lang_type(filename) not in self.languages 220 221 def check(self, filename, regions=all_regions): 222 """Check specified regions of file 'filename'. 223 224 Line-by-line checks can simply provide a check_line() method 225 that returns True if the line is OK and False if it has an 226 error. Verifiers that need a multi-line view (like 227 SortedIncludes) must override this entire function. 228 229 Returns a count of errors (0 if none), though actual non-zero 230 count value is not currently used anywhere. 231 """ 232 233 f = self.open(filename, 'r') 234 235 errors = 0 236 for num,line in enumerate(f): 237 if num not in regions: 238 continue 239 if not self.check_line(line): 240 self.write("invalid %s in %s:%d\n" % \ 241 (self.test_name, filename, num + 1)) 242 if self.ui.verbose: 243 self.write(">>%s<<\n" % line[:-1]) 244 errors += 1 245 return errors 246 247 def fix(self, filename, regions=all_regions): 248 """Fix specified regions of file 'filename'. 249 250 Line-by-line fixes can simply provide a fix_line() method that 251 returns the fixed line. Verifiers that need a multi-line view 252 (like SortedIncludes) must override this entire function. 253 """ 254 255 f = self.open(filename, 'r+') 256 257 lines = list(f) 258 259 f.seek(0) 260 f.truncate() 261 262 for i,line in enumerate(lines): 263 if i in regions: 264 line = self.fix_line(line) 265 266 f.write(line) 267 f.close() 268 269 270 def apply(self, filename, regions=all_regions): 271 """Possibly apply to specified regions of file 'filename'. 272 273 Verifier is skipped if --skip-<test> option was provided or if 274 file is not of an applicable type. Otherwise file is checked 275 and error messages printed. Errors are fixed or ignored if 276 the corresponding --fix-<test> or --ignore-<test> options were 277 provided. If neither, the user is prompted for an action. 278 279 Returns True to abort, False otherwise. 280 """ 281 if not (self.opt_skip or self.skip(filename)): 282 errors = self.check(filename, regions) 283 if errors and not self.opt_ignore: 284 if self.opt_fix: 285 self.fix(filename, regions) 286 else: 287 result = self.ui.prompt("(a)bort, (i)gnore, or (f)ix?", 288 'aif', 'a') 289 if result == 'f': 290 self.fix(filename, regions) 291 elif result == 'a': 292 return True # abort 293 294 return False 295 296 297class Whitespace(Verifier): 298 """Check whitespace. 299 300 Specifically: 301 - No tabs used for indent 302 - No trailing whitespace 303 """ 304 305 languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons')) 306 test_name = 'whitespace' 307 opt_name = 'white' 308 309 def check_line(self, line): 310 match = lead.search(line) 311 if match and match.group(1).find('\t') != -1: 312 return False 313 314 match = trail.search(line) 315 if match: 316 return False 317 318 return True 319 320 def fix_line(self, line): 321 if lead.search(line): 322 newline = '' 323 for i,c in enumerate(line): 324 if c == ' ': 325 newline += ' ' 326 elif c == '\t': 327 newline += ' ' * (tabsize - len(newline) % tabsize) 328 else: 329 newline += line[i:] 330 break 331 332 line = newline 333 334 return line.rstrip() + '\n' 335 336 337class ControlSpace(Verifier): 338 """Check for exactly one space after if/while/for""" 339 340 languages = set(('C', 'C++')) 341 test_name = 'spacing after if/while/for' 342 opt_name = 'control' 343 344 def check_line(self, line): 345 match = any_control.search(line) 346 return not (match and match.group(2) != " ") 347 348 def fix_line(self, line): 349 new_line = any_control.sub(r'\1 (', line) 350 return new_line 351 352 353class SortedIncludes(Verifier): 354 """Check for proper sorting of include statements""" 355 356 languages = sort_includes.default_languages 357 test_name = 'include file order' 358 opt_name = 'include' 359 360 def __init__(self, *args, **kwargs): 361 super(SortedIncludes, self).__init__(*args, **kwargs) 362 self.sort_includes = sort_includes.SortIncludes() 363 364 def check(self, filename, regions=all_regions): 365 f = self.open(filename, 'r') 366 367 lines = [ l.rstrip('\n') for l in f.xreadlines() ] 368 old = ''.join(line + '\n' for line in lines) 369 f.close() 370 371 if len(lines) == 0: 372 return 0 373 374 language = lang_type(filename, lines[0]) 375 sort_lines = list(self.sort_includes(lines, filename, language)) 376 new = ''.join(line + '\n' for line in sort_lines) 377 378 mod = modified_regions(old, new) 379 modified = mod & regions 380 381 if modified: 382 self.write("invalid sorting of includes in %s\n" % (filename)) 383 if self.ui.verbose: 384 for start, end in modified.regions: 385 self.write("bad region [%d, %d)\n" % (start, end)) 386 return 1 387 388 return 0 389 390 def fix(self, filename, regions=all_regions): 391 f = self.open(filename, 'r+') 392 393 old = f.readlines() 394 lines = [ l.rstrip('\n') for l in old ] 395 language = lang_type(filename, lines[0]) 396 sort_lines = list(self.sort_includes(lines, filename, language)) 397 new = ''.join(line + '\n' for line in sort_lines) 398 399 f.seek(0) 400 f.truncate() 401 402 for i,line in enumerate(sort_lines): 403 f.write(line) 404 f.write('\n') 405 f.close() 406 407 408def linelen(line): 409 tabs = line.count('\t') 410 if not tabs: 411 return len(line) 412 413 count = 0 414 for c in line: 415 if c == '\t': 416 count += tabsize - count % tabsize 417 else: 418 count += 1 419 420 return count 421 422class LineLength(Verifier): 423 languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons')) 424 test_name = 'line length' 425 opt_name = 'length' 426 427 def check_line(self, line): 428 return linelen(line) <= 78 429 430 def fix(self, filename, regions=all_regions): 431 self.write("Warning: cannot automatically fix overly long lines.\n") 432 433 434class BoolCompare(Verifier): 435 languages = set(('C', 'C++', 'python')) 436 test_name = 'boolean comparison' 437 opt_name = 'boolcomp' 438 439 regex = re.compile(r'\s*==\s*([Tt]rue|[Ff]alse)\b') 440 441 def check_line(self, line): 442 return self.regex.search(line) == None 443 444 def fix_line(self, line): 445 match = self.regex.search(line) 446 if match: 447 if match.group(1) in ('true', 'True'): 448 line = self.regex.sub('', line) 449 else: 450 self.write("Warning: cannot automatically fix " 451 "comparisons with false/False.\n") 452 return line 453 454 455# list of all verifier classes 456all_verifiers = [ 457 Whitespace, 458 ControlSpace, 459 LineLength, 460 BoolCompare, 461 SortedIncludes 462] 463 464class ValidationStats(object): 465 def __init__(self): 466 self.toolong = 0 467 self.toolong80 = 0 468 self.leadtabs = 0 469 self.trailwhite = 0 470 self.badcontrol = 0 471 self.cret = 0 472 473 def dump(self): 474 print '''\ 475%d violations of lines over 79 chars. %d of which are 80 chars exactly. 476%d cases of whitespace at the end of a line. 477%d cases of tabs to indent. 478%d bad parens after if/while/for. 479%d carriage returns found. 480''' % (self.toolong, self.toolong80, self.trailwhite, self.leadtabs, 481 self.badcontrol, self.cret) 482 483 def __nonzero__(self): 484 return self.toolong or self.toolong80 or self.leadtabs or \ 485 self.trailwhite or self.badcontrol or self.cret 486 487def validate(filename, stats, verbose, exit_code): 488 lang = lang_type(filename) 489 if lang not in format_types: 490 return 491 492 def msg(lineno, line, message): 493 print '%s:%d>' % (filename, lineno + 1), message 494 if verbose > 2: 495 print line 496 497 def bad(): 498 if exit_code is not None: 499 sys.exit(exit_code) 500 501 try: 502 f = file(filename, 'r') 503 except OSError: 504 if verbose > 0: 505 print 'could not open file %s' % filename 506 bad() 507 return 508 509 for i,line in enumerate(f): 510 line = line.rstrip('\n') 511 512 # no carriage returns 513 if line.find('\r') != -1: 514 self.cret += 1 515 if verbose > 1: 516 msg(i, line, 'carriage return found') 517 bad() 518 519 # lines max out at 79 chars 520 llen = linelen(line) 521 if llen > 79: 522 stats.toolong += 1 523 if llen == 80: 524 stats.toolong80 += 1 525 if verbose > 1: 526 msg(i, line, 'line too long (%d chars)' % llen) 527 bad() 528 529 # no tabs used to indent 530 match = lead.search(line) 531 if match and match.group(1).find('\t') != -1: 532 stats.leadtabs += 1 533 if verbose > 1: 534 msg(i, line, 'using tabs to indent') 535 bad() 536 537 # no trailing whitespace 538 if trail.search(line): 539 stats.trailwhite +=1 540 if verbose > 1: 541 msg(i, line, 'trailing whitespace') 542 bad() 543 544 # for c++, exactly one space betwen if/while/for and ( 545 if lang == 'C++': 546 match = any_control.search(line) 547 if match and match.group(2) != " ": 548 stats.badcontrol += 1 549 if verbose > 1: 550 msg(i, line, 'improper spacing after %s' % match.group(1)) 551 bad() 552 553 554def _modified_regions(repo, patterns, **kwargs): 555 opt_all = kwargs.get('all', False) 556 opt_no_ignore = kwargs.get('no_ignore', False) 557 558 # Import the match (repository file name matching helper) 559 # function. Different versions of Mercurial keep it in different 560 # modules and implement them differently. 561 try: 562 from mercurial import scmutil 563 m = scmutil.match(repo[None], patterns, kwargs) 564 except ImportError: 565 from mercurial import cmdutil 566 m = cmdutil.match(repo, patterns, kwargs) 567 568 modified, added, removed, deleted, unknown, ignore, clean = \ 569 repo.status(match=m, clean=opt_all) 570 571 if not opt_all: 572 try: 573 wctx = repo.workingctx() 574 except: 575 from mercurial import context 576 wctx = context.workingctx(repo) 577 578 files = [ (fn, all_regions) for fn in added ] + \ 579 [ (fn, modregions(wctx, fn)) for fn in modified ] 580 else: 581 files = [ (fn, all_regions) for fn in added + modified + clean ] 582 583 for fname, mod_regions in files: 584 if opt_no_ignore or not check_ignores(fname): 585 yield fname, mod_regions 586 587 588def do_check_style(hgui, repo, *pats, **opts): 589 """check files for proper m5 style guidelines 590 591 Without an argument, checks all modified and added files for gem5 592 coding style violations. A list of files can be specified to limit 593 the checker to a subset of the repository. The style rules are 594 normally applied on a diff of the repository state (i.e., added 595 files are checked in their entirety while only modifications of 596 modified files are checked). 597 598 The --all option can be specified to include clean files and check 599 modified files in their entirety. 600 601 The --fix-<check>, --ignore-<check>, and --skip-<check> options 602 can be used to control individual style checks: 603 604 --fix-<check> will perform the check and automatically attempt to 605 fix sny style error (printing a warning if unsuccessful) 606 607 --ignore-<check> will perform the check but ignore any errors 608 found (other than printing a message for each) 609 610 --skip-<check> will skip performing the check entirely 611 612 If none of these options are given, all checks will be performed 613 and the user will be prompted on how to handle each error. 614 615 --fix-all, --ignore-all, and --skip-all are equivalent to specifying 616 --fix-<check>, --ignore-<check>, or --skip-<check> for all checks, 617 respectively. However, option settings for specific checks take 618 precedence. Thus --skip-all --fix-white can be used to skip every 619 check other than whitespace errors, which will be checked and 620 automatically fixed. 621 622 The -v/--verbose flag will display the offending line(s) as well 623 as their location. 624 """ 625 626 ui = MercurialUI(hgui, verbose=hgui.verbose) 627 628 # instantiate varifier objects 629 verifiers = [v(ui, repo, opts) for v in all_verifiers] 630 631 for fname, mod_regions in _modified_regions(repo, pats, **opts): 632 for verifier in verifiers: 633 if verifier.apply(fname, mod_regions): 634 return True 635 636 return False 637 638def do_check_format(hgui, repo, *pats, **opts): 639 """check files for gem5 code formatting violations 640 641 Without an argument, checks all modified and added files for gem5 642 code formatting violations. A list of files can be specified to 643 limit the checker to a subset of the repository. The style rules 644 are normally applied on a diff of the repository state (i.e., 645 added files are checked in their entirety while only modifications 646 of modified files are checked). 647 648 The --all option can be specified to include clean files and check 649 modified files in their entirety. 650 """ 651 ui = MercurialUI(hgui, hgui.verbose) 652 653 verbose = 0 654 for fname, mod_regions in _modified_regions(repo, pats, **opts): 655 stats = ValidationStats() 656 validate(joinpath(repo.root, fname), stats, verbose, None) 657 if stats: 658 print "%s:" % fname 659 stats.dump() 660 result = ui.prompt("invalid formatting\n(i)gnore or (a)bort?", 661 'ai', 'a') 662 if result == 'a': 663 return True 664 665 return False 666 667def check_hook(hooktype): 668 if hooktype not in ('pretxncommit', 'pre-qrefresh'): 669 raise AttributeError, \ 670 "This hook is not meant for %s" % hooktype 671 672# This function provides a hook that is called before transaction 673# commit and on qrefresh 674def check_style(ui, repo, hooktype, **kwargs): 675 check_hook(hooktype) 676 args = {} 677 678 try: 679 return do_check_style(ui, repo, **args) 680 except Exception, e: 681 import traceback 682 traceback.print_exc() 683 return True 684 685def check_format(ui, repo, hooktype, **kwargs): 686 check_hook(hooktype) 687 args = {} 688 689 try: 690 return do_check_format(ui, repo, **args) 691 except Exception, e: 692 import traceback 693 traceback.print_exc() 694 return True 695 696try: 697 from mercurial.i18n import _ 698except ImportError: 699 def _(arg): 700 return arg 701 702_common_region_options = [ 703 ('a', 'all', False, 704 _("include clean files and unmodified parts of modified files")), 705 ('', 'no-ignore', False, _("ignore the style ignore list")), 706 ] 707 708 709fix_opts = [('f', 'fix-all', False, _("fix all style errors"))] + \ 710 [('', 'fix-' + v.opt_name, False, 711 _('fix errors in ' + v.test_name)) for v in all_verifiers] 712ignore_opts = [('', 'ignore-all', False, _("ignore all style errors"))] + \ 713 [('', 'ignore-' + v.opt_name, False, 714 _('ignore errors in ' + v.test_name)) for v in all_verifiers] 715skip_opts = [('', 'skip-all', False, _("skip all style error checks"))] + \ 716 [('', 'skip-' + v.opt_name, False, 717 _('skip checking for ' + v.test_name)) for v in all_verifiers] 718all_opts = fix_opts + ignore_opts + skip_opts 719 720 721cmdtable = { 722 '^m5style' : ( 723 do_check_style, all_opts + _common_region_options + commands.walkopts, 724 _('hg m5style [-a] [FILE]...')), 725 '^m5format' : 726 ( do_check_format, [ 727 ] + _common_region_options + commands.walkopts, 728 _('hg m5format [FILE]...')), 729} 730 731if __name__ == '__main__': 732 import getopt 733 734 progname = sys.argv[0] 735 if len(sys.argv) < 2: 736 sys.exit('usage: %s <command> [<command args>]' % progname) 737 738 fixwhite_usage = '%s fixwhite [-t <tabsize> ] <path> [...] \n' % progname 739 chkformat_usage = '%s chkformat <path> [...] \n' % progname 740 chkwhite_usage = '%s chkwhite <path> [...] \n' % progname 741 742 command = sys.argv[1] 743 if command == 'fixwhite': 744 flags = 't:' 745 usage = fixwhite_usage 746 elif command == 'chkwhite': 747 flags = 'nv' 748 usage = chkwhite_usage 749 elif command == 'chkformat': 750 flags = 'nv' 751 usage = chkformat_usage 752 else: 753 sys.exit(fixwhite_usage + chkwhite_usage + chkformat_usage) 754 755 opts, args = getopt.getopt(sys.argv[2:], flags) 756 757 code = 1 758 verbose = 1 759 for opt,arg in opts: 760 if opt == '-n': 761 code = None 762 if opt == '-t': 763 tabsize = int(arg) 764 if opt == '-v': 765 verbose += 1 766 767 if command == 'fixwhite': 768 for filename in args: 769 fixwhite(filename, tabsize) 770 elif command == 'chkwhite': 771 for filename in args: 772 for line,num in checkwhite(filename): 773 print 'invalid whitespace: %s:%d' % (filename, num) 774 if verbose: 775 print '>>%s<<' % line[:-1] 776 elif command == 'chkformat': 777 stats = ValidationStats() 778 for filename in args: 779 validate(filename, stats=stats, verbose=verbose, exit_code=code) 780 781 if verbose > 0: 782 stats.dump() 783 else: 784 sys.exit("command '%s' not found" % command) 785