git @ Cat's Eye Technologies yucca / e232206
Move script to a `bin` directory, for consistency. --HG-- rename : yucca => bin/yucca catseye 13 years ago
2 changed file(s) with 443 addition(s) and 443 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/bin/env python
1
2 # Copyright (c)2012 Chris Pressey, Cat's Eye Technologies
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining a
5 # copy of this software and associated documentation files (the "Software"),
6 # to deal in the Software without restriction, including without limitation
7 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Software, and to permit persons to whom the
9 # Software is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be included in
12 # all copies or substantial portions of the Software.
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 # DEALINGS IN THE SOFTWARE.
21
22 import re
23 import sys
24 import fileinput
25 from optparse import OptionParser
26
27
28 class LineNumber(object):
29 """An object representing the line number of a line in a
30 BASIC program.
31
32 The object retains any whitespace encountered while parsing
33 the line, and outputs it in its string representation.
34
35 """
36 def __init__(self, text):
37 self.text = text
38 self.number = int(text)
39
40 def __str__(self):
41 return self.text
42
43
44 class BasicCommand(object):
45 @classmethod
46 def create(class_, text):
47 match = re.match(r'^(\s*rem)(.*)$', text, re.I)
48 if match:
49 return Remark(match.group(1), match.group(2))
50 match = re.match(r'^(\s*goto)(\s*\d+\s*)$', text, re.I)
51 if match:
52 return Goto(match.group(1), LineNumber(match.group(2)))
53 match = re.match(r'^(\s*gosub)(\s*\d+\s*)$', text, re.I)
54 if match:
55 return Gosub(match.group(1), LineNumber(match.group(2)))
56 match = re.match(r'^(\s*if(.*?)(then|goto))(\s*\d+\s*)$', text, re.I)
57 if match:
58 return IfThenLine(match.group(1), LineNumber(match.group(4)))
59 match = re.match(r'^(\s*if(.*?)then)(.*?)$', text, re.I)
60 if match:
61 return IfThen(match.group(1), BasicCommand.create(match.group(3)))
62 match = re.match(r'^(\s*on(.*?)go(to|sub))(.*?)$', text, re.I)
63 if match:
64 line_numbers = [LineNumber(x) for x in match.group(4).split(',')]
65 return OnLines(match.group(1), line_numbers)
66 return GenericCommand(text)
67
68 def referenced_line_numbers(self):
69 raise NotImplementedError
70
71
72 class GenericCommand(BasicCommand):
73 def __init__(self, text):
74 self.text = text
75
76 def __str__(self):
77 return self.text
78
79 def referenced_line_numbers(self):
80 return []
81
82
83 class Remark(BasicCommand):
84 def __init__(self, command, text):
85 self.command = command
86 self.text = text
87
88 def __str__(self):
89 return "%s%s" % (self.command, self.text)
90
91 def referenced_line_numbers(self):
92 return []
93
94
95 class IfThen(BasicCommand):
96 def __init__(self, body, subsequent):
97 self.body = body
98 self.subsequent = subsequent
99
100 def __str__(self):
101 return "%s%s" % (self.body, self.subsequent)
102
103 def referenced_line_numbers(self):
104 return self.subsequent.referenced_line_numbers()
105
106
107 class IfThenLine(BasicCommand):
108 def __init__(self, body, line_number):
109 self.body = body
110 self.line_number = line_number
111
112 def __str__(self):
113 return "%s%s" % (self.body, self.line_number)
114
115 def referenced_line_numbers(self):
116 return [self.line_number]
117
118
119 class Goto(BasicCommand):
120 def __init__(self, command, line_number):
121 self.command = command
122 self.line_number = line_number
123
124 def __str__(self):
125 return "%s%s" % (self.command, self.line_number)
126
127 def referenced_line_numbers(self):
128 return [self.line_number]
129
130
131 class Gosub(BasicCommand):
132 def __init__(self, command, line_number):
133 self.command = command
134 self.line_number = line_number
135
136 def __str__(self):
137 return "%s%s" % (self.command, self.line_number)
138
139 def referenced_line_numbers(self):
140 return [self.line_number]
141
142
143 class OnLines(BasicCommand):
144 def __init__(self, body, line_numbers):
145 self.body = body
146 self.line_numbers = line_numbers
147
148 def __str__(self):
149 return "%s%s" % (self.body,
150 ','.join(str(x) for x in self.line_numbers))
151
152 def referenced_line_numbers(self):
153 return self.line_numbers
154
155
156 class BasicLine(object):
157 def __init__(self, text):
158 self.line_number = None
159 if text is None:
160 self.command = None
161 return
162 text = text.rstrip('\r\n')
163 match = re.match(r'^(\s*\d+\s*)(.*?)$', text)
164 if match:
165 self.line_number = LineNumber(match.group(1))
166 text = match.group(2)
167
168 self.commands = []
169
170 index = 0
171 start = 0
172 state = 'start'
173 while index < len(text):
174 if state in ('start', 'cmd'):
175 match = re.match(r'^rem', text[index:], re.I)
176 if match:
177 state = 'remark'
178 else:
179 state = 'cmd'
180 if state == 'cmd':
181 if text[index] == '"':
182 state = 'quoted'
183 elif text[index] == ':':
184 cmd = BasicCommand.create(text[start:index])
185 self.commands.append(cmd)
186 start = index + 1
187 state = 'start'
188 elif state == 'quoted':
189 if text[index] == '"':
190 state = 'cmd'
191 elif state == 'remark':
192 pass
193 index += 1
194 cmd = BasicCommand.create(text[start:index])
195 self.commands.append(cmd)
196
197 def __str__(self):
198 text = ':'.join(str(x) for x in self.commands)
199 if self.line_number:
200 return "%s%s" % (self.line_number, text)
201 else:
202 return text
203
204 def referenced_line_numbers(self):
205 line_numbers = []
206 for command in self.commands:
207 line_numbers.extend(command.referenced_line_numbers())
208 return line_numbers
209
210 def strip_remarks(self):
211 new_commands = []
212 for command in self.commands:
213 if not isinstance(command, Remark):
214 new_commands.append(command)
215 if new_commands:
216 new_line = BasicLine(None)
217 new_line.line_number = self.line_number
218 new_line.commands = new_commands
219 return new_line
220 else:
221 return None
222
223
224 class BasicProgram(object):
225 r"""An object which represents a BASIC program.
226
227 Rudimentary parsing of lines of commands:
228
229 >>> b = BasicProgram('10 PRINT "HELLO"\n'
230 ... '20 GOTO 10\n')
231 >>> b.dump()
232 10 PRINT "HELLO"
233 20 GOTO 10
234 >>> len(b.lines)
235 2
236 >>> print b.lines[0].commands[0]
237 PRINT "HELLO"
238 >>> print b.lines[1].commands[0].__class__.__name__
239 Goto
240
241 Checking for jumps to non-existant line numbers:
242
243 >>> b = BasicProgram()
244 >>> b.add_line('10 PRINT "HELLO"')
245 >>> b.add_line('20 GOTO 30')
246 >>> len(b.lines)
247 2
248 >>> num_errors = b.check_line_numbers()
249 ?UNDEFINED STATEMENT "30" IN: 20 GOTO 30
250 >>> num_errors
251 1
252
253 Checking for GOSUB and ON GOTO, and retaining case in
254 error messages:
255
256 >>> b = BasicProgram('5 goSUb 10\n'
257 ... '7goSUb8\n'
258 ... '10 oN (X+1 )* 5 gOtO 100,6')
259 >>> num_errors = b.check_line_numbers()
260 ?UNDEFINED STATEMENT "100" IN: 10 oN (X+1 )* 5 gOtO 100,6
261 ?UNDEFINED STATEMENT "6" IN: 10 oN (X+1 )* 5 gOtO 100,6
262 ?UNDEFINED STATEMENT "8" IN: 7goSUb8
263
264 Whitespace and case is retained when dumping a program:
265
266 >>> b = BasicProgram('5 goSUb 10\n'
267 ... '7gOSub8\n'
268 ... '9 rem WHAT? ??:>?: >?\n'
269 ... '\n'
270 ... '800 PRINT::print:ZORK 30\n'
271 ... '10 oN ERROR gOtO 100, 6,7, 800 ,3\n'
272 ... ' 99 what \n'
273 ... 'if50then60\n'
274 ... '50ifthisstuffistruegoto70\n'
275 ... '60 If This Stuff Is True Then Print:GoTo 9\n'
276 ... )
277 >>> b.dump()
278 5 goSUb 10
279 7gOSub8
280 9 rem WHAT? ??:>?: >?
281 <BLANKLINE>
282 800 PRINT::print:ZORK 30
283 10 oN ERROR gOtO 100, 6,7, 800 ,3
284 99 what
285 if50then60
286 50ifthisstuffistruegoto70
287 60 If This Stuff Is True Then Print:GoTo 9
288
289 Remarks may contain colons:
290
291 >>> b = BasicProgram('10 REM HELLO: GOTO 20')
292 >>> len(b.lines[0].commands)
293 1
294 >>> print b.lines[0].commands[0].__class__.__name__
295 Remark
296 >>> b.check_line_numbers()
297 0
298
299 Immediate mode commands are checked, and can be stripped:
300
301 >>> b = BasicProgram('10 REM HELLO\n'
302 ... 'PRINT "HELLO"\n'
303 ... 'GOTO 20')
304 >>> num_errors = b.check_line_numbers()
305 ?UNDEFINED STATEMENT "20" IN: GOTO 20 (immediate mode, text file line 3)
306 >>> b.strip_immediate_mode_commands()
307 >>> b.dump()
308 10 REM HELLO
309 >>> b.check_line_numbers()
310 0
311
312 Remarks, both on numbered lines and immediate mode, can be
313 stripped:
314
315 >>> b = BasicProgram('10 PRINT "HI":REM HELLO\n'
316 ... 'PRINT "HELLO"\n'
317 ... 'REM WHAT?\n'
318 ... '20 GOTO 30\n'
319 ... '30 REM THIS IS BUNK, MAN\n')
320 >>> b.strip_remarks()
321 >>> b.dump()
322 10 PRINT "HI"
323 PRINT "HELLO"
324 20 GOTO 30
325 >>> num_errors = b.check_line_numbers()
326 ?UNDEFINED STATEMENT "30" IN: 20 GOTO 30
327
328 """
329 def __init__(self, text=None):
330 self.lines = []
331 if text is not None:
332 for line in text.rstrip('\r\n').split('\n'):
333 self.add_line(line)
334
335 def add_line(self, line):
336 self.lines.append(BasicLine(line))
337
338 def check_line_numbers(self):
339 referenced = {}
340 defined = {}
341 errors = 0
342 text_file_line = 1
343 for line in self.lines:
344 if line.line_number is not None:
345 location = line.line_number.number
346 defined[location] = line
347 else:
348 location = "IMMEDIATE MODE (line %d)" % text_file_line
349 defined[location] = "%s (immediate mode, text file line %d)" % \
350 (line, text_file_line)
351 referenced[location] = line.referenced_line_numbers()
352 text_file_line += 1
353 for (location, referenced_line_numbers) in referenced.iteritems():
354 for referenced_line_number in referenced_line_numbers:
355 if referenced_line_number.number not in defined:
356 print '?UNDEFINED STATEMENT "%s" IN: %s' % (
357 referenced_line_number.number, defined[location])
358 errors += 1
359 return errors
360
361 def strip_immediate_mode_commands(self):
362 new_lines = []
363 for line in self.lines:
364 if line.line_number is not None:
365 new_lines.append(line)
366 self.lines = new_lines
367
368 def strip_remarks(self, program_lines_only=False):
369 new_lines = []
370 for line in self.lines:
371 if program_lines_only and line.line_number is None:
372 new_lines.append(line)
373 continue
374 new_line = line.strip_remarks()
375 if new_line is not None:
376 new_lines.append(new_line)
377 self.lines = new_lines
378
379 def dump(self):
380 for line in self.lines:
381 print line
382
383
384 if __name__ == '__main__':
385 parser = OptionParser()
386 parser.add_option("-I", "--strip-immediate-mode",
387 dest="strip_immediate_mode",
388 action="store_true",
389 help="strip immediate mode commands (implies -o)")
390 parser.add_option("-L", "--no-check-line-numbers",
391 dest="check_line_numbers",
392 default=True,
393 action="store_false",
394 help="do not check that all target line numbers exist")
395 parser.add_option("-o", "--dump-output",
396 dest="dump_output",
397 action="store_true",
398 help="dump (possibly transformed) program to standard "
399 "output; implied by other options")
400 parser.add_option("-p", "--program-lines-only",
401 dest="program_lines_only",
402 action="store_true",
403 help="have transformations only affect program lines, "
404 "not immediate mode lines")
405 parser.add_option("-R", "--strip-remarks",
406 dest="strip_remarks",
407 action="store_true",
408 help="strip all REM statements from program (implies -o)")
409 parser.add_option("-t", "--test",
410 action="store_true", dest="test", default=False,
411 help="run internal test cases and exit")
412
413 (options, args) = parser.parse_args()
414
415 if options.test:
416 import doctest
417 doctest.testmod()
418 sys.exit(0)
419
420 p = BasicProgram()
421 for line in fileinput.input(args):
422 p.add_line(line)
423
424 if options.strip_immediate_mode:
425 options.dump_output = True
426 p.strip_immediate_mode_commands()
427
428 if options.strip_remarks:
429 options.dump_output = True
430 p.strip_remarks(program_lines_only=options.program_lines_only)
431
432 errors = 0
433 if options.check_line_numbers:
434 errors += p.check_line_numbers()
435
436 if errors > 0:
437 sys.exit(1)
438
439 if options.dump_output:
440 p.dump()
441
442 sys.exit(0)
+0
-443
yucca less more
0 #!/usr/bin/env python
1
2 # Copyright (c)2012 Chris Pressey, Cat's Eye Technologies
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining a
5 # copy of this software and associated documentation files (the "Software"),
6 # to deal in the Software without restriction, including without limitation
7 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Software, and to permit persons to whom the
9 # Software is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be included in
12 # all copies or substantial portions of the Software.
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 # DEALINGS IN THE SOFTWARE.
21
22 import re
23 import sys
24 import fileinput
25 from optparse import OptionParser
26
27
28 class LineNumber(object):
29 """An object representing the line number of a line in a
30 BASIC program.
31
32 The object retains any whitespace encountered while parsing
33 the line, and outputs it in its string representation.
34
35 """
36 def __init__(self, text):
37 self.text = text
38 self.number = int(text)
39
40 def __str__(self):
41 return self.text
42
43
44 class BasicCommand(object):
45 @classmethod
46 def create(class_, text):
47 match = re.match(r'^(\s*rem)(.*)$', text, re.I)
48 if match:
49 return Remark(match.group(1), match.group(2))
50 match = re.match(r'^(\s*goto)(\s*\d+\s*)$', text, re.I)
51 if match:
52 return Goto(match.group(1), LineNumber(match.group(2)))
53 match = re.match(r'^(\s*gosub)(\s*\d+\s*)$', text, re.I)
54 if match:
55 return Gosub(match.group(1), LineNumber(match.group(2)))
56 match = re.match(r'^(\s*if(.*?)(then|goto))(\s*\d+\s*)$', text, re.I)
57 if match:
58 return IfThenLine(match.group(1), LineNumber(match.group(4)))
59 match = re.match(r'^(\s*if(.*?)then)(.*?)$', text, re.I)
60 if match:
61 return IfThen(match.group(1), BasicCommand.create(match.group(3)))
62 match = re.match(r'^(\s*on(.*?)go(to|sub))(.*?)$', text, re.I)
63 if match:
64 line_numbers = [LineNumber(x) for x in match.group(4).split(',')]
65 return OnLines(match.group(1), line_numbers)
66 return GenericCommand(text)
67
68 def referenced_line_numbers(self):
69 raise NotImplementedError
70
71
72 class GenericCommand(BasicCommand):
73 def __init__(self, text):
74 self.text = text
75
76 def __str__(self):
77 return self.text
78
79 def referenced_line_numbers(self):
80 return []
81
82
83 class Remark(BasicCommand):
84 def __init__(self, command, text):
85 self.command = command
86 self.text = text
87
88 def __str__(self):
89 return "%s%s" % (self.command, self.text)
90
91 def referenced_line_numbers(self):
92 return []
93
94
95 class IfThen(BasicCommand):
96 def __init__(self, body, subsequent):
97 self.body = body
98 self.subsequent = subsequent
99
100 def __str__(self):
101 return "%s%s" % (self.body, self.subsequent)
102
103 def referenced_line_numbers(self):
104 return self.subsequent.referenced_line_numbers()
105
106
107 class IfThenLine(BasicCommand):
108 def __init__(self, body, line_number):
109 self.body = body
110 self.line_number = line_number
111
112 def __str__(self):
113 return "%s%s" % (self.body, self.line_number)
114
115 def referenced_line_numbers(self):
116 return [self.line_number]
117
118
119 class Goto(BasicCommand):
120 def __init__(self, command, line_number):
121 self.command = command
122 self.line_number = line_number
123
124 def __str__(self):
125 return "%s%s" % (self.command, self.line_number)
126
127 def referenced_line_numbers(self):
128 return [self.line_number]
129
130
131 class Gosub(BasicCommand):
132 def __init__(self, command, line_number):
133 self.command = command
134 self.line_number = line_number
135
136 def __str__(self):
137 return "%s%s" % (self.command, self.line_number)
138
139 def referenced_line_numbers(self):
140 return [self.line_number]
141
142
143 class OnLines(BasicCommand):
144 def __init__(self, body, line_numbers):
145 self.body = body
146 self.line_numbers = line_numbers
147
148 def __str__(self):
149 return "%s%s" % (self.body,
150 ','.join(str(x) for x in self.line_numbers))
151
152 def referenced_line_numbers(self):
153 return self.line_numbers
154
155
156 class BasicLine(object):
157 def __init__(self, text):
158 self.line_number = None
159 if text is None:
160 self.command = None
161 return
162 text = text.rstrip('\r\n')
163 match = re.match(r'^(\s*\d+\s*)(.*?)$', text)
164 if match:
165 self.line_number = LineNumber(match.group(1))
166 text = match.group(2)
167
168 self.commands = []
169
170 index = 0
171 start = 0
172 state = 'start'
173 while index < len(text):
174 if state in ('start', 'cmd'):
175 match = re.match(r'^rem', text[index:], re.I)
176 if match:
177 state = 'remark'
178 else:
179 state = 'cmd'
180 if state == 'cmd':
181 if text[index] == '"':
182 state = 'quoted'
183 elif text[index] == ':':
184 cmd = BasicCommand.create(text[start:index])
185 self.commands.append(cmd)
186 start = index + 1
187 state = 'start'
188 elif state == 'quoted':
189 if text[index] == '"':
190 state = 'cmd'
191 elif state == 'remark':
192 pass
193 index += 1
194 cmd = BasicCommand.create(text[start:index])
195 self.commands.append(cmd)
196
197 def __str__(self):
198 text = ':'.join(str(x) for x in self.commands)
199 if self.line_number:
200 return "%s%s" % (self.line_number, text)
201 else:
202 return text
203
204 def referenced_line_numbers(self):
205 line_numbers = []
206 for command in self.commands:
207 line_numbers.extend(command.referenced_line_numbers())
208 return line_numbers
209
210 def strip_remarks(self):
211 new_commands = []
212 for command in self.commands:
213 if not isinstance(command, Remark):
214 new_commands.append(command)
215 if new_commands:
216 new_line = BasicLine(None)
217 new_line.line_number = self.line_number
218 new_line.commands = new_commands
219 return new_line
220 else:
221 return None
222
223
224 class BasicProgram(object):
225 r"""An object which represents a BASIC program.
226
227 Rudimentary parsing of lines of commands:
228
229 >>> b = BasicProgram('10 PRINT "HELLO"\n'
230 ... '20 GOTO 10\n')
231 >>> b.dump()
232 10 PRINT "HELLO"
233 20 GOTO 10
234 >>> len(b.lines)
235 2
236 >>> print b.lines[0].commands[0]
237 PRINT "HELLO"
238 >>> print b.lines[1].commands[0].__class__.__name__
239 Goto
240
241 Checking for jumps to non-existant line numbers:
242
243 >>> b = BasicProgram()
244 >>> b.add_line('10 PRINT "HELLO"')
245 >>> b.add_line('20 GOTO 30')
246 >>> len(b.lines)
247 2
248 >>> num_errors = b.check_line_numbers()
249 ?UNDEFINED STATEMENT "30" IN: 20 GOTO 30
250 >>> num_errors
251 1
252
253 Checking for GOSUB and ON GOTO, and retaining case in
254 error messages:
255
256 >>> b = BasicProgram('5 goSUb 10\n'
257 ... '7goSUb8\n'
258 ... '10 oN (X+1 )* 5 gOtO 100,6')
259 >>> num_errors = b.check_line_numbers()
260 ?UNDEFINED STATEMENT "100" IN: 10 oN (X+1 )* 5 gOtO 100,6
261 ?UNDEFINED STATEMENT "6" IN: 10 oN (X+1 )* 5 gOtO 100,6
262 ?UNDEFINED STATEMENT "8" IN: 7goSUb8
263
264 Whitespace and case is retained when dumping a program:
265
266 >>> b = BasicProgram('5 goSUb 10\n'
267 ... '7gOSub8\n'
268 ... '9 rem WHAT? ??:>?: >?\n'
269 ... '\n'
270 ... '800 PRINT::print:ZORK 30\n'
271 ... '10 oN ERROR gOtO 100, 6,7, 800 ,3\n'
272 ... ' 99 what \n'
273 ... 'if50then60\n'
274 ... '50ifthisstuffistruegoto70\n'
275 ... '60 If This Stuff Is True Then Print:GoTo 9\n'
276 ... )
277 >>> b.dump()
278 5 goSUb 10
279 7gOSub8
280 9 rem WHAT? ??:>?: >?
281 <BLANKLINE>
282 800 PRINT::print:ZORK 30
283 10 oN ERROR gOtO 100, 6,7, 800 ,3
284 99 what
285 if50then60
286 50ifthisstuffistruegoto70
287 60 If This Stuff Is True Then Print:GoTo 9
288
289 Remarks may contain colons:
290
291 >>> b = BasicProgram('10 REM HELLO: GOTO 20')
292 >>> len(b.lines[0].commands)
293 1
294 >>> print b.lines[0].commands[0].__class__.__name__
295 Remark
296 >>> b.check_line_numbers()
297 0
298
299 Immediate mode commands are checked, and can be stripped:
300
301 >>> b = BasicProgram('10 REM HELLO\n'
302 ... 'PRINT "HELLO"\n'
303 ... 'GOTO 20')
304 >>> num_errors = b.check_line_numbers()
305 ?UNDEFINED STATEMENT "20" IN: GOTO 20 (immediate mode, text file line 3)
306 >>> b.strip_immediate_mode_commands()
307 >>> b.dump()
308 10 REM HELLO
309 >>> b.check_line_numbers()
310 0
311
312 Remarks, both on numbered lines and immediate mode, can be
313 stripped:
314
315 >>> b = BasicProgram('10 PRINT "HI":REM HELLO\n'
316 ... 'PRINT "HELLO"\n'
317 ... 'REM WHAT?\n'
318 ... '20 GOTO 30\n'
319 ... '30 REM THIS IS BUNK, MAN\n')
320 >>> b.strip_remarks()
321 >>> b.dump()
322 10 PRINT "HI"
323 PRINT "HELLO"
324 20 GOTO 30
325 >>> num_errors = b.check_line_numbers()
326 ?UNDEFINED STATEMENT "30" IN: 20 GOTO 30
327
328 """
329 def __init__(self, text=None):
330 self.lines = []
331 if text is not None:
332 for line in text.rstrip('\r\n').split('\n'):
333 self.add_line(line)
334
335 def add_line(self, line):
336 self.lines.append(BasicLine(line))
337
338 def check_line_numbers(self):
339 referenced = {}
340 defined = {}
341 errors = 0
342 text_file_line = 1
343 for line in self.lines:
344 if line.line_number is not None:
345 location = line.line_number.number
346 defined[location] = line
347 else:
348 location = "IMMEDIATE MODE (line %d)" % text_file_line
349 defined[location] = "%s (immediate mode, text file line %d)" % \
350 (line, text_file_line)
351 referenced[location] = line.referenced_line_numbers()
352 text_file_line += 1
353 for (location, referenced_line_numbers) in referenced.iteritems():
354 for referenced_line_number in referenced_line_numbers:
355 if referenced_line_number.number not in defined:
356 print '?UNDEFINED STATEMENT "%s" IN: %s' % (
357 referenced_line_number.number, defined[location])
358 errors += 1
359 return errors
360
361 def strip_immediate_mode_commands(self):
362 new_lines = []
363 for line in self.lines:
364 if line.line_number is not None:
365 new_lines.append(line)
366 self.lines = new_lines
367
368 def strip_remarks(self, program_lines_only=False):
369 new_lines = []
370 for line in self.lines:
371 if program_lines_only and line.line_number is None:
372 new_lines.append(line)
373 continue
374 new_line = line.strip_remarks()
375 if new_line is not None:
376 new_lines.append(new_line)
377 self.lines = new_lines
378
379 def dump(self):
380 for line in self.lines:
381 print line
382
383
384 if __name__ == '__main__':
385 parser = OptionParser()
386 parser.add_option("-I", "--strip-immediate-mode",
387 dest="strip_immediate_mode",
388 action="store_true",
389 help="strip immediate mode commands (implies -o)")
390 parser.add_option("-L", "--no-check-line-numbers",
391 dest="check_line_numbers",
392 default=True,
393 action="store_false",
394 help="do not check that all target line numbers exist")
395 parser.add_option("-o", "--dump-output",
396 dest="dump_output",
397 action="store_true",
398 help="dump (possibly transformed) program to standard "
399 "output; implied by other options")
400 parser.add_option("-p", "--program-lines-only",
401 dest="program_lines_only",
402 action="store_true",
403 help="have transformations only affect program lines, "
404 "not immediate mode lines")
405 parser.add_option("-R", "--strip-remarks",
406 dest="strip_remarks",
407 action="store_true",
408 help="strip all REM statements from program (implies -o)")
409 parser.add_option("-t", "--test",
410 action="store_true", dest="test", default=False,
411 help="run internal test cases and exit")
412
413 (options, args) = parser.parse_args()
414
415 if options.test:
416 import doctest
417 doctest.testmod()
418 sys.exit(0)
419
420 p = BasicProgram()
421 for line in fileinput.input(args):
422 p.add_line(line)
423
424 if options.strip_immediate_mode:
425 options.dump_output = True
426 p.strip_immediate_mode_commands()
427
428 if options.strip_remarks:
429 options.dump_output = True
430 p.strip_remarks(program_lines_only=options.program_lines_only)
431
432 errors = 0
433 if options.check_line_numbers:
434 errors += p.check_line_numbers()
435
436 if errors > 0:
437 sys.exit(1)
438
439 if options.dump_output:
440 p.dump()
441
442 sys.exit(0)