blob: af0b3a8c206f874d06a7b401a9534ccbea6fc8ea [file] [log] [blame]
Martin v. Löwisef04c442008-03-19 05:04:44 +00001#!/usr/bin/env python2.5
2# Copyright 2006 Google, Inc. All Rights Reserved.
3# Licensed to PSF under a Contributor Agreement.
4
5"""Refactoring framework.
6
7Used as a main program, this can refactor any number of files and/or
8recursively descend down directories. Imported as a module, this
9provides infrastructure to write your own refactoring tool.
10"""
11
12__author__ = "Guido van Rossum <guido@python.org>"
13
14
15# Python imports
16import os
17import sys
18import difflib
19import optparse
20import logging
Christian Heimes81ee3ef2008-05-04 22:42:01 +000021from collections import defaultdict
22from itertools import chain
Martin v. Löwisef04c442008-03-19 05:04:44 +000023
24# Local imports
25from .pgen2 import driver
26from .pgen2 import tokenize
27
28from . import pytree
29from . import patcomp
30from . import fixes
31from . import pygram
32
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000033def main(fixer_dir, args=None):
Martin v. Löwisef04c442008-03-19 05:04:44 +000034 """Main program.
35
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000036 Args:
37 fixer_dir: directory where fixer modules are located.
38 args: optional; a list of command line arguments. If omitted,
39 sys.argv[1:] is used.
Martin v. Löwisef04c442008-03-19 05:04:44 +000040
41 Returns a suggested exit status (0, 1, 2).
42 """
43 # Set up option parser
44 parser = optparse.OptionParser(usage="refactor.py [options] file|dir ...")
45 parser.add_option("-d", "--doctests_only", action="store_true",
46 help="Fix up doctests only")
47 parser.add_option("-f", "--fix", action="append", default=[],
48 help="Each FIX specifies a transformation; default all")
49 parser.add_option("-l", "--list-fixes", action="store_true",
50 help="List available transformations (fixes/fix_*.py)")
51 parser.add_option("-p", "--print-function", action="store_true",
52 help="Modify the grammar so that print() is a function")
53 parser.add_option("-v", "--verbose", action="store_true",
54 help="More verbose logging")
55 parser.add_option("-w", "--write", action="store_true",
56 help="Write back modified files")
57
58 # Parse command line arguments
59 options, args = parser.parse_args(args)
60 if options.list_fixes:
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +000061 print("Available transformations for the -f/--fix option:")
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000062 for fixname in get_all_fix_names(fixer_dir):
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +000063 print(fixname)
Martin v. Löwisef04c442008-03-19 05:04:44 +000064 if not args:
65 return 0
66 if not args:
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +000067 print("At least one file or directory argument required.", file=sys.stderr)
68 print("Use --help to show usage.", file=sys.stderr)
Martin v. Löwisef04c442008-03-19 05:04:44 +000069 return 2
70
Benjamin Peterson2a691a82008-03-31 01:51:45 +000071 # Set up logging handler
72 if sys.version_info < (2, 4):
73 hdlr = logging.StreamHandler()
74 fmt = logging.Formatter('%(name)s: %(message)s')
75 hdlr.setFormatter(fmt)
76 logging.root.addHandler(hdlr)
77 else:
78 logging.basicConfig(format='%(name)s: %(message)s', level=logging.INFO)
79
Martin v. Löwisef04c442008-03-19 05:04:44 +000080 # Initialize the refactoring tool
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000081 rt = RefactoringTool(fixer_dir, options)
Martin v. Löwisef04c442008-03-19 05:04:44 +000082
83 # Refactor all files and directories passed as arguments
84 if not rt.errors:
85 rt.refactor_args(args)
86 rt.summarize()
87
88 # Return error status (0 if rt.errors is zero)
89 return int(bool(rt.errors))
90
91
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000092def get_all_fix_names(fixer_dir):
Martin v. Löwisef04c442008-03-19 05:04:44 +000093 """Return a sorted list of all available fix names."""
94 fix_names = []
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +000095 names = os.listdir(fixer_dir)
Martin v. Löwisef04c442008-03-19 05:04:44 +000096 names.sort()
97 for name in names:
98 if name.startswith("fix_") and name.endswith(".py"):
99 fix_names.append(name[4:-3])
100 fix_names.sort()
101 return fix_names
102
Christian Heimes81ee3ef2008-05-04 22:42:01 +0000103def get_head_types(pat):
104 """ Accepts a pytree Pattern Node and returns a set
105 of the pattern types which will match first. """
106
107 if isinstance(pat, (pytree.NodePattern, pytree.LeafPattern)):
108 # NodePatters must either have no type and no content
109 # or a type and content -- so they don't get any farther
110 # Always return leafs
111 return set([pat.type])
112
113 if isinstance(pat, pytree.NegatedPattern):
114 if pat.content:
115 return get_head_types(pat.content)
116 return set([None]) # Negated Patterns don't have a type
117
118 if isinstance(pat, pytree.WildcardPattern):
119 # Recurse on each node in content
120 r = set()
121 for p in pat.content:
122 for x in p:
123 r.update(get_head_types(x))
124 return r
125
126 raise Exception("Oh no! I don't understand pattern %s" %(pat))
127
128def get_headnode_dict(fixer_list):
129 """ Accepts a list of fixers and returns a dictionary
130 of head node type --> fixer list. """
131 head_nodes = defaultdict(list)
132 for fixer in fixer_list:
133 if not fixer.pattern:
134 head_nodes[None].append(fixer)
135 continue
136 for t in get_head_types(fixer.pattern):
137 head_nodes[t].append(fixer)
138 return head_nodes
139
Martin v. Löwisef04c442008-03-19 05:04:44 +0000140
141class RefactoringTool(object):
142
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +0000143 def __init__(self, fixer_dir, options):
Martin v. Löwisef04c442008-03-19 05:04:44 +0000144 """Initializer.
145
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +0000146 Args:
147 fixer_dir: directory in which to find fixer modules.
148 options: an optparse.Values instance.
Martin v. Löwisef04c442008-03-19 05:04:44 +0000149 """
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +0000150 self.fixer_dir = fixer_dir
Martin v. Löwisef04c442008-03-19 05:04:44 +0000151 self.options = options
152 self.errors = []
153 self.logger = logging.getLogger("RefactoringTool")
154 self.fixer_log = []
155 if self.options.print_function:
156 del pygram.python_grammar.keywords["print"]
157 self.driver = driver.Driver(pygram.python_grammar,
158 convert=pytree.convert,
159 logger=self.logger)
160 self.pre_order, self.post_order = self.get_fixers()
Christian Heimes81ee3ef2008-05-04 22:42:01 +0000161
162 self.pre_order = get_headnode_dict(self.pre_order)
163 self.post_order = get_headnode_dict(self.post_order)
164
Martin v. Löwisef04c442008-03-19 05:04:44 +0000165 self.files = [] # List of files that were or should be modified
166
167 def get_fixers(self):
168 """Inspects the options to load the requested patterns and handlers.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000169
Martin v. Löwisef04c442008-03-19 05:04:44 +0000170 Returns:
171 (pre_order, post_order), where pre_order is the list of fixers that
172 want a pre-order AST traversal, and post_order is the list that want
173 post-order traversal.
174 """
Amaury Forgeot d'Arc036aa542008-06-17 23:16:28 +0000175 fixer_pkg = self.fixer_dir.replace(os.path.sep, ".")
176 if os.path.altsep:
177 fixer_pkg = fixer_pkg.replace(os.path.altsep, ".")
Martin v. Löwisef04c442008-03-19 05:04:44 +0000178 pre_order_fixers = []
179 post_order_fixers = []
180 fix_names = self.options.fix
181 if not fix_names or "all" in fix_names:
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +0000182 fix_names = get_all_fix_names(self.fixer_dir)
Martin v. Löwisef04c442008-03-19 05:04:44 +0000183 for fix_name in fix_names:
184 try:
Benjamin Petersondf6dc8f2008-06-15 02:57:40 +0000185 mod = __import__(fixer_pkg + ".fix_" + fix_name, {}, {}, ["*"])
Martin v. Löwisef04c442008-03-19 05:04:44 +0000186 except ImportError:
187 self.log_error("Can't find transformation %s", fix_name)
188 continue
189 parts = fix_name.split("_")
190 class_name = "Fix" + "".join([p.title() for p in parts])
191 try:
192 fix_class = getattr(mod, class_name)
193 except AttributeError:
194 self.log_error("Can't find fixes.fix_%s.%s",
195 fix_name, class_name)
196 continue
197 try:
198 fixer = fix_class(self.options, self.fixer_log)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000199 except Exception as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000200 self.log_error("Can't instantiate fixes.fix_%s.%s()",
201 fix_name, class_name, exc_info=True)
202 continue
203 if fixer.explicit and fix_name not in self.options.fix:
204 self.log_message("Skipping implicit fixer: %s", fix_name)
205 continue
206
207 if self.options.verbose:
208 self.log_message("Adding transformation: %s", fix_name)
209 if fixer.order == "pre":
210 pre_order_fixers.append(fixer)
211 elif fixer.order == "post":
212 post_order_fixers.append(fixer)
213 else:
214 raise ValueError("Illegal fixer order: %r" % fixer.order)
Martin v. Löwis3faa84f2008-03-22 00:07:09 +0000215
216 pre_order_fixers.sort(key=lambda x: x.run_order)
217 post_order_fixers.sort(key=lambda x: x.run_order)
Martin v. Löwisef04c442008-03-19 05:04:44 +0000218 return (pre_order_fixers, post_order_fixers)
219
220 def log_error(self, msg, *args, **kwds):
221 """Increments error count and log a message."""
222 self.errors.append((msg, args, kwds))
223 self.logger.error(msg, *args, **kwds)
224
225 def log_message(self, msg, *args):
226 """Hook to log a message."""
227 if args:
228 msg = msg % args
229 self.logger.info(msg)
230
231 def refactor_args(self, args):
232 """Refactors files and directories from an argument list."""
233 for arg in args:
234 if arg == "-":
235 self.refactor_stdin()
236 elif os.path.isdir(arg):
237 self.refactor_dir(arg)
238 else:
239 self.refactor_file(arg)
240
241 def refactor_dir(self, arg):
242 """Descends down a directory and refactor every Python file found.
243
244 Python files are assumed to have a .py extension.
245
246 Files and subdirectories starting with '.' are skipped.
247 """
248 for dirpath, dirnames, filenames in os.walk(arg):
249 if self.options.verbose:
250 self.log_message("Descending into %s", dirpath)
251 dirnames.sort()
252 filenames.sort()
253 for name in filenames:
254 if not name.startswith(".") and name.endswith("py"):
255 fullname = os.path.join(dirpath, name)
256 self.refactor_file(fullname)
257 # Modify dirnames in-place to remove subdirs with leading dots
258 dirnames[:] = [dn for dn in dirnames if not dn.startswith(".")]
259
260 def refactor_file(self, filename):
261 """Refactors a file."""
262 try:
263 f = open(filename)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000264 except IOError as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000265 self.log_error("Can't open %s: %s", filename, err)
266 return
267 try:
268 input = f.read() + "\n" # Silence certain parse errors
269 finally:
270 f.close()
271 if self.options.doctests_only:
272 if self.options.verbose:
273 self.log_message("Refactoring doctests in %s", filename)
274 output = self.refactor_docstring(input, filename)
275 if output != input:
276 self.write_file(output, filename, input)
277 elif self.options.verbose:
278 self.log_message("No doctest changes in %s", filename)
279 else:
280 tree = self.refactor_string(input, filename)
281 if tree and tree.was_changed:
282 # The [:-1] is to take off the \n we added earlier
283 self.write_file(str(tree)[:-1], filename)
284 elif self.options.verbose:
285 self.log_message("No changes in %s", filename)
286
287 def refactor_string(self, data, name):
288 """Refactor a given input string.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000289
Martin v. Löwisef04c442008-03-19 05:04:44 +0000290 Args:
291 data: a string holding the code to be refactored.
292 name: a human-readable name for use in error/log messages.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000293
Martin v. Löwisef04c442008-03-19 05:04:44 +0000294 Returns:
295 An AST corresponding to the refactored input stream; None if
296 there were errors during the parse.
297 """
298 try:
299 tree = self.driver.parse_string(data,1)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000300 except Exception as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000301 self.log_error("Can't parse %s: %s: %s",
302 name, err.__class__.__name__, err)
303 return
304 if self.options.verbose:
305 self.log_message("Refactoring %s", name)
306 self.refactor_tree(tree, name)
307 return tree
308
309 def refactor_stdin(self):
310 if self.options.write:
311 self.log_error("Can't write changes back to stdin")
312 return
313 input = sys.stdin.read()
314 if self.options.doctests_only:
315 if self.options.verbose:
316 self.log_message("Refactoring doctests in stdin")
317 output = self.refactor_docstring(input, "<stdin>")
318 if output != input:
319 self.write_file(output, "<stdin>", input)
320 elif self.options.verbose:
321 self.log_message("No doctest changes in stdin")
322 else:
323 tree = self.refactor_string(input, "<stdin>")
324 if tree and tree.was_changed:
325 self.write_file(str(tree), "<stdin>", input)
326 elif self.options.verbose:
327 self.log_message("No changes in stdin")
328
329 def refactor_tree(self, tree, name):
330 """Refactors a parse tree (modifying the tree in place).
Martin v. Löwisf733c602008-03-19 05:26:18 +0000331
Martin v. Löwisef04c442008-03-19 05:04:44 +0000332 Args:
333 tree: a pytree.Node instance representing the root of the tree
334 to be refactored.
335 name: a human-readable name for this tree.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000336
Martin v. Löwisef04c442008-03-19 05:04:44 +0000337 Returns:
338 True if the tree was modified, False otherwise.
339 """
Christian Heimes81ee3ef2008-05-04 22:42:01 +0000340 # Two calls to chain are required because pre_order.values()
341 # will be a list of lists of fixers:
342 # [[<fixer ...>, <fixer ...>], [<fixer ...>]]
343 all_fixers = chain(chain(*self.pre_order.values()),\
344 chain(*self.post_order.values()))
Martin v. Löwisef04c442008-03-19 05:04:44 +0000345 for fixer in all_fixers:
346 fixer.start_tree(tree, name)
347
348 self.traverse_by(self.pre_order, tree.pre_order())
349 self.traverse_by(self.post_order, tree.post_order())
350
351 for fixer in all_fixers:
352 fixer.finish_tree(tree, name)
353 return tree.was_changed
354
355 def traverse_by(self, fixers, traversal):
356 """Traverse an AST, applying a set of fixers to each node.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000357
Martin v. Löwisef04c442008-03-19 05:04:44 +0000358 This is a helper method for refactor_tree().
Martin v. Löwisf733c602008-03-19 05:26:18 +0000359
Martin v. Löwisef04c442008-03-19 05:04:44 +0000360 Args:
361 fixers: a list of fixer instances.
362 traversal: a generator that yields AST nodes.
Martin v. Löwisf733c602008-03-19 05:26:18 +0000363
Martin v. Löwisef04c442008-03-19 05:04:44 +0000364 Returns:
365 None
366 """
367 if not fixers:
368 return
369 for node in traversal:
Christian Heimes81ee3ef2008-05-04 22:42:01 +0000370 for fixer in fixers[node.type] + fixers[None]:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000371 results = fixer.match(node)
372 if results:
373 new = fixer.transform(node, results)
374 if new is not None and (new != node or
375 str(new) != str(node)):
376 node.replace(new)
377 node = new
378
379 def write_file(self, new_text, filename, old_text=None):
380 """Writes a string to a file.
381
382 If there are no changes, this is a no-op.
383
384 Otherwise, it first shows a unified diff between the old text
385 and the new text, and then rewrites the file; the latter is
386 only done if the write option is set.
387 """
388 self.files.append(filename)
389 if old_text is None:
390 try:
391 f = open(filename, "r")
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000392 except IOError as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000393 self.log_error("Can't read %s: %s", filename, err)
394 return
395 try:
396 old_text = f.read()
397 finally:
398 f.close()
399 if old_text == new_text:
400 if self.options.verbose:
401 self.log_message("No changes to %s", filename)
402 return
403 diff_texts(old_text, new_text, filename)
404 if not self.options.write:
405 if self.options.verbose:
406 self.log_message("Not writing changes to %s", filename)
407 return
408 backup = filename + ".bak"
409 if os.path.lexists(backup):
410 try:
411 os.remove(backup)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000412 except os.error as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000413 self.log_message("Can't remove backup %s", backup)
414 try:
415 os.rename(filename, backup)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000416 except os.error as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000417 self.log_message("Can't rename %s to %s", filename, backup)
418 try:
419 f = open(filename, "w")
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000420 except os.error as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000421 self.log_error("Can't create %s: %s", filename, err)
422 return
423 try:
424 try:
425 f.write(new_text)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000426 except os.error as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000427 self.log_error("Can't write %s: %s", filename, err)
428 finally:
429 f.close()
430 if self.options.verbose:
431 self.log_message("Wrote changes to %s", filename)
432
433 PS1 = ">>> "
434 PS2 = "... "
435
436 def refactor_docstring(self, input, filename):
437 """Refactors a docstring, looking for doctests.
438
439 This returns a modified version of the input string. It looks
440 for doctests, which start with a ">>>" prompt, and may be
441 continued with "..." prompts, as long as the "..." is indented
442 the same as the ">>>".
443
444 (Unfortunately we can't use the doctest module's parser,
445 since, like most parsers, it is not geared towards preserving
446 the original source.)
447 """
448 result = []
449 block = None
450 block_lineno = None
451 indent = None
452 lineno = 0
453 for line in input.splitlines(True):
454 lineno += 1
455 if line.lstrip().startswith(self.PS1):
456 if block is not None:
457 result.extend(self.refactor_doctest(block, block_lineno,
458 indent, filename))
459 block_lineno = lineno
460 block = [line]
461 i = line.find(self.PS1)
462 indent = line[:i]
463 elif (indent is not None and
464 (line.startswith(indent + self.PS2) or
465 line == indent + self.PS2.rstrip() + "\n")):
466 block.append(line)
467 else:
468 if block is not None:
469 result.extend(self.refactor_doctest(block, block_lineno,
470 indent, filename))
471 block = None
472 indent = None
473 result.append(line)
474 if block is not None:
475 result.extend(self.refactor_doctest(block, block_lineno,
476 indent, filename))
477 return "".join(result)
478
479 def refactor_doctest(self, block, lineno, indent, filename):
480 """Refactors one doctest.
481
482 A doctest is given as a block of lines, the first of which starts
483 with ">>>" (possibly indented), while the remaining lines start
484 with "..." (identically indented).
485
486 """
487 try:
488 tree = self.parse_block(block, lineno, indent)
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000489 except Exception as err:
Martin v. Löwisef04c442008-03-19 05:04:44 +0000490 if self.options.verbose:
491 for line in block:
492 self.log_message("Source: %s", line.rstrip("\n"))
493 self.log_error("Can't parse docstring in %s line %s: %s: %s",
494 filename, lineno, err.__class__.__name__, err)
495 return block
496 if self.refactor_tree(tree, filename):
497 new = str(tree).splitlines(True)
498 # Undo the adjustment of the line numbers in wrap_toks() below.
499 clipped, new = new[:lineno-1], new[lineno-1:]
500 assert clipped == ["\n"] * (lineno-1), clipped
501 if not new[-1].endswith("\n"):
502 new[-1] += "\n"
503 block = [indent + self.PS1 + new.pop(0)]
504 if new:
505 block += [indent + self.PS2 + line for line in new]
506 return block
507
508 def summarize(self):
509 if self.options.write:
510 were = "were"
511 else:
512 were = "need to be"
513 if not self.files:
514 self.log_message("No files %s modified.", were)
515 else:
516 self.log_message("Files that %s modified:", were)
517 for file in self.files:
518 self.log_message(file)
519 if self.fixer_log:
520 self.log_message("Warnings/messages while refactoring:")
521 for message in self.fixer_log:
522 self.log_message(message)
523 if self.errors:
524 if len(self.errors) == 1:
525 self.log_message("There was 1 error:")
526 else:
527 self.log_message("There were %d errors:", len(self.errors))
528 for msg, args, kwds in self.errors:
529 self.log_message(msg, *args, **kwds)
530
531 def parse_block(self, block, lineno, indent):
532 """Parses a block into a tree.
533
534 This is necessary to get correct line number / offset information
535 in the parser diagnostics and embedded into the parse tree.
536 """
537 return self.driver.parse_tokens(self.wrap_toks(block, lineno, indent))
538
539 def wrap_toks(self, block, lineno, indent):
540 """Wraps a tokenize stream to systematically modify start/end."""
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000541 tokens = tokenize.generate_tokens(self.gen_lines(block, indent).__next__)
Martin v. Löwisef04c442008-03-19 05:04:44 +0000542 for type, value, (line0, col0), (line1, col1), line_text in tokens:
543 line0 += lineno - 1
544 line1 += lineno - 1
545 # Don't bother updating the columns; this is too complicated
546 # since line_text would also have to be updated and it would
547 # still break for tokens spanning lines. Let the user guess
548 # that the column numbers for doctests are relative to the
549 # end of the prompt string (PS1 or PS2).
550 yield type, value, (line0, col0), (line1, col1), line_text
551
552
553 def gen_lines(self, block, indent):
554 """Generates lines as expected by tokenize from a list of lines.
555
556 This strips the first len(indent + self.PS1) characters off each line.
557 """
558 prefix1 = indent + self.PS1
559 prefix2 = indent + self.PS2
560 prefix = prefix1
561 for line in block:
562 if line.startswith(prefix):
563 yield line[len(prefix):]
564 elif line == prefix.rstrip() + "\n":
565 yield "\n"
566 else:
567 raise AssertionError("line=%r, prefix=%r" % (line, prefix))
568 prefix = prefix2
569 while True:
570 yield ""
571
572
573def diff_texts(a, b, filename):
574 """Prints a unified diff of two strings."""
575 a = a.splitlines()
576 b = b.splitlines()
577 for line in difflib.unified_diff(a, b, filename, filename,
578 "(original)", "(refactored)",
579 lineterm=""):
Martin v. Löwis8a5f8ca2008-03-19 05:33:36 +0000580 print(line)
Martin v. Löwisef04c442008-03-19 05:04:44 +0000581
582
583if __name__ == "__main__":
Martin v. Löwisf733c602008-03-19 05:26:18 +0000584 sys.exit(main())