]>
Commit | Line | Data |
---|---|---|
4710c53d | 1 | #! /usr/bin/env python\r |
2 | \r | |
3 | # Released to the public domain, by Tim Peters, 03 October 2000.\r | |
4 | \r | |
5 | """reindent [-d][-r][-v] [ path ... ]\r | |
6 | \r | |
7 | -d (--dryrun) Dry run. Analyze, but don't make any changes to, files.\r | |
8 | -r (--recurse) Recurse. Search for all .py files in subdirectories too.\r | |
9 | -n (--nobackup) No backup. Does not make a ".bak" file before reindenting.\r | |
10 | -v (--verbose) Verbose. Print informative msgs; else no output.\r | |
11 | -h (--help) Help. Print this usage information and exit.\r | |
12 | \r | |
13 | Change Python (.py) files to use 4-space indents and no hard tab characters.\r | |
14 | Also trim excess spaces and tabs from ends of lines, and remove empty lines\r | |
15 | at the end of files. Also ensure the last line ends with a newline.\r | |
16 | \r | |
17 | If no paths are given on the command line, reindent operates as a filter,\r | |
18 | reading a single source file from standard input and writing the transformed\r | |
19 | source to standard output. In this case, the -d, -r and -v flags are\r | |
20 | ignored.\r | |
21 | \r | |
22 | You can pass one or more file and/or directory paths. When a directory\r | |
23 | path, all .py files within the directory will be examined, and, if the -r\r | |
24 | option is given, likewise recursively for subdirectories.\r | |
25 | \r | |
26 | If output is not to standard output, reindent overwrites files in place,\r | |
27 | renaming the originals with a .bak extension. If it finds nothing to\r | |
28 | change, the file is left alone. If reindent does change a file, the changed\r | |
29 | file is a fixed-point for future runs (i.e., running reindent on the\r | |
30 | resulting .py file won't change it again).\r | |
31 | \r | |
32 | The hard part of reindenting is figuring out what to do with comment\r | |
33 | lines. So long as the input files get a clean bill of health from\r | |
34 | tabnanny.py, reindent should do a good job.\r | |
35 | \r | |
36 | The backup file is a copy of the one that is being reindented. The ".bak"\r | |
37 | file is generated with shutil.copy(), but some corner cases regarding\r | |
38 | user/group and permissions could leave the backup file more readable that\r | |
39 | you'd prefer. You can always use the --nobackup option to prevent this.\r | |
40 | """\r | |
41 | \r | |
42 | __version__ = "1"\r | |
43 | \r | |
44 | import tokenize\r | |
45 | import os, shutil\r | |
46 | import sys\r | |
47 | \r | |
48 | verbose = 0\r | |
49 | recurse = 0\r | |
50 | dryrun = 0\r | |
51 | makebackup = True\r | |
52 | \r | |
53 | def usage(msg=None):\r | |
54 | if msg is not None:\r | |
55 | print >> sys.stderr, msg\r | |
56 | print >> sys.stderr, __doc__\r | |
57 | \r | |
58 | def errprint(*args):\r | |
59 | sep = ""\r | |
60 | for arg in args:\r | |
61 | sys.stderr.write(sep + str(arg))\r | |
62 | sep = " "\r | |
63 | sys.stderr.write("\n")\r | |
64 | \r | |
65 | def main():\r | |
66 | import getopt\r | |
67 | global verbose, recurse, dryrun, makebackup\r | |
68 | try:\r | |
69 | opts, args = getopt.getopt(sys.argv[1:], "drnvh",\r | |
70 | ["dryrun", "recurse", "nobackup", "verbose", "help"])\r | |
71 | except getopt.error, msg:\r | |
72 | usage(msg)\r | |
73 | return\r | |
74 | for o, a in opts:\r | |
75 | if o in ('-d', '--dryrun'):\r | |
76 | dryrun += 1\r | |
77 | elif o in ('-r', '--recurse'):\r | |
78 | recurse += 1\r | |
79 | elif o in ('-n', '--nobackup'):\r | |
80 | makebackup = False\r | |
81 | elif o in ('-v', '--verbose'):\r | |
82 | verbose += 1\r | |
83 | elif o in ('-h', '--help'):\r | |
84 | usage()\r | |
85 | return\r | |
86 | if not args:\r | |
87 | r = Reindenter(sys.stdin)\r | |
88 | r.run()\r | |
89 | r.write(sys.stdout)\r | |
90 | return\r | |
91 | for arg in args:\r | |
92 | check(arg)\r | |
93 | \r | |
94 | def check(file):\r | |
95 | if os.path.isdir(file) and not os.path.islink(file):\r | |
96 | if verbose:\r | |
97 | print "listing directory", file\r | |
98 | names = os.listdir(file)\r | |
99 | for name in names:\r | |
100 | fullname = os.path.join(file, name)\r | |
101 | if ((recurse and os.path.isdir(fullname) and\r | |
102 | not os.path.islink(fullname) and\r | |
103 | not os.path.split(fullname)[1].startswith("."))\r | |
104 | or name.lower().endswith(".py")):\r | |
105 | check(fullname)\r | |
106 | return\r | |
107 | \r | |
108 | if verbose:\r | |
109 | print "checking", file, "...",\r | |
110 | try:\r | |
111 | f = open(file)\r | |
112 | except IOError, msg:\r | |
113 | errprint("%s: I/O Error: %s" % (file, str(msg)))\r | |
114 | return\r | |
115 | \r | |
116 | r = Reindenter(f)\r | |
117 | f.close()\r | |
118 | if r.run():\r | |
119 | if verbose:\r | |
120 | print "changed."\r | |
121 | if dryrun:\r | |
122 | print "But this is a dry run, so leaving it alone."\r | |
123 | if not dryrun:\r | |
124 | bak = file + ".bak"\r | |
125 | if makebackup:\r | |
126 | shutil.copyfile(file, bak)\r | |
127 | if verbose:\r | |
128 | print "backed up", file, "to", bak\r | |
129 | f = open(file, "w")\r | |
130 | r.write(f)\r | |
131 | f.close()\r | |
132 | if verbose:\r | |
133 | print "wrote new", file\r | |
134 | return True\r | |
135 | else:\r | |
136 | if verbose:\r | |
137 | print "unchanged."\r | |
138 | return False\r | |
139 | \r | |
140 | def _rstrip(line, JUNK='\n \t'):\r | |
141 | """Return line stripped of trailing spaces, tabs, newlines.\r | |
142 | \r | |
143 | Note that line.rstrip() instead also strips sundry control characters,\r | |
144 | but at least one known Emacs user expects to keep junk like that, not\r | |
145 | mentioning Barry by name or anything <wink>.\r | |
146 | """\r | |
147 | \r | |
148 | i = len(line)\r | |
149 | while i > 0 and line[i-1] in JUNK:\r | |
150 | i -= 1\r | |
151 | return line[:i]\r | |
152 | \r | |
153 | class Reindenter:\r | |
154 | \r | |
155 | def __init__(self, f):\r | |
156 | self.find_stmt = 1 # next token begins a fresh stmt?\r | |
157 | self.level = 0 # current indent level\r | |
158 | \r | |
159 | # Raw file lines.\r | |
160 | self.raw = f.readlines()\r | |
161 | \r | |
162 | # File lines, rstripped & tab-expanded. Dummy at start is so\r | |
163 | # that we can use tokenize's 1-based line numbering easily.\r | |
164 | # Note that a line is all-blank iff it's "\n".\r | |
165 | self.lines = [_rstrip(line).expandtabs() + "\n"\r | |
166 | for line in self.raw]\r | |
167 | self.lines.insert(0, None)\r | |
168 | self.index = 1 # index into self.lines of next line\r | |
169 | \r | |
170 | # List of (lineno, indentlevel) pairs, one for each stmt and\r | |
171 | # comment line. indentlevel is -1 for comment lines, as a\r | |
172 | # signal that tokenize doesn't know what to do about them;\r | |
173 | # indeed, they're our headache!\r | |
174 | self.stats = []\r | |
175 | \r | |
176 | def run(self):\r | |
177 | tokenize.tokenize(self.getline, self.tokeneater)\r | |
178 | # Remove trailing empty lines.\r | |
179 | lines = self.lines\r | |
180 | while lines and lines[-1] == "\n":\r | |
181 | lines.pop()\r | |
182 | # Sentinel.\r | |
183 | stats = self.stats\r | |
184 | stats.append((len(lines), 0))\r | |
185 | # Map count of leading spaces to # we want.\r | |
186 | have2want = {}\r | |
187 | # Program after transformation.\r | |
188 | after = self.after = []\r | |
189 | # Copy over initial empty lines -- there's nothing to do until\r | |
190 | # we see a line with *something* on it.\r | |
191 | i = stats[0][0]\r | |
192 | after.extend(lines[1:i])\r | |
193 | for i in range(len(stats)-1):\r | |
194 | thisstmt, thislevel = stats[i]\r | |
195 | nextstmt = stats[i+1][0]\r | |
196 | have = getlspace(lines[thisstmt])\r | |
197 | want = thislevel * 4\r | |
198 | if want < 0:\r | |
199 | # A comment line.\r | |
200 | if have:\r | |
201 | # An indented comment line. If we saw the same\r | |
202 | # indentation before, reuse what it most recently\r | |
203 | # mapped to.\r | |
204 | want = have2want.get(have, -1)\r | |
205 | if want < 0:\r | |
206 | # Then it probably belongs to the next real stmt.\r | |
207 | for j in xrange(i+1, len(stats)-1):\r | |
208 | jline, jlevel = stats[j]\r | |
209 | if jlevel >= 0:\r | |
210 | if have == getlspace(lines[jline]):\r | |
211 | want = jlevel * 4\r | |
212 | break\r | |
213 | if want < 0: # Maybe it's a hanging\r | |
214 | # comment like this one,\r | |
215 | # in which case we should shift it like its base\r | |
216 | # line got shifted.\r | |
217 | for j in xrange(i-1, -1, -1):\r | |
218 | jline, jlevel = stats[j]\r | |
219 | if jlevel >= 0:\r | |
220 | want = have + getlspace(after[jline-1]) - \\r | |
221 | getlspace(lines[jline])\r | |
222 | break\r | |
223 | if want < 0:\r | |
224 | # Still no luck -- leave it alone.\r | |
225 | want = have\r | |
226 | else:\r | |
227 | want = 0\r | |
228 | assert want >= 0\r | |
229 | have2want[have] = want\r | |
230 | diff = want - have\r | |
231 | if diff == 0 or have == 0:\r | |
232 | after.extend(lines[thisstmt:nextstmt])\r | |
233 | else:\r | |
234 | for line in lines[thisstmt:nextstmt]:\r | |
235 | if diff > 0:\r | |
236 | if line == "\n":\r | |
237 | after.append(line)\r | |
238 | else:\r | |
239 | after.append(" " * diff + line)\r | |
240 | else:\r | |
241 | remove = min(getlspace(line), -diff)\r | |
242 | after.append(line[remove:])\r | |
243 | return self.raw != self.after\r | |
244 | \r | |
245 | def write(self, f):\r | |
246 | f.writelines(self.after)\r | |
247 | \r | |
248 | # Line-getter for tokenize.\r | |
249 | def getline(self):\r | |
250 | if self.index >= len(self.lines):\r | |
251 | line = ""\r | |
252 | else:\r | |
253 | line = self.lines[self.index]\r | |
254 | self.index += 1\r | |
255 | return line\r | |
256 | \r | |
257 | # Line-eater for tokenize.\r | |
258 | def tokeneater(self, type, token, (sline, scol), end, line,\r | |
259 | INDENT=tokenize.INDENT,\r | |
260 | DEDENT=tokenize.DEDENT,\r | |
261 | NEWLINE=tokenize.NEWLINE,\r | |
262 | COMMENT=tokenize.COMMENT,\r | |
263 | NL=tokenize.NL):\r | |
264 | \r | |
265 | if type == NEWLINE:\r | |
266 | # A program statement, or ENDMARKER, will eventually follow,\r | |
267 | # after some (possibly empty) run of tokens of the form\r | |
268 | # (NL | COMMENT)* (INDENT | DEDENT+)?\r | |
269 | self.find_stmt = 1\r | |
270 | \r | |
271 | elif type == INDENT:\r | |
272 | self.find_stmt = 1\r | |
273 | self.level += 1\r | |
274 | \r | |
275 | elif type == DEDENT:\r | |
276 | self.find_stmt = 1\r | |
277 | self.level -= 1\r | |
278 | \r | |
279 | elif type == COMMENT:\r | |
280 | if self.find_stmt:\r | |
281 | self.stats.append((sline, -1))\r | |
282 | # but we're still looking for a new stmt, so leave\r | |
283 | # find_stmt alone\r | |
284 | \r | |
285 | elif type == NL:\r | |
286 | pass\r | |
287 | \r | |
288 | elif self.find_stmt:\r | |
289 | # This is the first "real token" following a NEWLINE, so it\r | |
290 | # must be the first token of the next program statement, or an\r | |
291 | # ENDMARKER.\r | |
292 | self.find_stmt = 0\r | |
293 | if line: # not endmarker\r | |
294 | self.stats.append((sline, self.level))\r | |
295 | \r | |
296 | # Count number of leading blanks.\r | |
297 | def getlspace(line):\r | |
298 | i, n = 0, len(line)\r | |
299 | while i < n and line[i] == " ":\r | |
300 | i += 1\r | |
301 | return i\r | |
302 | \r | |
303 | if __name__ == '__main__':\r | |
304 | main()\r |