1#
2# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License as
6# published by the Free Software Foundation; either version 2 of
7# the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14
15import sys
16import os
17import re
18
19
20class Params:
21	def __init__(self):
22		self.excstk = False
23
24
25class Options:
26	def __init__(self, need_args, help_text, version_text):
27		for name in need_args:
28			if not re.fullmatch('(-[^-]|--[^=]+)', name):
29				raise Exception('invalid option name "%s"' % name)
30
31		self.need_args = need_args
32		self.help_text = help_text
33		self.version_text = version_text
34
35
36	def posixly_correct(self):  # pylint: disable=no-self-use
37		return 'POSIXLY_CORRECT' in os.environ
38
39
40	def needs_arg(self, name):
41		return name in self.need_args
42
43
44	def fallback(self, name, params):
45		if name == '--excstk':
46			params.excstk = True
47		elif name == '--help' and self.help_text is not None:
48			sys.stdout.write(self.help_text)
49			sys.exit(0)
50		elif name == '--version' and self.version_text is not None:
51			sys.stdout.write(self.version_text)
52			sys.exit(0)
53		else:
54			suffix = ' (taking an argument?)' if self.needs_arg(name) else ''
55			suffix += ', try --help' if self.help_text is not None else ''
56			raise Exception('unknown option "%s"%s' % (name, suffix))
57
58
59	def reader(self, args, skip_zero=True):
60		return Options.Reader(self, args, skip_zero)
61
62
63	class Reader:
64		def __init__(self, options, args, skip_zero):
65			self.options = options
66			self.args = args
67			self.skip_zero = skip_zero
68
69
70		def __iter__(self):
71			return Options.Reader.Iterator(self)
72
73
74		class Iterator:
75			def __init__(self, reader):
76				self.options = reader.options
77				self.args = reader.args
78				self.optind = int(reader.skip_zero)
79				self.chrind = 1
80				self.endopt = False
81
82
83			def __next__(self):
84				if self.chrind == 0:
85					self.optind += 1
86					self.chrind = 1
87
88				if self.optind == len(self.args):
89					raise StopIteration
90
91				arg = self.args[self.optind]
92
93				if self.endopt or arg == '-' or not arg.startswith('-'):
94					self.endopt = self.options.posixly_correct()
95					name = None
96					value = arg
97				elif arg == '--':
98					self.chrind = 0
99					self.endopt = True
100					return next(self)
101				elif not arg.startswith('--'):
102					name = '-' + arg[self.chrind]
103					self.chrind += 1
104					if self.chrind < len(arg):
105						if not self.options.needs_arg(name):
106							return (name, None)
107						value = arg[self.chrind:]
108					else:
109						value = None
110				elif '=' in arg and arg.index('=') >= 3:
111					name = arg.split('=', 1)[0]
112					if not self.options.needs_arg(name):
113						raise Exception('option "%s" does not take an argument' % name)
114					value = arg[len(name) + 1:]
115				else:
116					name = arg
117					value = None
118
119				if value is None and int(self.options.needs_arg(name)) > 0:
120					self.optind += 1
121					if self.optind == len(self.args):
122						raise Exception('option "%s" requires an argument' % name)
123					value = self.args[self.optind]
124
125				self.chrind = 0
126				return (name, value)
127
128
129def start(program_name, options, params, main_program):
130	parsed = Params() if params is None else params
131
132	try:
133		if sys.hexversion < 0x3050000:
134			raise Exception('python 3.5.0 or later required')
135
136		if params is None:
137			return main_program(options.reader(sys.argv), lambda name: options.fallback(name, parsed))
138
139		nonopt = []
140
141		for [name, value] in options.reader(sys.argv):
142			if name is None:
143				nonopt.append(value)
144			else:
145				options.parse(name, value, parsed)
146
147		return main_program(nonopt, parsed)
148
149	except Exception as ex:
150		if parsed.excstk:
151			raise
152
153		sys.stderr.write('%s: %s\n' % (sys.argv[0] if sys.argv[0] else program_name, str(ex)))
154		sys.exit(1)
155