pathfix.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env python3
  2. # Change the #! line occurring in Python scripts. The new interpreter
  3. # pathname must be given with a -i option.
  4. #
  5. # Command line arguments are files or directories to be processed.
  6. # Directories are searched recursively for files whose name looks
  7. # like a python module.
  8. # Symbolic links are always ignored (except as explicit directory
  9. # arguments).
  10. # The original file is kept as a back-up (with a "~" attached to its name),
  11. # -n flag can be used to disable this.
  12. #
  13. # Undoubtedly you can do this using find and sed or perl, but this is
  14. # a nice example of Python code that recurses down a directory tree
  15. # and uses regular expressions. Also note several subtleties like
  16. # preserving the file's mode and avoiding to even write a temp file
  17. # when no changes are needed for a file.
  18. #
  19. # NB: by changing only the function fixfile() you can turn this
  20. # into a program for a different change to Python programs...
  21. import sys
  22. import re
  23. import os
  24. from stat import *
  25. import getopt
  26. err = sys.stderr.write
  27. dbg = err
  28. rep = sys.stdout.write
  29. new_interpreter = None
  30. preserve_timestamps = False
  31. create_backup = True
  32. def main():
  33. global new_interpreter
  34. global preserve_timestamps
  35. global create_backup
  36. usage = ('usage: %s -i /interpreter -p -n file-or-directory ...\n' %
  37. sys.argv[0])
  38. try:
  39. opts, args = getopt.getopt(sys.argv[1:], 'i:pn')
  40. except getopt.error as msg:
  41. err(str(msg) + '\n')
  42. err(usage)
  43. sys.exit(2)
  44. for o, a in opts:
  45. if o == '-i':
  46. new_interpreter = a.encode()
  47. if o == '-p':
  48. preserve_timestamps = True
  49. if o == '-n':
  50. create_backup = False
  51. if not new_interpreter or not new_interpreter.startswith(b'/') or \
  52. not args:
  53. err('-i option or file-or-directory missing\n')
  54. err(usage)
  55. sys.exit(2)
  56. bad = 0
  57. for arg in args:
  58. if os.path.isdir(arg):
  59. if recursedown(arg): bad = 1
  60. elif os.path.islink(arg):
  61. err(arg + ': will not process symbolic links\n')
  62. bad = 1
  63. else:
  64. if fix(arg): bad = 1
  65. sys.exit(bad)
  66. def ispython(name):
  67. return name.endswith('.py')
  68. def recursedown(dirname):
  69. dbg('recursedown(%r)\n' % (dirname,))
  70. bad = 0
  71. try:
  72. names = os.listdir(dirname)
  73. except OSError as msg:
  74. err('%s: cannot list directory: %r\n' % (dirname, msg))
  75. return 1
  76. names.sort()
  77. subdirs = []
  78. for name in names:
  79. if name in (os.curdir, os.pardir): continue
  80. fullname = os.path.join(dirname, name)
  81. if os.path.islink(fullname): pass
  82. elif os.path.isdir(fullname):
  83. subdirs.append(fullname)
  84. elif ispython(name):
  85. if fix(fullname): bad = 1
  86. for fullname in subdirs:
  87. if recursedown(fullname): bad = 1
  88. return bad
  89. def fix(filename):
  90. ## dbg('fix(%r)\n' % (filename,))
  91. try:
  92. f = open(filename, 'rb')
  93. except IOError as msg:
  94. err('%s: cannot open: %r\n' % (filename, msg))
  95. return 1
  96. line = f.readline()
  97. fixed = fixline(line)
  98. if line == fixed:
  99. rep(filename+': no change\n')
  100. f.close()
  101. return
  102. head, tail = os.path.split(filename)
  103. tempname = os.path.join(head, '@' + tail)
  104. try:
  105. g = open(tempname, 'wb')
  106. except IOError as msg:
  107. f.close()
  108. err('%s: cannot create: %r\n' % (tempname, msg))
  109. return 1
  110. rep(filename + ': updating\n')
  111. g.write(fixed)
  112. BUFSIZE = 8*1024
  113. while 1:
  114. buf = f.read(BUFSIZE)
  115. if not buf: break
  116. g.write(buf)
  117. g.close()
  118. f.close()
  119. # Finishing touch -- move files
  120. mtime = None
  121. atime = None
  122. # First copy the file's mode to the temp file
  123. try:
  124. statbuf = os.stat(filename)
  125. mtime = statbuf.st_mtime
  126. atime = statbuf.st_atime
  127. os.chmod(tempname, statbuf[ST_MODE] & 0o7777)
  128. except OSError as msg:
  129. err('%s: warning: chmod failed (%r)\n' % (tempname, msg))
  130. # Then make a backup of the original file as filename~
  131. if create_backup:
  132. try:
  133. os.rename(filename, filename + '~')
  134. except OSError as msg:
  135. err('%s: warning: backup failed (%r)\n' % (filename, msg))
  136. else:
  137. try:
  138. os.remove(filename)
  139. except OSError as msg:
  140. err('%s: warning: removing failed (%r)\n' % (filename, msg))
  141. # Now move the temp file to the original file
  142. try:
  143. os.rename(tempname, filename)
  144. except OSError as msg:
  145. err('%s: rename failed (%r)\n' % (filename, msg))
  146. return 1
  147. if preserve_timestamps:
  148. if atime and mtime:
  149. try:
  150. os.utime(filename, (atime, mtime))
  151. except OSError as msg:
  152. err('%s: reset of timestamp failed (%r)\n' % (filename, msg))
  153. return 1
  154. # Return success
  155. return 0
  156. def fixline(line):
  157. if not line.startswith(b'#!'):
  158. return line
  159. if b"python" not in line:
  160. return line
  161. return b'#! ' + new_interpreter + b'\n'
  162. if __name__ == '__main__':
  163. main()