Home | History | Annotate | Line # | Download | only in legacy
      1  1.1  mrg #!/usr/bin/env python3
      2  1.1  mrg 
      3  1.1  mrg # Copyright (C) 2017-2019 Free Software Foundation, Inc.
      4  1.1  mrg #
      5  1.1  mrg # This file is part of GCC.
      6  1.1  mrg #
      7  1.1  mrg # GCC is free software; you can redistribute it and/or modify
      8  1.1  mrg # it under the terms of the GNU General Public License as published by
      9  1.1  mrg # the Free Software Foundation; either version 3, or (at your option)
     10  1.1  mrg # any later version.
     11  1.1  mrg #
     12  1.1  mrg # GCC is distributed in the hope that it will be useful,
     13  1.1  mrg # but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  1.1  mrg # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     15  1.1  mrg # GNU General Public License for more details.
     16  1.1  mrg #
     17  1.1  mrg # You should have received a copy of the GNU General Public License
     18  1.1  mrg # along with GCC; see the file COPYING.  If not, write to
     19  1.1  mrg # the Free Software Foundation, 51 Franklin Street, Fifth Floor,
     20  1.1  mrg # Boston, MA 02110-1301, USA.
     21  1.1  mrg 
     22  1.1  mrg # This script parses a .diff file generated with 'diff -up' or 'diff -cp'
     23  1.1  mrg # and adds a skeleton ChangeLog file to the file. It does not try to be
     24  1.1  mrg # too smart when parsing function names, but it produces a reasonable
     25  1.1  mrg # approximation.
     26  1.1  mrg #
     27  1.1  mrg # This is a straightforward adaptation of original Perl script.
     28  1.1  mrg #
     29  1.1  mrg # Author: Yury Gribov <tetra2005 (at] gmail.com>
     30  1.1  mrg 
     31  1.1  mrg import argparse
     32  1.1  mrg import sys
     33  1.1  mrg import re
     34  1.1  mrg import os.path
     35  1.1  mrg import os
     36  1.1  mrg import tempfile
     37  1.1  mrg import time
     38  1.1  mrg import shutil
     39  1.1  mrg from subprocess import Popen, PIPE
     40  1.1  mrg 
     41  1.1  mrg me = os.path.basename(sys.argv[0])
     42  1.1  mrg 
     43  1.1  mrg pr_regex = re.compile('\+(\/(\/|\*)|[Cc*!])\s+(PR [a-z+-]+\/[0-9]+)')
     44  1.1  mrg 
     45  1.1  mrg def error(msg):
     46  1.1  mrg   sys.stderr.write("%s: error: %s\n" % (me, msg))
     47  1.1  mrg   sys.exit(1)
     48  1.1  mrg 
     49  1.1  mrg def warn(msg):
     50  1.1  mrg   sys.stderr.write("%s: warning: %s\n" % (me, msg))
     51  1.1  mrg 
     52  1.1  mrg class RegexCache(object):
     53  1.1  mrg   """Simple trick to Perl-like combined match-and-bind."""
     54  1.1  mrg 
     55  1.1  mrg   def __init__(self):
     56  1.1  mrg     self.last_match = None
     57  1.1  mrg 
     58  1.1  mrg   def match(self, p, s):
     59  1.1  mrg     self.last_match = re.match(p, s) if isinstance(p, str) else p.match(s)
     60  1.1  mrg     return self.last_match
     61  1.1  mrg 
     62  1.1  mrg   def search(self, p, s):
     63  1.1  mrg     self.last_match = re.search(p, s) if isinstance(p, str) else p.search(s)
     64  1.1  mrg     return self.last_match
     65  1.1  mrg 
     66  1.1  mrg   def group(self, n):
     67  1.1  mrg     return self.last_match.group(n)
     68  1.1  mrg 
     69  1.1  mrg cache = RegexCache()
     70  1.1  mrg 
     71  1.1  mrg def run(cmd, die_on_error):
     72  1.1  mrg   """Simple wrapper for Popen."""
     73  1.1  mrg   proc = Popen(cmd.split(' '), stderr = PIPE, stdout = PIPE)
     74  1.1  mrg   (out, err) = proc.communicate()
     75  1.1  mrg   if die_on_error and proc.returncode != 0:
     76  1.1  mrg     error("`%s` failed:\n" % (cmd, proc.stderr))
     77  1.1  mrg   return proc.returncode, out.decode(), err
     78  1.1  mrg 
     79  1.1  mrg def read_user_info():
     80  1.1  mrg   dot_mklog_format_msg = """\
     81  1.1  mrg The .mklog format is:
     82  1.1  mrg NAME = ...
     83  1.1  mrg EMAIL = ...
     84  1.1  mrg """
     85  1.1  mrg 
     86  1.1  mrg   # First try to read .mklog config
     87  1.1  mrg   mklog_conf = os.path.expanduser('~/.mklog')
     88  1.1  mrg   if os.path.exists(mklog_conf):
     89  1.1  mrg     attrs = {}
     90  1.1  mrg     f = open(mklog_conf)
     91  1.1  mrg     for s in f:
     92  1.1  mrg       if cache.match(r'^\s*([a-zA-Z0-9_]+)\s*=\s*(.*?)\s*$', s):
     93  1.1  mrg         attrs[cache.group(1)] = cache.group(2)
     94  1.1  mrg     f.close()
     95  1.1  mrg     if 'NAME' not in attrs:
     96  1.1  mrg       error("'NAME' not present in .mklog")
     97  1.1  mrg     if 'EMAIL' not in attrs:
     98  1.1  mrg       error("'EMAIL' not present in .mklog")
     99  1.1  mrg     return attrs['NAME'], attrs['EMAIL']
    100  1.1  mrg 
    101  1.1  mrg   # Otherwise go with git
    102  1.1  mrg 
    103  1.1  mrg   rc1, name, _ = run('git config user.name', False)
    104  1.1  mrg   name = name.rstrip()
    105  1.1  mrg   rc2, email, _ = run('git config user.email', False)
    106  1.1  mrg   email = email.rstrip()
    107  1.1  mrg 
    108  1.1  mrg   if rc1 != 0 or rc2 != 0:
    109  1.1  mrg     error("""\
    110  1.1  mrg Could not read git user.name and user.email settings.
    111  1.1  mrg Please add missing git settings, or create a %s.
    112  1.1  mrg """ % mklog_conf)
    113  1.1  mrg 
    114  1.1  mrg   return name, email
    115  1.1  mrg 
    116  1.1  mrg def get_parent_changelog (s):
    117  1.1  mrg   """See which ChangeLog this file change should go to."""
    118  1.1  mrg 
    119  1.1  mrg   if s.find('\\') == -1 and s.find('/') == -1:
    120  1.1  mrg     return "ChangeLog", s
    121  1.1  mrg 
    122  1.1  mrg   gcc_root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
    123  1.1  mrg 
    124  1.1  mrg   d = s
    125  1.1  mrg   while d:
    126  1.1  mrg     clname = d + "/ChangeLog"
    127  1.1  mrg     if os.path.exists(gcc_root + '/' + clname) or os.path.exists(clname):
    128  1.1  mrg       relname = s[len(d)+1:]
    129  1.1  mrg       return clname, relname
    130  1.1  mrg     d, _ = os.path.split(d)
    131  1.1  mrg 
    132  1.1  mrg   return "Unknown ChangeLog", s
    133  1.1  mrg 
    134  1.1  mrg class FileDiff:
    135  1.1  mrg   """Class to represent changes in a single file."""
    136  1.1  mrg 
    137  1.1  mrg   def __init__(self, filename):
    138  1.1  mrg     self.filename = filename
    139  1.1  mrg     self.hunks = []
    140  1.1  mrg     self.clname, self.relname = get_parent_changelog(filename);
    141  1.1  mrg 
    142  1.1  mrg   def dump(self):
    143  1.1  mrg     print("Diff for %s:\n  ChangeLog = %s\n  rel name = %s\n" % (self.filename, self.clname, self.relname))
    144  1.1  mrg     for i, h in enumerate(self.hunks):
    145  1.1  mrg       print("Next hunk %d:" % i)
    146  1.1  mrg       h.dump()
    147  1.1  mrg 
    148  1.1  mrg class Hunk:
    149  1.1  mrg   """Class to represent a single hunk of changes."""
    150  1.1  mrg 
    151  1.1  mrg   def __init__(self, hdr):
    152  1.1  mrg     self.hdr = hdr
    153  1.1  mrg     self.lines = []
    154  1.1  mrg     self.ctx_diff = is_ctx_hunk_start(hdr)
    155  1.1  mrg 
    156  1.1  mrg   def dump(self):
    157  1.1  mrg     print('%s' % self.hdr)
    158  1.1  mrg     print('%s' % '\n'.join(self.lines))
    159  1.1  mrg 
    160  1.1  mrg   def is_file_addition(self):
    161  1.1  mrg     """Does hunk describe addition of file?"""
    162  1.1  mrg     if self.ctx_diff:
    163  1.1  mrg       for line in self.lines:
    164  1.1  mrg         if re.match(r'^\*\*\* 0 \*\*\*\*', line):
    165  1.1  mrg           return True
    166  1.1  mrg     else:
    167  1.1  mrg       return re.match(r'^@@ -0,0 \+1.* @@', self.hdr)
    168  1.1  mrg 
    169  1.1  mrg   def is_file_removal(self):
    170  1.1  mrg     """Does hunk describe removal of file?"""
    171  1.1  mrg     if self.ctx_diff:
    172  1.1  mrg       for line in self.lines:
    173  1.1  mrg         if re.match(r'^--- 0 ----', line):
    174  1.1  mrg           return True
    175  1.1  mrg     else:
    176  1.1  mrg       return re.match(r'^@@ -1.* \+0,0 @@', self.hdr)
    177  1.1  mrg 
    178  1.1  mrg def is_file_diff_start(s):
    179  1.1  mrg   # Don't be fooled by context diff line markers:
    180  1.1  mrg   #   *** 385,391 ****
    181  1.1  mrg   return ((s.startswith('*** ') and not s.endswith('***'))
    182  1.1  mrg           or (s.startswith('--- ') and not s.endswith('---')))
    183  1.1  mrg 
    184  1.1  mrg def is_ctx_hunk_start(s):
    185  1.1  mrg   return re.match(r'^\*\*\*\*\*\**', s)
    186  1.1  mrg 
    187  1.1  mrg def is_uni_hunk_start(s):
    188  1.1  mrg   return re.match(r'^@@ .* @@', s)
    189  1.1  mrg 
    190  1.1  mrg def is_hunk_start(s):
    191  1.1  mrg   return is_ctx_hunk_start(s) or is_uni_hunk_start(s)
    192  1.1  mrg 
    193  1.1  mrg def remove_suffixes(s):
    194  1.1  mrg   if s.startswith('a/') or s.startswith('b/'):
    195  1.1  mrg     s = s[2:]
    196  1.1  mrg   if s.endswith('.jj'):
    197  1.1  mrg     s = s[:-3]
    198  1.1  mrg   return s
    199  1.1  mrg 
    200  1.1  mrg def find_changed_funs(hunk):
    201  1.1  mrg   """Find all functions touched by hunk.  We don't try too hard
    202  1.1  mrg      to find good matches.  This should return a superset
    203  1.1  mrg      of the actual set of functions in the .diff file.
    204  1.1  mrg   """
    205  1.1  mrg 
    206  1.1  mrg   fns = []
    207  1.1  mrg   fn = None
    208  1.1  mrg 
    209  1.1  mrg   if (cache.match(r'^\*\*\*\*\*\** ([a-zA-Z0-9_].*)', hunk.hdr)
    210  1.1  mrg       or cache.match(r'^@@ .* @@ ([a-zA-Z0-9_].*)', hunk.hdr)):
    211  1.1  mrg     fn = cache.group(1)
    212  1.1  mrg 
    213  1.1  mrg   for i, line in enumerate(hunk.lines):
    214  1.1  mrg     # Context diffs have extra whitespace after first char;
    215  1.1  mrg     # remove it to make matching easier.
    216  1.1  mrg     if hunk.ctx_diff:
    217  1.1  mrg       line = re.sub(r'^([-+! ]) ', r'\1', line)
    218  1.1  mrg 
    219  1.1  mrg     # Remember most recent identifier in hunk
    220  1.1  mrg     # that might be a function name.
    221  1.1  mrg     if cache.match(r'^[-+! ]([a-zA-Z0-9_#].*)', line):
    222  1.1  mrg       fn = cache.group(1)
    223  1.1  mrg 
    224  1.1  mrg     change = line and re.match(r'^[-+!][^-]', line)
    225  1.1  mrg 
    226  1.1  mrg     # Top-level comment cannot belong to function
    227  1.1  mrg     if re.match(r'^[-+! ]\/\*', line):
    228  1.1  mrg       fn = None
    229  1.1  mrg 
    230  1.1  mrg     if change and fn:
    231  1.1  mrg       if cache.match(r'^((class|struct|union|enum)\s+[a-zA-Z0-9_]+)', fn):
    232  1.1  mrg         # Struct declaration
    233  1.1  mrg         fn = cache.group(1)
    234  1.1  mrg       elif cache.search(r'#\s*define\s+([a-zA-Z0-9_]+)', fn):
    235  1.1  mrg         # Macro definition
    236  1.1  mrg         fn = cache.group(1)
    237  1.1  mrg       elif cache.match('^DEF[A-Z0-9_]+\s*\(([a-zA-Z0-9_]+)', fn):
    238  1.1  mrg         # Supermacro
    239  1.1  mrg         fn = cache.group(1)
    240  1.1  mrg       elif cache.search(r'([a-zA-Z_][^()\s]*)\s*\([^*]', fn):
    241  1.1  mrg         # Discard template and function parameters.
    242  1.1  mrg         fn = cache.group(1)
    243  1.1  mrg         fn = re.sub(r'<[^<>]*>', '', fn)
    244  1.1  mrg         fn = fn.rstrip()
    245  1.1  mrg       else:
    246  1.1  mrg         fn = None
    247  1.1  mrg 
    248  1.1  mrg       if fn and fn not in fns:  # Avoid dups
    249  1.1  mrg         fns.append(fn)
    250  1.1  mrg 
    251  1.1  mrg       fn = None
    252  1.1  mrg 
    253  1.1  mrg   return fns
    254  1.1  mrg 
    255  1.1  mrg def parse_patch(contents):
    256  1.1  mrg   """Parse patch contents to a sequence of FileDiffs."""
    257  1.1  mrg 
    258  1.1  mrg   diffs = []
    259  1.1  mrg 
    260  1.1  mrg   lines = contents.split('\n')
    261  1.1  mrg 
    262  1.1  mrg   i = 0
    263  1.1  mrg   while i < len(lines):
    264  1.1  mrg     line = lines[i]
    265  1.1  mrg 
    266  1.1  mrg     # Diff headers look like
    267  1.1  mrg     #   --- a/gcc/tree.c
    268  1.1  mrg     #   +++ b/gcc/tree.c
    269  1.1  mrg     # or
    270  1.1  mrg     #   *** gcc/cfgexpand.c     2013-12-25 20:07:24.800350058 +0400
    271  1.1  mrg     #   --- gcc/cfgexpand.c     2013-12-25 20:06:30.612350178 +0400
    272  1.1  mrg 
    273  1.1  mrg     if is_file_diff_start(line):
    274  1.1  mrg       left = re.split(r'\s+', line)[1]
    275  1.1  mrg     else:
    276  1.1  mrg       i += 1
    277  1.1  mrg       continue
    278  1.1  mrg 
    279  1.1  mrg     left = remove_suffixes(left);
    280  1.1  mrg 
    281  1.1  mrg     i += 1
    282  1.1  mrg     line = lines[i]
    283  1.1  mrg 
    284  1.1  mrg     if not cache.match(r'^[+-][+-][+-] +(\S+)', line):
    285  1.1  mrg       error("expected filename in line %d" % i)
    286  1.1  mrg     right = remove_suffixes(cache.group(1));
    287  1.1  mrg 
    288  1.1  mrg     # Extract real file name from left and right names.
    289  1.1  mrg     filename = None
    290  1.1  mrg     if left == right:
    291  1.1  mrg       filename = left
    292  1.1  mrg     elif left == '/dev/null':
    293  1.1  mrg       filename = right;
    294  1.1  mrg     elif right == '/dev/null':
    295  1.1  mrg       filename = left;
    296  1.1  mrg     else:
    297  1.1  mrg       comps = []
    298  1.1  mrg       while left and right:
    299  1.1  mrg         left, l = os.path.split(left)
    300  1.1  mrg         right, r = os.path.split(right)
    301  1.1  mrg         if l != r:
    302  1.1  mrg           break
    303  1.1  mrg         comps.append(l)
    304  1.1  mrg 
    305  1.1  mrg       if not comps:
    306  1.1  mrg         error("failed to extract common name for %s and %s" % (left, right))
    307  1.1  mrg 
    308  1.1  mrg       comps.reverse()
    309  1.1  mrg       filename = '/'.join(comps)
    310  1.1  mrg 
    311  1.1  mrg     d = FileDiff(filename)
    312  1.1  mrg     diffs.append(d)
    313  1.1  mrg 
    314  1.1  mrg     # Collect hunks for current file.
    315  1.1  mrg     hunk = None
    316  1.1  mrg     i += 1
    317  1.1  mrg     while i < len(lines):
    318  1.1  mrg       line = lines[i]
    319  1.1  mrg 
    320  1.1  mrg       # Create new hunk when we see hunk header
    321  1.1  mrg       if is_hunk_start(line):
    322  1.1  mrg         if hunk is not None:
    323  1.1  mrg           d.hunks.append(hunk)
    324  1.1  mrg         hunk = Hunk(line)
    325  1.1  mrg         i += 1
    326  1.1  mrg         continue
    327  1.1  mrg 
    328  1.1  mrg       # Stop when we reach next diff
    329  1.1  mrg       if (is_file_diff_start(line)
    330  1.1  mrg           or line.startswith('diff ')
    331  1.1  mrg           or line.startswith('Index: ')):
    332  1.1  mrg         i -= 1
    333  1.1  mrg         break
    334  1.1  mrg 
    335  1.1  mrg       if hunk is not None:
    336  1.1  mrg         hunk.lines.append(line)
    337  1.1  mrg       i += 1
    338  1.1  mrg 
    339  1.1  mrg     d.hunks.append(hunk)
    340  1.1  mrg 
    341  1.1  mrg   return diffs
    342  1.1  mrg 
    343  1.1  mrg 
    344  1.1  mrg def get_pr_from_testcase(line):
    345  1.1  mrg     r = pr_regex.search(line)
    346  1.1  mrg     if r != None:
    347  1.1  mrg         return r.group(3)
    348  1.1  mrg     else:
    349  1.1  mrg         return None
    350  1.1  mrg 
    351  1.1  mrg def main():
    352  1.1  mrg   name, email = read_user_info()
    353  1.1  mrg 
    354  1.1  mrg   help_message =  """\
    355  1.1  mrg Generate ChangeLog template for PATCH.
    356  1.1  mrg PATCH must be generated using diff(1)'s -up or -cp options
    357  1.1  mrg (or their equivalent in Subversion/git).
    358  1.1  mrg """
    359  1.1  mrg 
    360  1.1  mrg   inline_message = """\
    361  1.1  mrg Prepends ChangeLog to PATCH.
    362  1.1  mrg If PATCH is not stdin, modifies PATCH in-place,
    363  1.1  mrg otherwise writes to stdout.'
    364  1.1  mrg """
    365  1.1  mrg 
    366  1.1  mrg   parser = argparse.ArgumentParser(description = help_message)
    367  1.1  mrg   parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Verbose messages')
    368  1.1  mrg   parser.add_argument('-i', '--inline', action = 'store_true', help = inline_message)
    369  1.1  mrg   parser.add_argument('input', nargs = '?', help = 'Patch file (or missing, read standard input)')
    370  1.1  mrg   args = parser.parse_args()
    371  1.1  mrg   if args.input == '-':
    372  1.1  mrg       args.input = None
    373  1.1  mrg   input = open(args.input) if args.input else sys.stdin
    374  1.1  mrg   contents = input.read()
    375  1.1  mrg   diffs = parse_patch(contents)
    376  1.1  mrg 
    377  1.1  mrg   if args.verbose:
    378  1.1  mrg     print("Parse results:")
    379  1.1  mrg     for d in diffs:
    380  1.1  mrg       d.dump()
    381  1.1  mrg 
    382  1.1  mrg   # Generate template ChangeLog.
    383  1.1  mrg 
    384  1.1  mrg   logs = {}
    385  1.1  mrg   prs = []
    386  1.1  mrg   for d in diffs:
    387  1.1  mrg     log_name = d.clname
    388  1.1  mrg 
    389  1.1  mrg     logs.setdefault(log_name, '')
    390  1.1  mrg     logs[log_name] += '\t* %s' % d.relname
    391  1.1  mrg 
    392  1.1  mrg     change_msg = ''
    393  1.1  mrg 
    394  1.1  mrg     # Check if file was removed or added.
    395  1.1  mrg     # Two patterns for context and unified diff.
    396  1.1  mrg     if len(d.hunks) == 1:
    397  1.1  mrg       hunk0 = d.hunks[0]
    398  1.1  mrg       if hunk0.is_file_addition():
    399  1.1  mrg         if re.search(r'testsuite.*(?<!\.exp)$', d.filename):
    400  1.1  mrg           change_msg = ': New test.\n'
    401  1.1  mrg           pr = get_pr_from_testcase(hunk0.lines[0])
    402  1.1  mrg           if pr and pr not in prs:
    403  1.1  mrg               prs.append(pr)
    404  1.1  mrg         else:
    405  1.1  mrg           change_msg = ": New file.\n"
    406  1.1  mrg       elif hunk0.is_file_removal():
    407  1.1  mrg         change_msg = ": Remove.\n"
    408  1.1  mrg 
    409  1.1  mrg     _, ext = os.path.splitext(d.filename)
    410  1.1  mrg     if (not change_msg and ext in ['.c', '.cpp', '.C', '.cc', '.h', '.inc', '.def']
    411  1.1  mrg         and not 'testsuite' in d.filename):
    412  1.1  mrg       fns = []
    413  1.1  mrg       for hunk in d.hunks:
    414  1.1  mrg         for fn in find_changed_funs(hunk):
    415  1.1  mrg           if fn not in fns:
    416  1.1  mrg             fns.append(fn)
    417  1.1  mrg 
    418  1.1  mrg       for fn in fns:
    419  1.1  mrg         if change_msg:
    420  1.1  mrg           change_msg += "\t(%s):\n" % fn
    421  1.1  mrg         else:
    422  1.1  mrg           change_msg = " (%s):\n" % fn
    423  1.1  mrg 
    424  1.1  mrg     logs[log_name] += change_msg if change_msg else ":\n"
    425  1.1  mrg 
    426  1.1  mrg   if args.inline and args.input:
    427  1.1  mrg     # Get a temp filename, rather than an open filehandle, because we use
    428  1.1  mrg     # the open to truncate.
    429  1.1  mrg     fd, tmp = tempfile.mkstemp("tmp.XXXXXXXX")
    430  1.1  mrg     os.close(fd)
    431  1.1  mrg 
    432  1.1  mrg     # Copy permissions to temp file
    433  1.1  mrg     # (old Pythons do not support shutil.copymode)
    434  1.1  mrg     shutil.copymode(args.input, tmp)
    435  1.1  mrg 
    436  1.1  mrg     # Open the temp file, clearing contents.
    437  1.1  mrg     out = open(tmp, 'w')
    438  1.1  mrg   else:
    439  1.1  mrg     tmp = None
    440  1.1  mrg     out = sys.stdout
    441  1.1  mrg 
    442  1.1  mrg   # Print log
    443  1.1  mrg   date = time.strftime('%Y-%m-%d')
    444  1.1  mrg   bugmsg = ''
    445  1.1  mrg   if len(prs):
    446  1.1  mrg     bugmsg = '\n'.join(['\t' + pr for pr in prs]) + '\n'
    447  1.1  mrg 
    448  1.1  mrg   for log_name, msg in sorted(logs.items()):
    449  1.1  mrg     out.write("""\
    450  1.1  mrg %s:
    451  1.1  mrg 
    452  1.1  mrg %s  %s  <%s>
    453  1.1  mrg 
    454  1.1  mrg %s%s\n""" % (log_name, date, name, email, bugmsg, msg))
    455  1.1  mrg 
    456  1.1  mrg   if args.inline:
    457  1.1  mrg     # Append patch body
    458  1.1  mrg     out.write(contents)
    459  1.1  mrg 
    460  1.1  mrg     if args.input:
    461  1.1  mrg       # Write new contents atomically
    462  1.1  mrg       out.close()
    463  1.1  mrg       shutil.move(tmp, args.input)
    464  1.1  mrg 
    465  1.1  mrg if __name__ == '__main__':
    466  1.1  mrg     main()
    467