home *** CD-ROM | disk | FTP | other *** search
/ Celestin Apprentice 2 / Apprentice-Release2.iso / Tools / Languages / Python 1.1 / Demo / scripts / pindent.py < prev    next >
Encoding:
Text File  |  1994-08-19  |  11.8 KB  |  430 lines  |  [TEXT/R*ch]

  1.  "block-closing comment" is a comment of the form '# end <keyword>'
  2. # where <keyword> is the keyword that opened the block.  If the
  3. # opening keyword is 'def' or 'class', the function or class name may
  4. # be repeated in the block-closing comment as well.  Here is an
  5. # example of a program fully augmented with block-closing comments:
  6.  
  7. # def foobar(a, b):
  8. #    if a == b:
  9. #        a = a+1
  10. #    elif a < b:
  11. #        b = b-1
  12. #        if b > a: a = a-1
  13. #        # end if
  14. #    else:
  15. #        print 'oops!'
  16. #    # end if
  17. # # end def foobar
  18.  
  19. # Note that only the last part of an if...elif...else... block needs a
  20. # block-closing comment; the same is true for other compound
  21. # statements (e.g. try...except).  Also note that "short-form" blocks
  22. # like the second 'if' in the example must be closed as well;
  23. # otherwise the 'else' in the example would be ambiguous (remember
  24. # that indentation is not significant when interpreting block-closing
  25. # comments).
  26.  
  27. # Both operations are idempotent (i.e. applied to their own output
  28. # they yield an identical result).  Running first "pindent -c" and
  29. # then "pindent -r" on a valid Python program produces a program that
  30. # is semantically identical to the input (though its indentation may
  31. # be different).
  32.  
  33. # Other options:
  34. # -s stepsize: set the indentation step size (default 8)
  35. # -t tabsize : set the number of spaces a tab character is worth (default 8)
  36. # file ...   : input file(s) (default standard input)
  37. # The results always go to standard output
  38.  
  39. # Caveats:
  40. # - comments ending in a backslash will be mistaken for continued lines
  41. # - continuations using backslash are always left unchanged
  42. # - continuations inside parentheses are not extra indented by -r
  43. #   but must be indented for -c to work correctly (this breaks
  44. #   idempotency!)
  45. # - continued lines inside triple-quoted strings are totally garbled
  46.  
  47. # Secret feature:
  48. # - On input, a block may also be closed with an "end statement" --
  49. #   this is a block-closing comment without the '#' sign.
  50.  
  51. # Possible improvements:
  52. # - check syntax based on transitions in 'next' table
  53. # - better error reporting
  54. # - better error recovery
  55. # - check identifier after class/def
  56.  
  57. # The following wishes need a more complete tokenization of the source:
  58. # - Don't get fooled by comments ending in backslash
  59. # - reindent continuation lines indicated by backslash
  60. # - handle continuation lines inside parentheses/braces/brackets
  61. # - handle triple quoted strings spanning lines
  62. # - realign comments
  63. # - optionally do much more thorough reformatting, a la C indent
  64.  
  65. # Defaults
  66. STEPSIZE = 8
  67. TABSIZE = 8
  68.  
  69. import os
  70. import regex
  71. import string
  72. import sys
  73.  
  74. next = {}
  75. next['if'] = next['elif'] = 'elif', 'else', 'end'
  76. next['while'] = next['for'] = 'else', 'end'
  77. next['try'] = 'except', 'finally'
  78. next['except'] = 'except', 'else', 'end'
  79. next['else'] = next['finally'] = next['def'] = next['class'] = 'end'
  80. next['end'] = ()
  81. start = 'if', 'while', 'for', 'try', 'def', 'class'
  82.  
  83. class PythonIndenter:
  84.  
  85.     def __init__(self, fpi = sys.stdin, fpo = sys.stdout,
  86.              indentsize = STEPSIZE, tabsize = TABSIZE):
  87.         self.fpi = fpi
  88.         self.fpo = fpo
  89.         self.indentsize = indentsize
  90.         self.tabsize = tabsize
  91.         self.lineno = 0
  92.         self.write = fpo.write
  93.         self.kwprog = regex.symcomp(
  94.             '^[ \t]*\(<kw>[a-z]+\)'
  95.             '\([ \t]+\(<id>[a-zA-Z_][a-zA-Z0-9_]*\)\)?'
  96.             '[^a-zA-Z0-9_]')
  97.         self.endprog = regex.symcomp(
  98.             '^[ \t]*#?[ \t]*end[ \t]+\(<kw>[a-z]+\)'
  99.             '\([ \t]+\(<id>[a-zA-Z_][a-zA-Z0-9_]*\)\)?'
  100.             '[^a-zA-Z0-9_]')
  101.         self.wsprog = regex.compile('^[ \t]*')
  102.     # end def __init__
  103.  
  104.     def readline(self):
  105.         line = self.fpi.readline()
  106.         if line: self.lineno = self.lineno + 1
  107.         # end if
  108.         return line
  109.     # end def readline
  110.  
  111.     def error(self, fmt, *args):
  112.         if args: fmt = fmt % args
  113.         # end if
  114.         sys.stderr.write('Error at line %d: %s\n' % (self.lineno, fmt))
  115.         self.write('### %s ###\n' % fmt)
  116.     # end def error
  117.  
  118.     def getline(self):
  119.         line = self.readline()
  120.         while line[-2:] == '\\\n':
  121.             line2 = self.readline()
  122.             if not line2: break
  123.             # end if
  124.             line = line + line2
  125.         # end while
  126.         return line
  127.     # end def getline
  128.  
  129.     def putline(self, line, indent = None):
  130.         if indent is None:
  131.             self.write(line)
  132.             return
  133.         # end if
  134.         tabs, spaces = divmod(indent*self.indentsize, self.tabsize)
  135.         i = max(0, self.wsprog.match(line))
  136.         self.write('\t'*tabs + ' '*spaces + line[i:])
  137.     # end def putline
  138.  
  139.     def reformat(self):
  140.         stack = []
  141.         while 1:
  142.             line = self.getline()
  143.             if not line: break    # EOF
  144.             # end if
  145.             if self.endprog.match(line) >= 0:
  146.                 kw = 'end'
  147.                 kw2 = self.endprog.group('kw')
  148.                 if not stack:
  149.                     self.error('unexpected end')
  150.                 elif stack[-1][0] != kw2:
  151.                     self.error('unmatched end')
  152.                 # end if
  153.                 del stack[-1:]
  154.                 self.putline(line, len(stack))
  155.                 continue
  156.             # end if
  157.             if self.kwprog.match(line) >= 0:
  158.                 kw = self.kwprog.group('kw')
  159.                 if kw in start:
  160.                     self.putline(line, len(stack))
  161.                     stack.append((kw, kw))
  162.                     continue
  163.                 # end if
  164.                 if next.has_key(kw) and stack:
  165.                     self.putline(line, len(stack)-1)
  166.                     kwa, kwb = stack[-1]
  167.                     stack[-1] = kwa, kw
  168.                     continue
  169.                 # end if
  170.             # end if
  171.             self.putline(line, len(stack))
  172.         # end while
  173.         if stack:
  174.             self.error('unterminated keywords')
  175.             for kwa, kwb in stack:
  176.                 self.write('\t%s\n' % kwa)
  177.             # end for
  178.         # end if
  179.     # end def reformat
  180.  
  181.     def complete(self):
  182.         self.indentsize = 1
  183.         stack = []
  184.         todo = []
  185.         current, firstkw, lastkw, topid = 0, '', '', ''
  186.         while 1:
  187.             line = self.getline()
  188.             i = max(0, self.wsprog.match(line))
  189.             if self.endprog.match(line) >= 0:
  190.                 thiskw = 'end'
  191.                 endkw = self.endprog.group('kw')
  192.                 thisid = self.endprog.group('id')
  193.             elif self.kwprog.match(line) >= 0:
  194.                 thiskw = self.kwprog.group('kw')
  195.                 if not next.has_key(thiskw):
  196.                     thiskw = ''
  197.                 # end if
  198.                 if thiskw in ('def', 'class'):
  199.                     thisid = self.kwprog.group('id')
  200.                 else:
  201.                     thisid = ''
  202.                 # end if
  203.             elif line[i:i+1] in ('\n', '#'):
  204.                 todo.append(line)
  205.                 continue
  206.             else:
  207.                 thiskw = ''
  208.             # end if
  209.             indent = len(string.expandtabs(line[:i], self.tabsize))
  210.             while indent < current:
  211.                 if firstkw:
  212.                     if topid:
  213.                         s = '# end %s %s\n' % (
  214.                             firstkw, topid)
  215.                     else:
  216.                         s = '# end %s\n' % firstkw
  217.                     # end if
  218.                     self.putline(s, current)
  219.                     firstkw = lastkw = ''
  220.                 # end if
  221.                 current, firstkw, lastkw, topid = stack[-1]
  222.                 del stack[-1]
  223.             # end while
  224.             if indent == current and firstkw:
  225.                 if thiskw == 'end':
  226.                     if endkw != firstkw:
  227.                         self.error('mismatched end')
  228.                     # end if
  229.                     firstkw = lastkw = ''
  230.                 elif not thiskw or thiskw in start:
  231.                     if topid:
  232.                         s = '# end %s %s\n' % (
  233.                             firstkw, topid)
  234.                     else:
  235.                         s = '# end %s\n' % firstkw
  236.                     # end if
  237.                     self.putline(s, current)
  238.                     firstkw = lastkw = topid = ''
  239.                 # end if
  240.             # end if
  241.             if indent > current:
  242.                 stack.append(current, firstkw, lastkw, topid)
  243.                 if thiskw and thiskw not in start:
  244.                     # error
  245.                     thiskw = ''
  246.                 # end if
  247.                 current, firstkw, lastkw, topid = \
  248.                      indent, thiskw, thiskw, thisid
  249.             # end if
  250.             if thiskw:
  251.                 if thiskw in start:
  252.                     firstkw = lastkw = thiskw
  253.                     topid = thisid
  254.                 else:
  255.                     lastkw = thiskw
  256.                 # end if
  257.             # end if
  258.             for l in todo: self.write(l)
  259.             # end for
  260.             todo = []
  261.             if not line: break
  262.             # end if
  263.             self.write(line)
  264.         # end while
  265.     # end def complete
  266.  
  267. # end class PythonIndenter
  268.  
  269. # Simplified user interface
  270. # - xxx_filter(input, output): read and write file objects
  271. # - xxx_string(s): take and return string object
  272. # - xxx_file(filename): process file in place, return true iff changed
  273.  
  274. def complete_filter(input= sys.stdin, output = sys.stdout,
  275.             stepsize = STEPSIZE, tabsize = TABSIZE):
  276.     pi = PythonIndenter(input, output, stepsize, tabsize)
  277.     pi.complete()
  278. # end def complete_filter
  279.  
  280. def reformat_filter(input = sys.stdin, output = sys.stdout,
  281.             stepsize = STEPSIZE, tabsize = TABSIZE):
  282.     pi = PythonIndenter(input, output, stepsize, tabsize)
  283.     pi.reformat()
  284. # end def reformat
  285.  
  286. class StringReader:
  287.     def __init__(self, buf):
  288.         self.buf = buf
  289.         self.pos = 0
  290.         self.len = len(self.buf)
  291.     # end def __init__
  292.     def read(self, n = 0):
  293.         if n <= 0:
  294.             n = self.len - self.pos
  295.         else:
  296.             n = min(n, self.len - self.pos)
  297.         # end if
  298.         r = self.buf[self.pos : self.pos + n]
  299.         self.pos = self.pos + n
  300.         return r
  301.     # end def read
  302.     def readline(self):
  303.         i = string.find(self.buf, '\n', self.pos)
  304.         return self.read(i + 1 - self.pos)
  305.     # end def readline
  306.     def readlines(self):
  307.         lines = []
  308.         line = self.readline()
  309.         while line:
  310.             lines.append(line)
  311.             line = self.readline()
  312.         # end while
  313.         return lines
  314.     # end def readlines
  315.     # seek/tell etc. are left as an exercise for the reader
  316. # end class StringReader
  317.  
  318. class StringWriter:
  319.     def __init__(self):
  320.         self.buf = ''
  321.     # end def __init__
  322.     def write(self, s):
  323.         self.buf = self.buf + s
  324.     # end def write
  325.     def getvalue(self):
  326.         return self.buf
  327.     # end def getvalue
  328. # end class StringWriter
  329.  
  330. def complete_string(source, stepsize = STEPSIZE, tabsize = TABSIZE):
  331.     input = StringReader(source)
  332.     output = StringWriter()
  333.     pi = PythonIndenter(input, output, stepsize, tabsize)
  334.     pi.complete()
  335.     return output.getvalue()
  336. # end def complete_string
  337.  
  338. def reformat_string(source, stepsize = STEPSIZE, tabsize = TABSIZE):
  339.     input = StringReader(source)
  340.     output = StringWriter()
  341.     pi = PythonIndenter(input, output, stepsize, tabsize)
  342.     pi.reformat()
  343.     return output.getvalue()
  344. # end def reformat_string
  345.  
  346. def complete_file(filename, stepsize = STEPSIZE, tabsize = TABSIZE):
  347.     source = open(filename, 'r').read()
  348.     result = complete_string(source, stepsize, tabsize)
  349.     if source == result: return 0
  350.     # end if
  351.     import os
  352.     try: os.rename(filename, filename + '~')
  353.     except os.error: pass
  354.     # end try
  355.     f = open(filename, 'w')
  356.     f.write(result)
  357.     f.close()
  358.     return 1
  359. # end def complete_file
  360.  
  361. def reformat_file(filename, stepsize = STEPSIZE, tabsize = TABSIZE):
  362.     source = open(filename, 'r').read()
  363.     result = reformat_string(source, stepsize, tabsize)
  364.     if source == result: return 0
  365.     # end if
  366.     import os
  367.     os.rename(filename, filename + '~')
  368.     f = open(filename, 'w')
  369.     f.write(result)
  370.     f.close()
  371.     return 1
  372. # end def reformat_file
  373.  
  374. # Test program when called as a script
  375.  
  376. usage = """
  377. usage: pindent (-c|-r) [-s stepsize] [-t tabsize] [file] ...
  378. -c         : complete a correctly indented program (add #end directives)
  379. -r         : reformat a completed program (use #end directives)
  380. -s stepsize: indentation step (default %(STEPSIZE)d)
  381. -t tabsize : the worth in spaces of a tab (default %(TABSIZE)d)
  382. [file] ... : files are changed in place, with backups in file~
  383. If no files are specified or a single - is given,
  384. the program acts as a filter (reads stdin, writes stdout).
  385. """ % vars()
  386.  
  387. def test():
  388.     import getopt
  389.     try:
  390.         opts, args = getopt.getopt(sys.argv[1:], 'crs:t:')
  391.     except getopt.error, msg:
  392.         sys.stderr.write('Error: %s\n' % msg)
  393.         sys.stderr.write(usage)
  394.         sys.exit(2)
  395.     # end try
  396.     action = None
  397.     stepsize = STEPSIZE
  398.     tabsize = TABSIZE
  399.     for o, a in opts:
  400.         if o == '-c':
  401.             action = 'complete'
  402.         elif o == '-r':
  403.             action = 'reformat'
  404.         elif o == '-s':
  405.             stepsize = string.atoi(a)
  406.         elif o == '-t':
  407.             tabsize = string.atoi(a)
  408.         # end if
  409.     # end for
  410.     if not action:
  411.         sys.stderr.write(
  412.             'You must specify -c(omplete) or -r(eformat)\n')
  413.         sys.stderr.write(usage)
  414.         sys.exit(2)
  415.     # end if
  416.     if not args or args == ['-']:
  417.         action = eval(action + '_filter')
  418.         action(sys.stdin, sys.stdout, stepsize, tabsize)
  419.     else:
  420.         action = eval(action + '_file')
  421.         for file in args:
  422.             action(file, stepsize, tabsize)
  423.         # end for
  424.     # end if
  425. # end def test
  426.  
  427. if __name__ == '__main__':
  428.     test()
  429. # end if
  430.