1 | #!/usr/bin/env python
|
2 | # Copyright 2016 Andy Chu. All rights reserved.
|
3 | # Licensed under the Apache License, Version 2.0 (the "License");
|
4 | # you may not use this file except in compliance with the License.
|
5 | # You may obtain a copy of the License at
|
6 | #
|
7 | # http://www.apache.org/licenses/LICENSE-2.0
|
8 | from __future__ import print_function
|
9 | """
|
10 | oil.py - A busybox-like binary for oil.
|
11 |
|
12 | Based on argv[0], it acts like a few different programs.
|
13 |
|
14 | Builtins that can be exposed:
|
15 |
|
16 | - test / [ -- call BoolParser at runtime
|
17 | - 'time' -- because it has format strings, etc.
|
18 | - find/xargs equivalents (even if they are not compatible)
|
19 | - list/each/every
|
20 |
|
21 | - echo: most likely don't care about this
|
22 | """
|
23 |
|
24 | import os
|
25 | import sys
|
26 | import time # for perf measurement
|
27 |
|
28 | # TODO: Set PYTHONPATH from outside?
|
29 | this_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
30 | sys.path.append(os.path.join(this_dir, '..'))
|
31 |
|
32 | _trace_path = os.environ.get('_PY_TRACE')
|
33 | if _trace_path:
|
34 | from benchmarks import pytrace
|
35 | _tracer = pytrace.Tracer()
|
36 | _tracer.Start()
|
37 | else:
|
38 | _tracer = None
|
39 |
|
40 | # Uncomment this to see startup time problems.
|
41 | if os.environ.get('OIL_TIMING'):
|
42 | start_time = time.time()
|
43 | def _tlog(msg):
|
44 | pid = os.getpid() # TODO: Maybe remove PID later.
|
45 | print('[%d] %.3f %s' % (pid, (time.time() - start_time) * 1000, msg))
|
46 | else:
|
47 | def _tlog(msg):
|
48 | pass
|
49 |
|
50 | _tlog('before imports')
|
51 |
|
52 | import errno
|
53 | #import traceback # for debugging
|
54 |
|
55 | # Set in Modules/main.c.
|
56 | HAVE_READLINE = os.getenv('_HAVE_READLINE') != ''
|
57 |
|
58 | from asdl import format as fmt
|
59 | from asdl import encode
|
60 |
|
61 | from osh import word_parse # for tracing
|
62 | from osh import cmd_parse # for tracing
|
63 |
|
64 | from osh import ast_lib
|
65 | from osh import parse_lib
|
66 |
|
67 | from core import alloc
|
68 | from core import args
|
69 | from core import builtin
|
70 | from core import cmd_exec
|
71 | from osh.meta import Id
|
72 | from core import legacy
|
73 | from core import lexer # for tracing
|
74 | from core import process
|
75 | from core import reader
|
76 | from core import state
|
77 | from core import word
|
78 | from core import word_eval
|
79 | from core import ui
|
80 | from core import util
|
81 |
|
82 | if HAVE_READLINE:
|
83 | from core import completion
|
84 | else:
|
85 | completion = None
|
86 |
|
87 | from tools import deps
|
88 | from tools import osh2oil
|
89 |
|
90 | log = util.log
|
91 |
|
92 | _tlog('after imports')
|
93 |
|
94 |
|
95 | def InteractiveLoop(opts, ex, c_parser, w_parser, line_reader):
|
96 | if opts.show_ast:
|
97 | ast_f = fmt.DetectConsoleOutput(sys.stdout)
|
98 | else:
|
99 | ast_f = None
|
100 |
|
101 | status = 0
|
102 | while True:
|
103 | try:
|
104 | w = c_parser.Peek()
|
105 | except KeyboardInterrupt:
|
106 | print('Ctrl-C')
|
107 | break
|
108 |
|
109 | if w is None:
|
110 | raise RuntimeError('Failed parse: %s' % c_parser.Error())
|
111 | c_id = word.CommandId(w)
|
112 | if c_id == Id.Op_Newline:
|
113 | print('nothing to execute')
|
114 | elif c_id == Id.Eof_Real:
|
115 | print('EOF')
|
116 | break
|
117 | else:
|
118 | node = c_parser.ParseCommandLine()
|
119 |
|
120 | # TODO: Need an error for an empty command, which we ignore? GetLine
|
121 | # could do that in the first position?
|
122 | # ParseSimpleCommand fails with '\n' token?
|
123 | if not node:
|
124 | # TODO: PrintError here
|
125 | raise RuntimeError('failed parse: %s' % c_parser.Error())
|
126 |
|
127 | if ast_f:
|
128 | ast_lib.PrettyPrint(node)
|
129 |
|
130 | status, is_control_flow = ex.ExecuteAndCatch(node)
|
131 | if is_control_flow: # exit or return
|
132 | break
|
133 |
|
134 | if opts.print_status:
|
135 | print('STATUS', repr(status))
|
136 |
|
137 | # Reset prompt to PS1.
|
138 | line_reader.Reset()
|
139 |
|
140 | # Reset internal newline state.
|
141 | # NOTE: It would actually be correct to reinitialize all objects (except
|
142 | # Env) on every iteration. But we know that the w_parser is the only thing
|
143 | # that needs to be reset, for now.
|
144 | w_parser.Reset()
|
145 | c_parser.Reset()
|
146 |
|
147 | return status
|
148 |
|
149 |
|
150 | # bash --noprofile --norc uses 'bash-4.3$ '
|
151 | OSH_PS1 = 'osh$ '
|
152 |
|
153 |
|
154 | def _ShowVersion():
|
155 | util.ShowAppVersion('Oil')
|
156 |
|
157 |
|
158 | def OshMain(argv0, argv, login_shell):
|
159 | spec = args.FlagsAndOptions()
|
160 | spec.ShortFlag('-c', args.Str, quit_parsing_flags=True) # command string
|
161 | spec.ShortFlag('-i') # interactive
|
162 |
|
163 | # TODO: -h too
|
164 | spec.LongFlag('--help')
|
165 | spec.LongFlag('--version')
|
166 | spec.LongFlag('--ast-format',
|
167 | ['text', 'abbrev-text', 'html', 'abbrev-html', 'oheap', 'none'],
|
168 | default='abbrev-text')
|
169 | spec.LongFlag('--show-ast') # execute and show
|
170 | spec.LongFlag('--fix')
|
171 | spec.LongFlag('--debug-spans') # For oshc translate
|
172 | spec.LongFlag('--print-status')
|
173 | spec.LongFlag('--trace', ['cmd-parse', 'word-parse', 'lexer']) # NOTE: can only trace one now
|
174 | spec.LongFlag('--hijack-shebang')
|
175 |
|
176 | # For benchmarks/*.sh
|
177 | spec.LongFlag('--parser-mem-dump', args.Str)
|
178 | spec.LongFlag('--runtime-mem-dump', args.Str)
|
179 |
|
180 | builtin.AddOptionsToArgSpec(spec)
|
181 |
|
182 | try:
|
183 | opts, opt_index = spec.Parse(argv)
|
184 | except args.UsageError as e:
|
185 | util.usage(str(e))
|
186 | return 2
|
187 |
|
188 | if opts.help:
|
189 | loader = util.GetResourceLoader()
|
190 | builtin.Help(['osh-usage'], loader)
|
191 | return 0
|
192 | if opts.version:
|
193 | # OSH version is the only binary in Oil right now, so it's all one version.
|
194 | _ShowVersion()
|
195 | return 0
|
196 |
|
197 | trace_state = util.TraceState()
|
198 | if 'cmd-parse' == opts.trace:
|
199 | util.WrapMethods(cmd_parse.CommandParser, trace_state)
|
200 | if 'word-parse' == opts.trace:
|
201 | util.WrapMethods(word_parse.WordParser, trace_state)
|
202 | if 'lexer' == opts.trace:
|
203 | util.WrapMethods(lexer.Lexer, trace_state)
|
204 |
|
205 | if opt_index == len(argv):
|
206 | dollar0 = argv0
|
207 | else:
|
208 | dollar0 = argv[opt_index] # the script name, or the arg after -c
|
209 |
|
210 | # TODO: Create a --parse action or 'osh parse' or 'oil osh-parse'
|
211 | # osh-fix
|
212 | # It uses a different memory-management model. It's a batch program and not
|
213 | # an interactive program.
|
214 |
|
215 | pool = alloc.Pool()
|
216 | arena = pool.NewArena()
|
217 |
|
218 | # TODO: Maybe wrap this initialization sequence up in an oil_State, like
|
219 | # lua_State.
|
220 | status_lines = ui.MakeStatusLines()
|
221 | mem = state.Mem(dollar0, argv[opt_index + 1:], os.environ, arena)
|
222 | funcs = {}
|
223 |
|
224 | # Passed to Executor for 'complete', and passed to completion.Init
|
225 | if completion:
|
226 | comp_lookup = completion.CompletionLookup()
|
227 | else:
|
228 | # TODO: NullLookup?
|
229 | comp_lookup = None
|
230 |
|
231 | exec_opts = state.ExecOpts(mem)
|
232 | builtin.SetExecOpts(exec_opts, opts.opt_changes)
|
233 |
|
234 | fd_state = process.FdState()
|
235 | ex = cmd_exec.Executor(mem, fd_state, status_lines, funcs, completion,
|
236 | comp_lookup, exec_opts, arena)
|
237 |
|
238 | # NOTE: The rc file can contain both commands and functions... ideally we
|
239 | # would only want to save nodes/lines for the functions.
|
240 | try:
|
241 | rc_path = 'oilrc'
|
242 | arena.PushSource(rc_path)
|
243 | with open(rc_path) as f:
|
244 | rc_line_reader = reader.FileLineReader(f, arena)
|
245 | _, rc_c_parser = parse_lib.MakeParser(rc_line_reader, arena)
|
246 | try:
|
247 | rc_node = rc_c_parser.ParseWholeFile()
|
248 | if not rc_node:
|
249 | err = rc_c_parser.Error()
|
250 | ui.PrintErrorStack(err, arena, sys.stderr)
|
251 | return 2 # parse error is code 2
|
252 | finally:
|
253 | arena.PopSource()
|
254 |
|
255 | status = ex.Execute(rc_node)
|
256 | #print('oilrc:', status, cflow, file=sys.stderr)
|
257 | # Ignore bad status?
|
258 | except IOError as e:
|
259 | if e.errno != errno.ENOENT:
|
260 | raise
|
261 |
|
262 | if opts.c is not None:
|
263 | arena.PushSource('<command string>')
|
264 | line_reader = reader.StringLineReader(opts.c, arena)
|
265 | interactive = False
|
266 | elif opts.i: # force interactive
|
267 | arena.PushSource('<stdin -i>')
|
268 | line_reader = reader.InteractiveLineReader(OSH_PS1, arena)
|
269 | interactive = True
|
270 | else:
|
271 | try:
|
272 | script_name = argv[opt_index]
|
273 | except IndexError:
|
274 | if sys.stdin.isatty():
|
275 | arena.PushSource('<interactive>')
|
276 | line_reader = reader.InteractiveLineReader(OSH_PS1, arena)
|
277 | interactive = True
|
278 | else:
|
279 | arena.PushSource('<stdin>')
|
280 | line_reader = reader.FileLineReader(sys.stdin, arena)
|
281 | interactive = False
|
282 | else:
|
283 | arena.PushSource(script_name)
|
284 | try:
|
285 | f = fd_state.Open(script_name)
|
286 | except OSError as e:
|
287 | util.error("Couldn't open %r: %s", script_name, os.strerror(e.errno))
|
288 | return 1
|
289 | line_reader = reader.FileLineReader(f, arena)
|
290 | interactive = False
|
291 |
|
292 | # TODO: assert arena.NumSourcePaths() == 1
|
293 | # TODO: .rc file needs its own arena.
|
294 | w_parser, c_parser = parse_lib.MakeParser(line_reader, arena)
|
295 |
|
296 | if interactive:
|
297 | # NOTE: We're using a different evaluator here. The completion system can
|
298 | # also run functions... it gets the Executor through Executor._Complete.
|
299 | if HAVE_READLINE:
|
300 | splitter = legacy.SplitContext(mem)
|
301 | ev = word_eval.CompletionWordEvaluator(mem, exec_opts, splitter)
|
302 | status_out = completion.StatusOutput(status_lines, exec_opts)
|
303 | completion.Init(pool, builtin.BUILTIN_DEF, mem, funcs, comp_lookup,
|
304 | status_out, ev)
|
305 |
|
306 | return InteractiveLoop(opts, ex, c_parser, w_parser, line_reader)
|
307 | else:
|
308 | # Parse the whole thing up front
|
309 | #print('Parsing file')
|
310 |
|
311 | _tlog('ParseWholeFile')
|
312 | # TODO: Do I need ParseAndEvalLoop? How is it different than
|
313 | # InteractiveLoop?
|
314 | try:
|
315 | node = c_parser.ParseWholeFile()
|
316 | except util.ParseError as e:
|
317 | ui.PrettyPrintError(e, arena, sys.stderr)
|
318 | print('parse error: %s' % e.UserErrorString(), file=sys.stderr)
|
319 | return 2
|
320 | else:
|
321 | # TODO: Remove this older form of error handling.
|
322 | if not node:
|
323 | err = c_parser.Error()
|
324 | assert err, err # can't be empty
|
325 | ui.PrintErrorStack(err, arena, sys.stderr)
|
326 | return 2 # parse error is code 2
|
327 |
|
328 | do_exec = True
|
329 | if opts.fix:
|
330 | #log('SPANS: %s', arena.spans)
|
331 | osh2oil.PrintAsOil(arena, node, opts.debug_spans)
|
332 | do_exec = False
|
333 | if exec_opts.noexec:
|
334 | do_exec = False
|
335 |
|
336 | # Do this after parsing the entire file. There could be another option to
|
337 | # do it before exiting runtime?
|
338 | if opts.parser_mem_dump:
|
339 | # This might be superstition, but we want to let the value stabilize
|
340 | # after parsing. bash -c 'cat /proc/$$/status' gives different results
|
341 | # with a sleep.
|
342 | time.sleep(0.001)
|
343 | input_path = '/proc/%d/status' % os.getpid()
|
344 | with open(input_path) as f, open(opts.parser_mem_dump, 'w') as f2:
|
345 | contents = f.read()
|
346 | f2.write(contents)
|
347 | log('Wrote %s to %s (--parser-mem-dump)', input_path,
|
348 | opts.parser_mem_dump)
|
349 |
|
350 | # -n prints AST, --show-ast prints and executes
|
351 | if exec_opts.noexec or opts.show_ast:
|
352 | if opts.ast_format == 'none':
|
353 | print('AST not printed.', file=sys.stderr)
|
354 | elif opts.ast_format == 'oheap':
|
355 | # TODO: Make this a separate flag?
|
356 | if sys.stdout.isatty():
|
357 | raise RuntimeError('ERROR: Not dumping binary data to a TTY.')
|
358 | f = sys.stdout
|
359 |
|
360 | enc = encode.Params()
|
361 | out = encode.BinOutput(f)
|
362 | encode.EncodeRoot(node, enc, out)
|
363 |
|
364 | else: # text output
|
365 | f = sys.stdout
|
366 |
|
367 | if opts.ast_format in ('text', 'abbrev-text'):
|
368 | ast_f = fmt.DetectConsoleOutput(f)
|
369 | elif opts.ast_format in ('html', 'abbrev-html'):
|
370 | ast_f = fmt.HtmlOutput(f)
|
371 | else:
|
372 | raise AssertionError
|
373 | abbrev_hook = (
|
374 | ast_lib.AbbreviateNodes if 'abbrev-' in opts.ast_format else None)
|
375 | tree = fmt.MakeTree(node, abbrev_hook=abbrev_hook)
|
376 | ast_f.FileHeader()
|
377 | fmt.PrintTree(tree, ast_f)
|
378 | ast_f.FileFooter()
|
379 | ast_f.write('\n')
|
380 |
|
381 | #util.log("Execution skipped because 'noexec' is on ")
|
382 | status = 0
|
383 |
|
384 | if do_exec:
|
385 | _tlog('Execute(node)')
|
386 | status = ex.ExecuteAndRunExitTrap(node)
|
387 | # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub
|
388 | # don't because they call sys.exit().
|
389 | if opts.runtime_mem_dump:
|
390 | # This might be superstition, but we want to let the value stabilize
|
391 | # after parsing. bash -c 'cat /proc/$$/status' gives different results
|
392 | # with a sleep.
|
393 | time.sleep(0.001)
|
394 | input_path = '/proc/%d/status' % os.getpid()
|
395 | with open(input_path) as f, open(opts.runtime_mem_dump, 'w') as f2:
|
396 | contents = f.read()
|
397 | f2.write(contents)
|
398 | log('Wrote %s to %s (--runtime-mem-dump)', input_path,
|
399 | opts.runtime_mem_dump)
|
400 |
|
401 | else:
|
402 | status = 0
|
403 |
|
404 | return status
|
405 |
|
406 |
|
407 | def OilMain(argv):
|
408 | spec = args.FlagsAndOptions()
|
409 | # TODO: -h too
|
410 | spec.LongFlag('--help')
|
411 | spec.LongFlag('--version')
|
412 | #builtin.AddOptionsToArgSpec(spec)
|
413 |
|
414 | try:
|
415 | opts, opt_index = spec.Parse(argv)
|
416 | except args.UsageError as e:
|
417 | util.usage(str(e))
|
418 | return 2
|
419 |
|
420 | if opts.help:
|
421 | loader = util.GetResourceLoader()
|
422 | builtin.Help(['oil-usage'], loader)
|
423 | return 0
|
424 | if opts.version:
|
425 | # OSH version is the only binary in Oil right now, so it's all one version.
|
426 | _ShowVersion()
|
427 | return 0
|
428 |
|
429 | raise NotImplementedError('oil')
|
430 | return 0
|
431 |
|
432 |
|
433 | def WokMain(main_argv):
|
434 | raise NotImplementedError('wok')
|
435 |
|
436 |
|
437 | def BoilMain(main_argv):
|
438 | raise NotImplementedError('boil')
|
439 |
|
440 |
|
441 | # TODO: Hook up to completion.
|
442 | SUBCOMMANDS = ['translate', 'format', 'deps', 'undefined-vars']
|
443 |
|
444 | def OshCommandMain(argv):
|
445 | """Run an 'oshc' tool.
|
446 |
|
447 | 'osh' is short for "osh compiler" or "osh command".
|
448 |
|
449 | TODO:
|
450 | - oshc --help
|
451 |
|
452 | oshc deps
|
453 | --path: the $PATH to use to find executables. What about libraries?
|
454 |
|
455 | NOTE: we're leaving out su -c, find, xargs, etc.? Those should generally
|
456 | run functions using the $0 pattern.
|
457 | --chained-command sudo
|
458 | """
|
459 | try:
|
460 | action = argv[0]
|
461 | except IndexError:
|
462 | raise args.UsageError('oshc: Missing required subcommand.')
|
463 |
|
464 | if action not in SUBCOMMANDS:
|
465 | raise args.UsageError('oshc: Invalid subcommand %r.' % action)
|
466 |
|
467 | try:
|
468 | script_name = argv[1]
|
469 | except IndexError:
|
470 | script_name = '<stdin>'
|
471 | f = sys.stdin
|
472 | else:
|
473 | try:
|
474 | f = open(script_name)
|
475 | except IOError as e:
|
476 | util.error("Couldn't open %r: %s", script_name, os.strerror(e.errno))
|
477 | return 2
|
478 |
|
479 | pool = alloc.Pool()
|
480 | arena = pool.NewArena()
|
481 | arena.PushSource(script_name)
|
482 |
|
483 | line_reader = reader.FileLineReader(f, arena)
|
484 | _, c_parser = parse_lib.MakeParser(line_reader, arena)
|
485 |
|
486 | try:
|
487 | node = c_parser.ParseWholeFile()
|
488 | except util.ParseError as e:
|
489 | ui.PrettyPrintError(e, arena, sys.stderr)
|
490 | print('parse error: %s' % e.UserErrorString(), file=sys.stderr)
|
491 | return 2
|
492 | else:
|
493 | # TODO: Remove this older form of error handling.
|
494 | if not node:
|
495 | err = c_parser.Error()
|
496 | assert err, err # can't be empty
|
497 | ui.PrintErrorStack(err, arena, sys.stderr)
|
498 | return 2 # parse error is code 2
|
499 |
|
500 | f.close()
|
501 |
|
502 | # Columns for list-*
|
503 | # path line name
|
504 | # where name is the binary path, variable name, or library path.
|
505 |
|
506 | # bin-deps and lib-deps can be used to make an app bundle.
|
507 | # Maybe I should list them together? 'deps' can show 4 columns?
|
508 | #
|
509 | # path, line, type, name
|
510 | #
|
511 | # --pretty can show the LST location.
|
512 |
|
513 | # stderr: show how we're following imports?
|
514 |
|
515 | if action == 'translate':
|
516 | # TODO: FIx this invocation up.
|
517 | #debug_spans = opt.debug_spans
|
518 | debug_spans = False
|
519 | osh2oil.PrintAsOil(arena, node, debug_spans)
|
520 |
|
521 | elif action == 'format':
|
522 | # TODO: autoformat code
|
523 | raise NotImplementedError(action)
|
524 |
|
525 | elif action == 'deps':
|
526 | deps.Deps(node)
|
527 |
|
528 | elif action == 'undefined-vars': # could be environment variables
|
529 | pass
|
530 |
|
531 | else:
|
532 | raise AssertionError # Checked above
|
533 |
|
534 | return 0
|
535 |
|
536 |
|
537 | # The valid applets right now.
|
538 | # TODO: Hook up to completion.
|
539 | APPLETS = ['osh', 'oshc']
|
540 |
|
541 |
|
542 | def AppBundleMain(argv):
|
543 | login_shell = False
|
544 |
|
545 | b = os.path.basename(argv[0])
|
546 | main_name, ext = os.path.splitext(b)
|
547 | if main_name.startswith('-'):
|
548 | login_shell = True
|
549 | main_name = main_name[1:]
|
550 |
|
551 | if main_name == 'oil' and ext: # oil.py or oil.ovm
|
552 | try:
|
553 | first_arg = argv[1]
|
554 | except IndexError:
|
555 | raise args.UsageError('Missing required applet name.')
|
556 |
|
557 | if first_arg in ('-h', '--help'):
|
558 | builtin.Help(['bundle-usage'], util.GetResourceLoader())
|
559 | sys.exit(0)
|
560 |
|
561 | if first_arg in ('-V', '--version'):
|
562 | _ShowVersion()
|
563 | sys.exit(0)
|
564 |
|
565 | main_name = first_arg
|
566 | if main_name.startswith('-'): # TODO: Remove duplication above
|
567 | login_shell = True
|
568 | main_name = main_name[1:]
|
569 | argv0 = argv[1]
|
570 | main_argv = argv[2:]
|
571 | else:
|
572 | argv0 = argv[0]
|
573 | main_argv = argv[1:]
|
574 |
|
575 | if main_name in ('osh', 'sh'):
|
576 | status = OshMain(argv0, main_argv, login_shell)
|
577 | _tlog('done osh main')
|
578 | return status
|
579 | elif main_name == 'oshc':
|
580 | return OshCommandMain(main_argv)
|
581 |
|
582 | elif main_name == 'oil':
|
583 | return OilMain(main_argv)
|
584 | elif main_name == 'wok':
|
585 | return WokMain(main_argv)
|
586 | elif main_name == 'boil':
|
587 | return BoilMain(main_argv)
|
588 |
|
589 | # For testing latency
|
590 | elif main_name == 'true':
|
591 | return 0
|
592 | elif main_name == 'false':
|
593 | return 1
|
594 | else:
|
595 | raise args.UsageError('Invalid applet name %r.' % main_name)
|
596 |
|
597 |
|
598 | def main(argv):
|
599 | try:
|
600 | sys.exit(AppBundleMain(argv))
|
601 | except NotImplementedError as e:
|
602 | raise
|
603 | except args.UsageError as e:
|
604 | #builtin.Help(['oil-usage'], util.GetResourceLoader())
|
605 | log('oil: %s', e)
|
606 | sys.exit(2)
|
607 | except RuntimeError as e:
|
608 | log('FATAL: %s', e)
|
609 | sys.exit(1)
|
610 | finally:
|
611 | _tlog('Exiting main()')
|
612 | if _trace_path:
|
613 | _tracer.Stop(_trace_path)
|
614 |
|
615 |
|
616 | if __name__ == '__main__':
|
617 | # NOTE: This could end up as opy.InferTypes(), opy.GenerateCode(), etc.
|
618 | if os.getenv('CALLGRAPH') == '1':
|
619 | from opy import callgraph
|
620 | callgraph.Walk(main, sys.modules)
|
621 | else:
|
622 | main(sys.argv)
|
623 |
|