Commit 26058b5b authored by Kirill Smelkov's avatar Kirill Smelkov

gpython: Factor-out options parsing into getopt-style _IGetOpt helper

Python allows multiple single-letter options and their arguments to be
coming on single argument, for example:

	python -OQc'print 1'
	python -OQc 'print 1'
	python -OQ -c 'print 1'
	etc...

We are currently trying to handle that at every option, but even though
it kind of works, it is limited and will break once we will start adding
options.

-> Refactor options parsing into getopt-style helper. We cannot use
getopt itself because it will complain e.g. on `gpython file.py
--my-custom-opt` that my-custom-opt is unexpected.
parent 167912d3
...@@ -41,6 +41,9 @@ $GPYTHON_RUNTIME=threads. ...@@ -41,6 +41,9 @@ $GPYTHON_RUNTIME=threads.
from __future__ import print_function, absolute_import from __future__ import print_function, absolute_import
_pyopt = "c:m:VW:X:"
_pyopt_long = ('version',)
# pymain mimics `python ...` # pymain mimics `python ...`
# #
# argv is what comes via `...` without first [0] for python. # argv is what comes via `...` without first [0] for python.
...@@ -53,20 +56,17 @@ def pymain(argv, init=None): ...@@ -53,20 +56,17 @@ def pymain(argv, init=None):
version = False # set if `-V` version = False # set if `-V`
warnoptions = [] # collected `-W arg` warnoptions = [] # collected `-W arg`
while len(argv) > 0: igetopt = _IGetOpt(argv, _pyopt, _pyopt_long)
for (opt, arg) in igetopt:
# -V / --version # -V / --version
if argv[0] in ('-V', '--version'): if opt in ('-V', '--version'):
version = True version = True
break break
# -c command # -c command
elif argv[0].startswith('-c'): elif opt == '-c':
cmd = argv[0][2:] # -c<command> also works cmd = arg
argv = argv[1:] sys.argv = ['-c'] + igetopt.argv # python leaves '-c' as argv[0]
if cmd == '':
cmd = argv[0]
argv = argv[1:]
sys.argv = ['-c'] + argv # python leaves '-c' as argv[0]
sys.path.insert(0, '') # cwd sys.path.insert(0, '') # cwd
def run(): def run():
import six import six
...@@ -78,13 +78,9 @@ def pymain(argv, init=None): ...@@ -78,13 +78,9 @@ def pymain(argv, init=None):
break break
# -m module # -m module
elif argv[0].startswith('-m'): elif opt == '-m':
mod = argv[0][2:] # -m<module> also works mod = arg
argv = argv[1:] sys.argv = [mod] + igetopt.argv
if mod == '':
mod = argv[0]
argv = argv[1:]
sys.argv = [mod] + argv
# sys.path <- cwd # sys.path <- cwd
# NOTE python2 injects '', while python3 injects realpath('') # NOTE python2 injects '', while python3 injects realpath('')
# we stick to python3 behaviour, as it is more sane because e.g. # we stick to python3 behaviour, as it is more sane because e.g.
...@@ -98,20 +94,17 @@ def pymain(argv, init=None): ...@@ -98,20 +94,17 @@ def pymain(argv, init=None):
break break
# -W arg (warning control) # -W arg (warning control)
elif argv[0].startswith('-W'): elif opt == '-W':
wopt = argv[0][2:] # -W<arg> also works warnoptions.append(arg)
argv = argv[1:]
if wopt == '': else:
wopt = argv[0] print("unknown option: '%s'" % opt, file=sys.stderr)
argv = argv[1:]
warnoptions.append(wopt)
elif argv[0].startswith('-'):
print("unknown option: '%s'" % argv[0], file=sys.stderr)
sys.exit(2) sys.exit(2)
argv = igetopt.argv
if run is None:
# file # file
else: if len(argv) > 0:
sys.argv = argv sys.argv = argv
filepath = argv[0] filepath = argv[0]
...@@ -124,33 +117,32 @@ def pymain(argv, init=None): ...@@ -124,33 +117,32 @@ def pymain(argv, init=None):
'__doc__': None, '__doc__': None,
'__package__': None} '__package__': None}
_execfile(filepath, g) _execfile(filepath, g)
break
# interactive console # interactive console
if run is None: else:
sys.argv = [''] sys.argv = ['']
sys.path.insert(0, '') # cwd sys.path.insert(0, '') # cwd
def run(): def run():
import code import code
from six.moves import input as raw_input from six.moves import input as raw_input
# like code.interact() but with overridden console.raw_input _and_ # like code.interact() but with overridden console.raw_input _and_
# readline imported (code.interact mutually excludes those two). # readline imported (code.interact mutually excludes those two).
try: try:
import readline # enable interactive editing import readline # enable interactive editing
except ImportError: except ImportError:
pass pass
console = code.InteractiveConsole() console = code.InteractiveConsole()
def _(prompt): def _(prompt):
# python behaviour: don't print '>>>' if stdin is not a tty # python behaviour: don't print '>>>' if stdin is not a tty
# (builtin raw_input always prints prompt) # (builtin raw_input always prints prompt)
if not sys.stdin.isatty(): if not sys.stdin.isatty():
prompt='' prompt=''
return raw_input(prompt) return raw_input(prompt)
console.raw_input = _ console.raw_input = _
console.interact() console.interact()
# ---- options processed -> start the interpreter ---- # ---- options processed -> start the interpreter ----
...@@ -299,35 +291,33 @@ def main(): ...@@ -299,35 +291,33 @@ def main():
raise RuntimeError('gpython: internal error: sys.path does not contain dirname(gpython):' raise RuntimeError('gpython: internal error: sys.path does not contain dirname(gpython):'
'\n\n\tgpython:\t%s\n\tsys.path:\t%s' % (exe, sys.path)) '\n\n\tgpython:\t%s\n\tsys.path:\t%s' % (exe, sys.path))
# extract and process -X # extract and process `-X gpython.*`
# -X gpython.runtime=(gevent|threads) + $GPYTHON_RUNTIME # -X gpython.runtime=(gevent|threads) + $GPYTHON_RUNTIME
sys._xoptions = getattr(sys, '_xoptions', {}) sys._xoptions = getattr(sys, '_xoptions', {})
argv_ = [] argv_ = []
gpy_runtime = os.getenv('GPYTHON_RUNTIME', 'gevent') gpy_runtime = os.getenv('GPYTHON_RUNTIME', 'gevent')
while len(argv) > 0: igetopt = _IGetOpt(argv, _pyopt, _pyopt_long)
arg = argv[0] for (opt, arg) in igetopt:
argv = argv[1:] if opt == '-X':
if arg.startswith('gpython.'):
if arg.startswith('gpython.runtime='):
gpy_runtime = arg[len('gpython.runtime='):]
sys._xoptions['gpython.runtime'] = gpy_runtime
if not arg.startswith('-X'): else:
argv_.append(arg) raise RuntimeError('gpython: unknown -X option %s' % opt)
# continue looking for -X only until options end
if not arg.startswith('-'):
break
continue
# -X <opt> continue
opt = arg[2:] # -X<opt>
if opt == '':
opt = argv[0] # -X <opt>
argv = argv[1:]
if opt.startswith('gpython.runtime='): argv_.append(opt)
gpy_runtime = opt[len('gpython.runtime='):] if arg is not None:
sys._xoptions['gpython.runtime'] = gpy_runtime argv_.append(arg)
else: # options after -c / -m are not for python itself
raise RuntimeError('gpython: unknown -X option %s' % opt) if opt in ('-c', '-m'):
argv = argv_ + argv break
argv = argv_ + igetopt.argv
# init initializes according to selected runtime # init initializes according to selected runtime
# it is called after options are parsed and sys.path is setup correspondingly. # it is called after options are parsed and sys.path is setup correspondingly.
...@@ -382,5 +372,106 @@ def _is_buildout_script(path): ...@@ -382,5 +372,106 @@ def _is_buildout_script(path):
return ('\nsys.path[0:0] = [\n' in src) return ('\nsys.path[0:0] = [\n' in src)
# _IGetOpt provides getopt-style incremental options parsing.
# ( we cannot use getopt directly, because it complains about "unrecognized options"
# on e.g. `gpython file.py -opt` )
class _IGetOpt:
def __init__(self, argv, shortopts, longopts):
self.argv = argv
self._opts = {} # opt -> bool(arg-required)
self._shortopttail = '' # current tail of short options from e.g. -abcd
# parse shortopts -> ._opts
opt = None
for _ in shortopts:
if _ == ':':
if opt is None:
raise RuntimeError("invalid shortopts: unexpected ':'")
self._opts['-'+opt] = True
opt = None # prevent ::
else:
opt = _
if opt in self._opts:
raise RuntimeError("invalid shortopts: double '%s'" % opt)
self._opts['-'+opt] = False
# parse longopts -> ._opts
for opt in longopts:
arg_required = (opt[-1:] == '=')
if arg_required:
opt = opt[:-1]
self._opts['--'+opt] = arg_required
def __iter__(self):
return self
def __next__(self):
# yield e.g. -b -c -d from -abcd
if len(self._shortopttail) > 0:
opt = '-'+self._shortopttail[0]
self._shortopttail = self._shortopttail[1:]
if opt not in self._opts:
raise RuntimeError('unexpected option %s' % opt)
arg = None
if self._opts[opt]: # arg required
if len(self._shortopttail) > 0:
# -o<arg>
arg = self._shortopttail
self._shortopttail = ''
else:
# -o <arg>
if len(self.argv) == 0:
raise RuntimeError('option %s requires an argument' % opt)
arg = self.argv[0]
self.argv = self.argv[1:]
return (opt, arg)
# ._shortopttail is empty - proceed with .argv
if len(self.argv) == 0:
raise StopIteration # end of argv
opt = self.argv[0]
if not opt.startswith('-'):
raise StopIteration # not an option
if opt == '-':
raise StopIteration # not an option
self.argv = self.argv[1:]
if opt == '--':
raise StopIteration # options -- args delimiter
# short option
if not opt.startswith('--'):
self._shortopttail = opt[1:]
return self.__next__()
# long option
arg = None
if '=' in opt:
opt, arg = opt.split('=')
if opt not in self._opts:
raise RuntimeError('unexpected option %s' % opt)
arg_required = self._opts[opt]
if not arg_required:
if arg is not None:
raise RuntimeError('option %s requires no argument' % opt)
else:
if arg is None:
if len(self.argv) == 0:
raise RuntimeError('option %s requires no argument' % opt)
arg = self.argv[0]
self.argv[0] = self.argv[1:]
return (opt, arg)
next = __next__ # for py2
if __name__ == '__main__': if __name__ == '__main__':
main() main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment