"Fossies" - the Fresh Open Source Software Archive

Member "codespell-2.0.0/codespell_lib/_codespell.py" (23 Nov 2020, 32319 Bytes) of package /linux/misc/codespell-2.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "_codespell.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.17.1_vs_2.0.0.

    1 # -*- coding: utf-8 -*-
    2 #
    3 # This program is free software; you can redistribute it and/or modify
    4 # it under the terms of the GNU General Public License as published by
    5 # the Free Software Foundation; version 2 of the License.
    6 #
    7 # This program is distributed in the hope that it will be useful,
    8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   10 # GNU General Public License for more details.
   11 #
   12 # You should have received a copy of the GNU General Public License
   13 # along with this program; if not, see
   14 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
   15 """
   16 Copyright (C) 2010-2011  Lucas De Marchi <lucas.de.marchi@gmail.com>
   17 Copyright (C) 2011  ProFUSION embedded systems
   18 """
   19 
   20 from __future__ import print_function
   21 
   22 import argparse
   23 import codecs
   24 import configparser
   25 import fnmatch
   26 import os
   27 import re
   28 import sys
   29 import textwrap
   30 
   31 word_regex_def = u"[\\w\\-'’`]+"
   32 encodings = ('utf-8', 'iso-8859-1')
   33 USAGE = """
   34 \t%prog [OPTIONS] [file1 file2 ... fileN]
   35 """
   36 VERSION = '2.0.0'
   37 
   38 supported_languages_en = ('en', 'en_GB', 'en_US', 'en_CA', 'en_AU')
   39 supported_languages = supported_languages_en
   40 
   41 # Users might want to link this file into /usr/local/bin, so we resolve the
   42 # symbolic link path to the real path if necessary.
   43 _data_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data')
   44 _builtin_dictionaries = (
   45     # name, desc, name, err in aspell, correction in aspell, \
   46     # err dictionary array, rep dictionary array
   47     # The arrays must contain the names of aspell dictionaries
   48     # The aspell tests here aren't the ideal state, but the None's are
   49     # realistic for obscure words
   50     ('clear', 'for unambiguous errors', '',
   51         False, None, supported_languages_en, None),
   52     ('rare', 'for rare but valid words', '_rare',
   53         None, None, None, None),
   54     ('informal', 'for informal words', '_informal',
   55         True, True, supported_languages_en, supported_languages_en),
   56     ('usage', 'for recommended terms', '_usage',
   57         None, None, None, None),
   58     ('code', 'for words common to code and/or mathematics', '_code',
   59         None, None, None, None,),
   60     ('names', 'for valid proper names that might be typos', '_names',
   61         None, None, None, None,),
   62     ('en-GB_to_en-US', 'for corrections from en-GB to en-US', '_en-GB_to_en-US',  # noqa: E501
   63         True, True, ('en_GB',), ('en_US',)),
   64 )
   65 _builtin_default = 'clear,rare'
   66 
   67 # docs say os.EX_USAGE et al. are only available on Unix systems, so to be safe
   68 # we protect and just use the values they are on macOS and Linux
   69 EX_OK = 0
   70 EX_USAGE = 64
   71 EX_DATAERR = 65
   72 
   73 # OPTIONS:
   74 #
   75 # ARGUMENTS:
   76 #    dict_filename       The file containing the dictionary of misspellings.
   77 #                        If set to '-', it will be read from stdin
   78 #    file1 .. fileN      Files to check spelling
   79 
   80 
   81 class QuietLevels(object):
   82     NONE = 0
   83     ENCODING = 1
   84     BINARY_FILE = 2
   85     DISABLED_FIXES = 4
   86     NON_AUTOMATIC_FIXES = 8
   87     FIXES = 16
   88 
   89 
   90 class GlobMatch(object):
   91     def __init__(self, pattern):
   92         if pattern:
   93             # Pattern might be a list of comma-delimited strings
   94             self.pattern_list = ','.join(pattern).split(',')
   95         else:
   96             self.pattern_list = None
   97 
   98     def match(self, filename):
   99         if self.pattern_list is None:
  100             return False
  101 
  102         for p in self.pattern_list:
  103             if fnmatch.fnmatch(filename, p):
  104                 return True
  105 
  106         return False
  107 
  108 
  109 class Misspelling(object):
  110     def __init__(self, data, fix, reason):
  111         self.data = data
  112         self.fix = fix
  113         self.reason = reason
  114 
  115 
  116 class TermColors(object):
  117     def __init__(self):
  118         self.FILE = '\033[33m'
  119         self.WWORD = '\033[31m'
  120         self.FWORD = '\033[32m'
  121         self.DISABLE = '\033[0m'
  122 
  123     def disable(self):
  124         self.FILE = ''
  125         self.WWORD = ''
  126         self.FWORD = ''
  127         self.DISABLE = ''
  128 
  129 
  130 class Summary(object):
  131     def __init__(self):
  132         self.summary = {}
  133 
  134     def update(self, wrongword):
  135         if wrongword in self.summary:
  136             self.summary[wrongword] += 1
  137         else:
  138             self.summary[wrongword] = 1
  139 
  140     def __str__(self):
  141         keys = list(self.summary.keys())
  142         keys.sort()
  143 
  144         return "\n".join(["{0}{1:{width}}".format(
  145                          key,
  146                          self.summary.get(key),
  147                          width=15 - len(key)) for key in keys])
  148 
  149 
  150 class FileOpener(object):
  151     def __init__(self, use_chardet, quiet_level):
  152         self.use_chardet = use_chardet
  153         if use_chardet:
  154             self.init_chardet()
  155         self.quiet_level = quiet_level
  156 
  157     def init_chardet(self):
  158         try:
  159             from chardet.universaldetector import UniversalDetector
  160         except ImportError:
  161             raise ImportError("There's no chardet installed to import from. "
  162                               "Please, install it and check your PYTHONPATH "
  163                               "environment variable")
  164 
  165         self.encdetector = UniversalDetector()
  166 
  167     def open(self, filename):
  168         if self.use_chardet:
  169             return self.open_with_chardet(filename)
  170         else:
  171             return self.open_with_internal(filename)
  172 
  173     def open_with_chardet(self, filename):
  174         self.encdetector.reset()
  175         with codecs.open(filename, 'rb') as f:
  176             for line in f:
  177                 self.encdetector.feed(line)
  178                 if self.encdetector.done:
  179                     break
  180         self.encdetector.close()
  181         encoding = self.encdetector.result['encoding']
  182 
  183         try:
  184             f = codecs.open(filename, 'r', encoding=encoding)
  185         except UnicodeDecodeError:
  186             print("ERROR: Could not detect encoding: %s" % filename,
  187                   file=sys.stderr)
  188             raise
  189         except LookupError:
  190             print("ERROR: Don't know how to handle encoding %s: %s"
  191                   % (encoding, filename,), file=sys.stderr)
  192             raise
  193         else:
  194             lines = f.readlines()
  195             f.close()
  196 
  197         return lines, encoding
  198 
  199     def open_with_internal(self, filename):
  200         curr = 0
  201         while True:
  202             try:
  203                 f = codecs.open(filename, 'r', encoding=encodings[curr])
  204             except UnicodeDecodeError:
  205                 if not self.quiet_level & QuietLevels.ENCODING:
  206                     print("WARNING: Decoding file using encoding=%s failed: %s"
  207                           % (encodings[curr], filename,), file=sys.stderr)
  208                     try:
  209                         print("WARNING: Trying next encoding %s"
  210                               % encodings[curr + 1], file=sys.stderr)
  211                     except IndexError:
  212                         pass
  213 
  214                 curr += 1
  215             else:
  216                 lines = f.readlines()
  217                 f.close()
  218                 break
  219         if not lines:
  220             raise Exception('Unknown encoding')
  221 
  222         encoding = encodings[curr]
  223 
  224         return lines, encoding
  225 
  226 # -.-:-.-:-.-:-.:-.-:-.-:-.-:-.-:-.:-.-:-.-:-.-:-.-:-.:-.-:-
  227 
  228 
  229 # If someday this breaks, we can just switch to using RawTextHelpFormatter,
  230 # but it has the disadvantage of not wrapping our long lines.
  231 
  232 class NewlineHelpFormatter(argparse.HelpFormatter):
  233     """Help formatter that preserves newlines and deals with lists."""
  234 
  235     def _split_lines(self, text, width):
  236         parts = text.split('\n')
  237         out = list()
  238         for pi, part in enumerate(parts):
  239             # Eventually we could allow others...
  240             indent_start = '- '
  241             if part.startswith(indent_start):
  242                 offset = len(indent_start)
  243             else:
  244                 offset = 0
  245             part = part[offset:]
  246             part = self._whitespace_matcher.sub(' ', part).strip()
  247             parts = textwrap.wrap(part, width - offset)
  248             parts = [' ' * offset + p for p in parts]
  249             if offset:
  250                 parts[0] = indent_start + parts[0][offset:]
  251             out.extend(parts)
  252         return out
  253 
  254 
  255 def parse_options(args):
  256     parser = argparse.ArgumentParser(formatter_class=NewlineHelpFormatter)
  257 
  258     parser.set_defaults(colors=sys.stdout.isatty())
  259     parser.add_argument('--version', action='version', version=VERSION)
  260 
  261     parser.add_argument('-d', '--disable-colors',
  262                         action='store_false', dest='colors',
  263                         help='disable colors, even when printing to terminal '
  264                              '(always set for Windows)')
  265     parser.add_argument('-c', '--enable-colors',
  266                         action='store_true', dest='colors',
  267                         help='enable colors, even when not printing to '
  268                              'terminal')
  269 
  270     parser.add_argument('-w', '--write-changes',
  271                         action='store_true', default=False,
  272                         help='write changes in place if possible')
  273 
  274     parser.add_argument('-D', '--dictionary',
  275                         action='append',
  276                         help='custom dictionary file that contains spelling '
  277                              'corrections. If this flag is not specified or '
  278                              'equals "-" then the default dictionary is used. '
  279                              'This option can be specified multiple times.')
  280     builtin_opts = '\n- '.join([''] + [
  281         '%r %s' % (d[0], d[1]) for d in _builtin_dictionaries])
  282     parser.add_argument('--builtin',
  283                         dest='builtin', default=_builtin_default,
  284                         metavar='BUILTIN-LIST',
  285                         help='comma-separated list of builtin dictionaries '
  286                         'to include (when "-D -" or no "-D" is passed). '
  287                         'Current options are:' + builtin_opts + '\n'
  288                         'The default is %(default)r.')
  289     parser.add_argument('--ignore-regex',
  290                         action='store', type=str,
  291                         help='regular expression which is used to find '
  292                              'patterns to ignore by treating as whitespace. '
  293                              'When writing regexes, consider ensuring there '
  294                              'are boundary non-word chars, e.g., '
  295                              '"\\Wmatch\\W". Defaults to empty/disabled.')
  296     parser.add_argument('-I', '--ignore-words',
  297                         action='append', metavar='FILE',
  298                         help='file that contains words which will be ignored '
  299                              'by codespell. File must contain 1 word per line.'
  300                              ' Words are case sensitive based on how they are '
  301                              'written in the dictionary file')
  302     parser.add_argument('-L', '--ignore-words-list',
  303                         action='append', metavar='WORDS',
  304                         help='comma separated list of words to be ignored '
  305                              'by codespell. Words are case sensitive based on '
  306                              'how they are written in the dictionary file')
  307     parser.add_argument('-r', '--regex',
  308                         action='store', type=str,
  309                         help='regular expression which is used to find words. '
  310                              'By default any alphanumeric character, the '
  311                              'underscore, the hyphen, and the apostrophe is '
  312                              'used to build words. This option cannot be '
  313                              'specified together with --write-changes.')
  314     parser.add_argument('-s', '--summary',
  315                         action='store_true', default=False,
  316                         help='print summary of fixes')
  317 
  318     parser.add_argument('--count',
  319                         action='store_true', default=False,
  320                         help='print the number of errors as the last line of '
  321                              'stderr')
  322 
  323     parser.add_argument('-S', '--skip',
  324                         action='append',
  325                         help='comma-separated list of files to skip. It '
  326                              'accepts globs as well. E.g.: if you want '
  327                              'codespell to skip .eps and .txt files, '
  328                              'you\'d give "*.eps,*.txt" to this option.')
  329 
  330     parser.add_argument('-x', '--exclude-file', type=str, metavar='FILE',
  331                         help='FILE with lines that should not be checked for '
  332                              'errors or changed')
  333 
  334     parser.add_argument('-i', '--interactive',
  335                         action='store', type=int, default=0,
  336                         help='set interactive mode when writing changes:\n'
  337                              '- 0: no interactivity.\n'
  338                              '- 1: ask for confirmation.\n'
  339                              '- 2: ask user to choose one fix when more than one is available.\n'  # noqa: E501
  340                              '- 3: both 1 and 2')
  341 
  342     parser.add_argument('-q', '--quiet-level',
  343                         action='store', type=int, default=2,
  344                         help='bitmask that allows suppressing messages:\n'
  345                              '- 0: print all messages.\n'
  346                              '- 1: disable warnings about wrong encoding.\n'
  347                              '- 2: disable warnings about binary files.\n'
  348                              '- 4: omit warnings about automatic fixes that were disabled in the dictionary.\n'  # noqa: E501
  349                              '- 8: don\'t print anything for non-automatic fixes.\n'  # noqa: E501
  350                              '- 16: don\'t print the list of fixed files.\n'
  351                              'As usual with bitmasks, these levels can be '
  352                              'combined; e.g. use 3 for levels 1+2, 7 for '
  353                              '1+2+4, 23 for 1+2+4+16, etc. '
  354                              'The default mask is %(default)s.')
  355 
  356     parser.add_argument('-e', '--hard-encoding-detection',
  357                         action='store_true', default=False,
  358                         help='use chardet to detect the encoding of each '
  359                              'file. This can slow down codespell, but is more '
  360                              'reliable in detecting encodings other than '
  361                              'utf-8, iso8859-1, and ascii.')
  362 
  363     parser.add_argument('-f', '--check-filenames',
  364                         action='store_true', default=False,
  365                         help='check file names as well')
  366 
  367     parser.add_argument('-H', '--check-hidden',
  368                         action='store_true', default=False,
  369                         help='check hidden files and directories (those '
  370                              'starting with ".") as well.')
  371     parser.add_argument('-A', '--after-context', type=int, metavar='LINES',
  372                         help='print LINES of trailing context')
  373     parser.add_argument('-B', '--before-context', type=int, metavar='LINES',
  374                         help='print LINES of leading context')
  375     parser.add_argument('-C', '--context', type=int, metavar='LINES',
  376                         help='print LINES of surrounding context')
  377     parser.add_argument('--config', type=str,
  378                         help='path to config file.')
  379 
  380     parser.add_argument('files', nargs='*',
  381                         help='files or directories to check')
  382 
  383     # Parse command line options.
  384     options = parser.parse_args(list(args))
  385 
  386     # Load config files and look for ``codespell`` options.
  387     cfg_files = ['setup.cfg', '.codespellrc']
  388     if options.config:
  389         cfg_files.append(options.config)
  390     config = configparser.ConfigParser()
  391     config.read(cfg_files)
  392 
  393     if config.has_section('codespell'):
  394         # Build a "fake" argv list using option name and value.
  395         cfg_args = []
  396         for key in config['codespell']:
  397             # Add option as arg.
  398             cfg_args.append("--%s" % key)
  399             # If value is blank, skip.
  400             val = config['codespell'][key]
  401             if val != "":
  402                 cfg_args.append(val)
  403 
  404         # Parse config file options.
  405         options = parser.parse_args(cfg_args)
  406 
  407         # Re-parse command line options to override config.
  408         options = parser.parse_args(list(args), namespace=options)
  409 
  410     if not options.files:
  411         options.files.append('.')
  412 
  413     return options, parser
  414 
  415 
  416 def build_exclude_hashes(filename, exclude_lines):
  417     with codecs.open(filename, 'r') as f:
  418         for line in f:
  419             exclude_lines.add(line)
  420 
  421 
  422 def build_ignore_words(filename, ignore_words):
  423     with codecs.open(filename, mode='r', encoding='utf-8') as f:
  424         for line in f:
  425             ignore_words.add(line.strip())
  426 
  427 
  428 def build_dict(filename, misspellings, ignore_words):
  429     with codecs.open(filename, mode='r', encoding='utf-8') as f:
  430         for line in f:
  431             [key, data] = line.split('->')
  432             # TODO for now, convert both to lower. Someday we can maybe add
  433             # support for fixing caps.
  434             key = key.lower()
  435             data = data.lower()
  436             if key in ignore_words:
  437                 continue
  438             data = data.strip()
  439             fix = data.rfind(',')
  440 
  441             if fix < 0:
  442                 fix = True
  443                 reason = ''
  444             elif fix == (len(data) - 1):
  445                 data = data[:fix]
  446                 reason = ''
  447                 fix = False
  448             else:
  449                 reason = data[fix + 1:].strip()
  450                 data = data[:fix]
  451                 fix = False
  452 
  453             misspellings[key] = Misspelling(data, fix, reason)
  454 
  455 
  456 def is_hidden(filename, check_hidden):
  457     bfilename = os.path.basename(filename)
  458 
  459     return bfilename not in ('', '.', '..') and \
  460         (not check_hidden and bfilename[0] == '.')
  461 
  462 
  463 def is_text_file(filename):
  464     with open(filename, mode='rb') as f:
  465         s = f.read(1024)
  466     if b'\x00' in s:
  467         return False
  468     return True
  469 
  470 
  471 def fix_case(word, fixword):
  472     if word == word.capitalize():
  473         return fixword.capitalize()
  474     elif word == word.upper():
  475         return fixword.upper()
  476     # they are both lower case
  477     # or we don't have any idea
  478     return fixword
  479 
  480 
  481 def ask_for_word_fix(line, wrongword, misspelling, interactivity):
  482     if interactivity <= 0:
  483         return misspelling.fix, fix_case(wrongword, misspelling.data)
  484 
  485     if misspelling.fix and interactivity & 1:
  486         r = ''
  487         fixword = fix_case(wrongword, misspelling.data)
  488         while not r:
  489             print("%s\t%s ==> %s (Y/n) " % (line, wrongword, fixword), end='')
  490             r = sys.stdin.readline().strip().upper()
  491             if not r:
  492                 r = 'Y'
  493             if r != 'Y' and r != 'N':
  494                 print("Say 'y' or 'n'")
  495                 r = ''
  496 
  497         if r == 'N':
  498             misspelling.fix = False
  499             misspelling.fixword = ''
  500 
  501     elif (interactivity & 2) and not misspelling.reason:
  502         # if it is not disabled, i.e. it just has more than one possible fix,
  503         # we ask the user which word to use
  504 
  505         r = ''
  506         opt = list(map(lambda x: x.strip(), misspelling.data.split(',')))
  507         while not r:
  508             print("%s Choose an option (blank for none): " % line, end='')
  509             for i in range(len(opt)):
  510                 fixword = fix_case(wrongword, opt[i])
  511                 print(" %d) %s" % (i, fixword), end='')
  512             print(": ", end='')
  513             sys.stdout.flush()
  514 
  515             n = sys.stdin.readline().strip()
  516             if not n:
  517                 break
  518 
  519             try:
  520                 n = int(n)
  521                 r = opt[n]
  522             except (ValueError, IndexError):
  523                 print("Not a valid option\n")
  524 
  525         if r:
  526             misspelling.fix = True
  527             misspelling.data = r
  528 
  529     return misspelling.fix, fix_case(wrongword, misspelling.data)
  530 
  531 
  532 def print_context(lines, index, context):
  533     # context = (context_before, context_after)
  534     for i in range(index - context[0], index + context[1] + 1):
  535         if 0 <= i < len(lines):
  536             print('%s %s' % ('>' if i == index else ':', lines[i].rstrip()))
  537 
  538 
  539 def extract_words(text, word_regex, ignore_word_regex):
  540     if ignore_word_regex:
  541         text = ignore_word_regex.sub(' ', text)
  542     return word_regex.findall(text)
  543 
  544 
  545 def parse_file(filename, colors, summary, misspellings, exclude_lines,
  546                file_opener, word_regex, ignore_word_regex, context, options):
  547     bad_count = 0
  548     lines = None
  549     changed = False
  550     encoding = encodings[0]  # if not defined, use UTF-8
  551 
  552     if filename == '-':
  553         f = sys.stdin
  554         lines = f.readlines()
  555     else:
  556         if options.check_filenames:
  557             for word in extract_words(filename, word_regex, ignore_word_regex):
  558                 lword = word.lower()
  559                 if lword not in misspellings:
  560                     continue
  561                 fix = misspellings[lword].fix
  562                 fixword = fix_case(word, misspellings[lword].data)
  563 
  564                 if summary and fix:
  565                     summary.update(lword)
  566 
  567                 cfilename = "%s%s%s" % (colors.FILE, filename, colors.DISABLE)
  568                 cwrongword = "%s%s%s" % (colors.WWORD, word, colors.DISABLE)
  569                 crightword = "%s%s%s" % (colors.FWORD, fixword, colors.DISABLE)
  570 
  571                 if misspellings[lword].reason:
  572                     if options.quiet_level & QuietLevels.DISABLED_FIXES:
  573                         continue
  574                     creason = "  | %s%s%s" % (colors.FILE,
  575                                               misspellings[lword].reason,
  576                                               colors.DISABLE)
  577                 else:
  578                     if options.quiet_level & QuietLevels.NON_AUTOMATIC_FIXES:
  579                         continue
  580                     creason = ''
  581 
  582                 bad_count += 1
  583 
  584                 print("%(FILENAME)s: %(WRONGWORD)s"
  585                       " ==> %(RIGHTWORD)s%(REASON)s"
  586                       % {'FILENAME': cfilename,
  587                          'WRONGWORD': cwrongword,
  588                          'RIGHTWORD': crightword, 'REASON': creason})
  589 
  590         # ignore irregular files
  591         if not os.path.isfile(filename):
  592             return bad_count
  593 
  594         text = is_text_file(filename)
  595         if not text:
  596             if not options.quiet_level & QuietLevels.BINARY_FILE:
  597                 print("WARNING: Binary file: %s" % filename, file=sys.stderr)
  598             return bad_count
  599         try:
  600             lines, encoding = file_opener.open(filename)
  601         except Exception:
  602             return bad_count
  603 
  604     for i, line in enumerate(lines):
  605         if line in exclude_lines:
  606             continue
  607 
  608         fixed_words = set()
  609         asked_for = set()
  610 
  611         for word in extract_words(line, word_regex, ignore_word_regex):
  612             lword = word.lower()
  613             if lword in misspellings:
  614                 context_shown = False
  615                 fix = misspellings[lword].fix
  616                 fixword = fix_case(word, misspellings[lword].data)
  617 
  618                 if options.interactive and lword not in asked_for:
  619                     if context is not None:
  620                         context_shown = True
  621                         print_context(lines, i, context)
  622                     fix, fixword = ask_for_word_fix(
  623                         lines[i], word, misspellings[lword],
  624                         options.interactive)
  625                     asked_for.add(lword)
  626 
  627                 if summary and fix:
  628                     summary.update(lword)
  629 
  630                 if word in fixed_words:  # can skip because of re.sub below
  631                     continue
  632 
  633                 if options.write_changes and fix:
  634                     changed = True
  635                     lines[i] = re.sub(r'\b%s\b' % word, fixword, lines[i])
  636                     fixed_words.add(word)
  637                     continue
  638 
  639                 # otherwise warning was explicitly set by interactive mode
  640                 if (options.interactive & 2 and not fix and not
  641                         misspellings[lword].reason):
  642                     continue
  643 
  644                 cfilename = "%s%s%s" % (colors.FILE, filename, colors.DISABLE)
  645                 cline = "%s%d%s" % (colors.FILE, i + 1, colors.DISABLE)
  646                 cwrongword = "%s%s%s" % (colors.WWORD, word, colors.DISABLE)
  647                 crightword = "%s%s%s" % (colors.FWORD, fixword, colors.DISABLE)
  648 
  649                 if misspellings[lword].reason:
  650                     if options.quiet_level & QuietLevels.DISABLED_FIXES:
  651                         continue
  652 
  653                     creason = "  | %s%s%s" % (colors.FILE,
  654                                               misspellings[lword].reason,
  655                                               colors.DISABLE)
  656                 else:
  657                     if options.quiet_level & QuietLevels.NON_AUTOMATIC_FIXES:
  658                         continue
  659 
  660                     creason = ''
  661 
  662                 # If we get to this point (uncorrected error) we should change
  663                 # our bad_count and thus return value
  664                 bad_count += 1
  665 
  666                 if (not context_shown) and (context is not None):
  667                     print_context(lines, i, context)
  668                 if filename != '-':
  669                     print("%(FILENAME)s:%(LINE)s: %(WRONGWORD)s "
  670                           "==> %(RIGHTWORD)s%(REASON)s"
  671                           % {'FILENAME': cfilename, 'LINE': cline,
  672                              'WRONGWORD': cwrongword,
  673                              'RIGHTWORD': crightword, 'REASON': creason})
  674                 else:
  675                     print("%(LINE)s: %(STRLINE)s\n\t%(WRONGWORD)s "
  676                           "==> %(RIGHTWORD)s%(REASON)s"
  677                           % {'LINE': cline, 'STRLINE': line.strip(),
  678                              'WRONGWORD': cwrongword,
  679                              'RIGHTWORD': crightword, 'REASON': creason})
  680 
  681     if changed:
  682         if filename == '-':
  683             print("---")
  684             for line in lines:
  685                 print(line, end='')
  686         else:
  687             if not options.quiet_level & QuietLevels.FIXES:
  688                 print("%sFIXED:%s %s"
  689                       % (colors.FWORD, colors.DISABLE, filename),
  690                       file=sys.stderr)
  691             with codecs.open(filename, 'w', encoding=encoding) as f:
  692                 f.writelines(lines)
  693     return bad_count
  694 
  695 
  696 def _script_main():
  697     """Wrap to main() for setuptools."""
  698     return main(*sys.argv[1:])
  699 
  700 
  701 def main(*args):
  702     """Contains flow control"""
  703     options, parser = parse_options(args)
  704 
  705     if options.regex and options.write_changes:
  706         print("ERROR: --write-changes cannot be used together with "
  707               "--regex")
  708         parser.print_help()
  709         return EX_USAGE
  710     word_regex = options.regex or word_regex_def
  711     try:
  712         word_regex = re.compile(word_regex)
  713     except re.error as err:
  714         print("ERROR: invalid --regex \"%s\" (%s)" %
  715               (word_regex, err), file=sys.stderr)
  716         parser.print_help()
  717         return EX_USAGE
  718 
  719     if options.ignore_regex:
  720         try:
  721             ignore_word_regex = re.compile(options.ignore_regex)
  722         except re.error as err:
  723             print("ERROR: invalid --ignore-regex \"%s\" (%s)" %
  724                   (options.ignore_regex, err), file=sys.stderr)
  725             parser.print_help()
  726             return EX_USAGE
  727     else:
  728         ignore_word_regex = None
  729 
  730     ignore_words_files = options.ignore_words or []
  731     ignore_words = set()
  732     for ignore_words_file in ignore_words_files:
  733         if not os.path.isfile(ignore_words_file):
  734             print("ERROR: cannot find ignore-words file: %s" %
  735                   ignore_words_file, file=sys.stderr)
  736             parser.print_help()
  737             return EX_USAGE
  738         build_ignore_words(ignore_words_file, ignore_words)
  739 
  740     ignore_words_list = options.ignore_words_list or []
  741     for comma_separated_words in ignore_words_list:
  742         for word in comma_separated_words.split(','):
  743             ignore_words.add(word.strip())
  744 
  745     if options.dictionary:
  746         dictionaries = options.dictionary
  747     else:
  748         dictionaries = ['-']
  749     use_dictionaries = list()
  750     for dictionary in dictionaries:
  751         if dictionary == "-":
  752             # figure out which builtin dictionaries to use
  753             use = sorted(set(options.builtin.split(',')))
  754             for u in use:
  755                 for builtin in _builtin_dictionaries:
  756                     if builtin[0] == u:
  757                         use_dictionaries.append(
  758                             os.path.join(_data_root, 'dictionary%s.txt'
  759                                          % (builtin[2],)))
  760                         break
  761                 else:
  762                     print("ERROR: Unknown builtin dictionary: %s" % (u,),
  763                           file=sys.stderr)
  764                     parser.print_help()
  765                     return EX_USAGE
  766         else:
  767             if not os.path.isfile(dictionary):
  768                 print("ERROR: cannot find dictionary file: %s" % dictionary,
  769                       file=sys.stderr)
  770                 parser.print_help()
  771                 return EX_USAGE
  772             use_dictionaries.append(dictionary)
  773     misspellings = dict()
  774     for dictionary in use_dictionaries:
  775         build_dict(dictionary, misspellings, ignore_words)
  776     colors = TermColors()
  777     if not options.colors or sys.platform == 'win32':
  778         colors.disable()
  779 
  780     if options.summary:
  781         summary = Summary()
  782     else:
  783         summary = None
  784 
  785     context = None
  786     if options.context is not None:
  787         if (options.before_context is not None) or \
  788                 (options.after_context is not None):
  789             print("ERROR: --context/-C cannot be used together with "
  790                   "--context-before/-B or --context-after/-A")
  791             parser.print_help()
  792             return EX_USAGE
  793         context_both = max(0, options.context)
  794         context = (context_both, context_both)
  795     elif (options.before_context is not None) or \
  796             (options.after_context is not None):
  797         context_before = 0
  798         context_after = 0
  799         if options.before_context is not None:
  800             context_before = max(0, options.before_context)
  801         if options.after_context is not None:
  802             context_after = max(0, options.after_context)
  803         context = (context_before, context_after)
  804 
  805     exclude_lines = set()
  806     if options.exclude_file:
  807         build_exclude_hashes(options.exclude_file, exclude_lines)
  808 
  809     file_opener = FileOpener(options.hard_encoding_detection,
  810                              options.quiet_level)
  811     glob_match = GlobMatch(options.skip)
  812 
  813     bad_count = 0
  814     for filename in options.files:
  815         # ignore hidden files
  816         if is_hidden(filename, options.check_hidden):
  817             continue
  818 
  819         if os.path.isdir(filename):
  820             for root, dirs, files in os.walk(filename):
  821                 if glob_match.match(root):  # skip (absolute) directories
  822                     del dirs[:]
  823                     continue
  824                 if is_hidden(root, options.check_hidden):  # dir itself hidden
  825                     continue
  826                 for file_ in files:
  827                     # ignore hidden files in directories
  828                     if is_hidden(file_, options.check_hidden):
  829                         continue
  830                     if glob_match.match(file_):  # skip files
  831                         continue
  832                     fname = os.path.join(root, file_)
  833                     if glob_match.match(fname):  # skip paths
  834                         continue
  835                     bad_count += parse_file(
  836                         fname, colors, summary, misspellings, exclude_lines,
  837                         file_opener, word_regex, ignore_word_regex, context,
  838                         options)
  839 
  840                 # skip (relative) directories
  841                 dirs[:] = [dir_ for dir_ in dirs if not glob_match.match(dir_)]
  842 
  843         else:
  844             bad_count += parse_file(
  845                 filename, colors, summary, misspellings, exclude_lines,
  846                 file_opener, word_regex, ignore_word_regex, context, options)
  847 
  848     if summary:
  849         print("\n-------8<-------\nSUMMARY:")
  850         print(summary)
  851     if options.count:
  852         print(bad_count, file=sys.stderr)
  853     return EX_DATAERR if bad_count else EX_OK