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