Robot Framework Integrated Development Environment (RIDE)
argumentparser.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import getopt # optparse was not supported by Jython 2.2
17 import os
18 import re
19 import shlex
20 import sys
21 import glob
22 import string
23 import textwrap
24 
25 from robotide.lib.robot.errors import DataError, Information, FrameworkError
26 from robotide.lib.robot.version import get_full_version
27 
28 from .misc import plural_or_not
29 from .encoding import console_decode, system_decode
30 from .platform import PY2
31 from .utf8reader import Utf8Reader
32 from .robottypes import is_falsy, is_integer, is_list_like, is_string, is_unicode
33 
34 
35 ESCAPES = dict(
36  space = ' ', apos = "'", quot = '"', lt = '<', gt = '>',
37  pipe = '|', star = '*', comma = ',', slash = '/', semic = ';',
38  colon = ':', quest = '?', hash = '#', amp = '&', dollar = '$',
39  percent = '%', at = '@', exclam = '!', paren1 = '(', paren2 = ')',
40  square1 = '[', square2 = ']', curly1 = '{', curly2 = '}', bslash = '\\'
41 )
42 
43 
44 def cmdline2list(args, escaping=False):
45  if PY2 and is_unicode(args):
46  args = args.encode('UTF-8')
47  decode = lambda item: item.decode('UTF-8')
48  else:
49  decode = lambda item: item
50  lexer = shlex.shlex(args, posix=True)
51  if is_falsy(escaping):
52  lexer.escape = ''
53  lexer.escapedquotes = '"\''
54  lexer.commenters = ''
55  lexer.whitespace_split = True
56  try:
57  return [decode(token) for token in lexer]
58  except ValueError as err:
59  raise ValueError("Parsing '%s' failed: %s" % (args, err))
60 
61 
63 
66  _opt_line_re = re.compile('''
67  ^\s{1,4} # 1-4 spaces in the beginning of the line
68  ((-\S\s)*) # all possible short options incl. spaces (group 1)
69  --(\S{2,}) # required long option (group 3)
70  (\s\S+)? # optional value (group 4)
71  (\s\*)? # optional '*' telling option allowed multiple times (group 5)
72  ''', re.VERBOSE)
73 
74 
77  _quotes_re = re.compile('(.*)(\".*\")(.*)?')
78 
79 
84  def __init__(self, usage, name=None, version=None, arg_limits=None,
85  validator=None, env_options=None, auto_help=True,
86  auto_version=True, auto_escape=True, auto_pythonpath=True,
87  auto_argumentfile=True):
88  if not usage:
89  raise FrameworkError('Usage cannot be empty')
90  self.namename = name or usage.splitlines()[0].split(' -- ')[0].strip()
91  self.versionversion = version or get_full_version()
92  self._usage_usage = usage
93  self._arg_limit_validator_arg_limit_validator = ArgLimitValidator(arg_limits)
94  self._validator_validator = validator
95  self._auto_help_auto_help = auto_help
96  self._auto_version_auto_version = auto_version
97  self._auto_escape_auto_escape = auto_escape
98  self._auto_pythonpath_auto_pythonpath = auto_pythonpath
99  self._auto_argumentfile_auto_argumentfile = auto_argumentfile
100  self._env_options_env_options = env_options
101  self._short_opts_short_opts = ''
102  self._long_opts_long_opts = []
103  self._multi_opts_multi_opts = []
104  self._flag_opts_flag_opts = []
105  self._short_to_long_short_to_long = {}
106  self._expected_args_expected_args = ()
107  self._create_options_create_options(usage)
108 
109 
153  def parse_args(self, args):
154  # print(f"DEBUG: RFlib parse_args ENTER args={args}")
155  args = self._get_env_options_get_env_options() + self._save_filenames_save_filenames(args)
156  # args = self._get_env_options() + list(args)
157  # print(f"DEBUG: RFlib parse_args after _save_filenames: {args}")
158  args = [system_decode(a) for a in args]
159  # print(f"DEBUG: RFlib parse_args after system_decode: {args}")
160  if self._auto_argumentfile_auto_argumentfile:
161  args = self._process_possible_argfile_process_possible_argfile(args)
162  opts, args = self._parse_args_parse_args(args)
163  if self._auto_argumentfile_auto_argumentfile and opts.get('argumentfile'):
164  raise DataError("Using '--argumentfile' option in shortened format "
165  "like '--argumentf' is not supported.")
166  opts, args = self._handle_special_options_handle_special_options(opts, args)
167  self._arg_limit_validator_arg_limit_validator(args)
168  if self._validator_validator:
169  opts, args = self._validator_validator(opts, args)
170  # print(f"DEBUG: RFlib parse_args returning final = opts={opts} args={args}")
171  return opts, args
172 
173  def _get_env_options(self):
174  if self._env_options_env_options:
175  options = os.getenv(self._env_options_env_options)
176  if options:
177  return cmdline2list(options)
178  return []
179 
180  def _handle_special_options(self, opts, args):
181  if self._auto_escape_auto_escape and opts.get('escape'):
182  opts, args = self._unescape_opts_and_args_unescape_opts_and_args(opts, args)
183  if self._auto_help_auto_help and opts.get('help'):
184  self._raise_help_raise_help()
185  if self._auto_version_auto_version and opts.get('version'):
186  self._raise_version_raise_version()
187  if self._auto_pythonpath_auto_pythonpath and opts.get('pythonpath'):
188  sys.path = self._get_pythonpath_get_pythonpath(opts['pythonpath']) + sys.path
189  for auto, opt in [(self._auto_help_auto_help, 'help'),
190  (self._auto_version_auto_version, 'version'),
191  (self._auto_escape_auto_escape, 'escape'),
192  (self._auto_pythonpath_auto_pythonpath, 'pythonpath'),
193  (self._auto_argumentfile_auto_argumentfile, 'argumentfile')]:
194  if auto and opt in opts:
195  opts.pop(opt)
196  return opts, args
197 
198  def _save_filenames(self, args):
199  res = self._quotes_re_quotes_re.match(args)
200  # print(f"DEBUG: RFlib ENTER _save_filenames res={res}")
201  if not res:
202  return args.strip().strip().split()
203  clean = []
204  # DEBUG: example args
205  # --xunit "another output file.xml" --variablefile "a test file for variables.py" -v abc:new
206  # --debugfile "debug file.log"
207  clean = []
208  # DEBUG: example args
209  # --xunit "another output file.xml" --variablefile "a test file for variables.py" -v abc:new
210  # --debugfile "debug file.log"
211  # print(f"DEBUG: RFlib _save_filenames res.groups {res.groups()}")
212  for gr in res.groups():
213  line = []
214  if gr is not None and gr != '':
215  second_m = re.split('"', gr)
216  # print(f"DEBUG: RFlib _save_filenames second_m = {second_m}")
217  m = len(second_m)
218  if m > 2: # the middle element is the content
219  m = len(second_m)
220  for idx in range(0, m):
221  if second_m[idx]:
222  if idx % 2 == 0:
223  line.extend(second_m[idx].strip().strip().split())
224  elif idx % 2 != 0:
225  line.append(f"{second_m[idx]}")
226  else:
227  for idx in range(0, m):
228  if second_m[idx]:
229  line.extend(second_m[idx].strip().strip().split())
230  clean.extend(line)
231  # Fix variables
232  # print(f"DEBUG: RFlib _save_filenames DEFORE FIX VARIABLES clean= {clean}")
233  for idx, value in enumerate(clean):
234  if value[-1] == ':' and idx + 1 < len(clean):
235  clean[idx] = ''.join([value, clean[idx+1]])
236  clean.pop(idx+1)
237  # print(f"DEBUG: RFlib _save_filenames returnin clean= {clean}")
238  return clean
239 
240  def _parse_args(self, args):
241  args = [self._lowercase_long_option_lowercase_long_option(a) for a in args]
242  try:
243  opts, args = getopt.getopt(args, self._short_opts_short_opts, self._long_opts_long_opts)
244  except getopt.GetoptError as err:
245  raise DataError(err.msg)
246  return self._process_opts_process_opts(opts), self._glob_args_glob_args(args)
247 
248  def _lowercase_long_option(self, opt):
249  if not opt.startswith('--'):
250  return opt
251  if '=' not in opt:
252  return opt.lower()
253  opt, value = opt.split('=', 1)
254  return '%s=%s' % (opt.lower(), value)
255 
256  def _unescape_opts_and_args(self, opts, args):
257  from robotide.lib.robot.output import LOGGER
258  with LOGGER.cache_only:
259  LOGGER.warn("Option '--escape' is deprecated. Use console escape "
260  "mechanism instead.")
261  try:
262  escape_strings = opts['escape']
263  except KeyError:
264  raise FrameworkError("No 'escape' in options")
265  escapes = self._get_escapes_get_escapes(escape_strings)
266  for name, value in opts.items():
267  if name != 'escape':
268  opts[name] = self._unescape_unescape(value, escapes)
269  return opts, [self._unescape_unescape(arg, escapes) for arg in args]
270 
271  def _process_possible_argfile(self, args):
272  options = ['--argumentfile']
273  for short_opt, long_opt in self._short_to_long_short_to_long.items():
274  if long_opt == 'argumentfile':
275  options.append('-'+short_opt)
276  return ArgFileParser(options).process(args)
277 
278  def _get_escapes(self, escape_strings):
279  escapes = {}
280  for estr in escape_strings:
281  try:
282  name, value = estr.split(':', 1)
283  except ValueError:
284  raise DataError("Invalid escape string syntax '%s'. "
285  "Expected: what:with" % estr)
286  try:
287  escapes[value] = ESCAPES[name.lower()]
288  except KeyError:
289  raise DataError("Invalid escape '%s'. Available: %s"
290  % (name, self._get_available_escapes_get_available_escapes()))
291  return escapes
292 
293  def _unescape(self, value, escapes):
294  if value in [None, True, False]:
295  return value
296  if is_list_like(value):
297  return [self._unescape_unescape(item, escapes) for item in value]
298  for esc_name, esc_value in escapes.items():
299  if esc_name in value:
300  value = value.replace(esc_name, esc_value)
301  return value
302 
303  def _process_opts(self, opt_tuple):
304  opts = self._get_default_opts_get_default_opts()
305  for name, value in opt_tuple:
306  name = self._get_name_get_name(name)
307  if name in self._multi_opts_multi_opts:
308  opts[name].append(value)
309  elif name in self._flag_opts_flag_opts:
310  opts[name] = True
311  elif name.startswith('no') and name[2:] in self._flag_opts_flag_opts:
312  opts[name[2:]] = False
313  else:
314  opts[name] = value
315  return opts
316 
317  def _get_default_opts(self):
318  defaults = {}
319  for opt in self._long_opts_long_opts:
320  opt = opt.rstrip('=')
321  if opt.startswith('no') and opt[2:] in self._flag_opts_flag_opts:
322  continue
323  defaults[opt] = [] if opt in self._multi_opts_multi_opts else None
324  return defaults
325 
326  def _glob_args(self, args):
327  temp = []
328  for path in args:
329  paths = sorted(glob.glob(path))
330  if paths:
331  temp.extend(paths)
332  else:
333  temp.append(path)
334  return temp
335 
336  def _get_name(self, name):
337  name = name.lstrip('-')
338  try:
339  return self._short_to_long_short_to_long[name]
340  except KeyError:
341  return name
342 
343  def _create_options(self, usage):
344  for line in usage.splitlines():
345  res = self._opt_line_re_opt_line_re.match(line)
346  if res:
347  self._create_option_create_option(short_opts=[o[1] for o in res.group(1).split()],
348  long_opt=res.group(3).lower(),
349  takes_arg=bool(res.group(4)),
350  is_multi=bool(res.group(5)))
351 
352  def _create_option(self, short_opts, long_opt, takes_arg, is_multi):
353  self._verify_long_not_already_used_verify_long_not_already_used(long_opt, not takes_arg)
354  for sopt in short_opts:
355  if sopt in self._short_to_long_short_to_long:
356  self._raise_option_multiple_times_in_usage_raise_option_multiple_times_in_usage('-' + sopt)
357  self._short_to_long_short_to_long[sopt] = long_opt
358  if is_multi:
359  self._multi_opts_multi_opts.append(long_opt)
360  if takes_arg:
361  long_opt += '='
362  short_opts = [sopt+':' for sopt in short_opts]
363  else:
364  if long_opt.startswith('no'):
365  long_opt = long_opt[2:]
366  self._long_opts_long_opts.append('no' + long_opt)
367  self._flag_opts_flag_opts.append(long_opt)
368  self._long_opts_long_opts.append(long_opt)
369  self._short_opts_short_opts += (''.join(short_opts))
370 
371  def _verify_long_not_already_used(self, opt, flag=False):
372  if flag:
373  if opt.startswith('no'):
374  opt = opt[2:]
375  self._verify_long_not_already_used_verify_long_not_already_used(opt)
376  self._verify_long_not_already_used_verify_long_not_already_used('no' + opt)
377  elif opt in [o.rstrip('=') for o in self._long_opts_long_opts]:
378  self._raise_option_multiple_times_in_usage_raise_option_multiple_times_in_usage('--' + opt)
379 
380  def _get_pythonpath(self, paths):
381  if is_string(paths):
382  paths = [paths]
383  temp = []
384  for path in self._split_pythonpath_split_pythonpath(paths):
385  temp.extend(glob.glob(path))
386  return [os.path.abspath(path) for path in temp if path]
387 
388  def _split_pythonpath(self, paths):
389  # paths may already contain ':' as separator
390  tokens = ':'.join(paths).split(':')
391  if os.sep == '/':
392  return tokens
393  # Fix paths split like 'c:\temp' -> 'c', '\temp'
394  ret = []
395  drive = ''
396  for item in tokens:
397  item = item.replace('/', '\\')
398  if drive and item.startswith('\\'):
399  ret.append('%s:%s' % (drive, item))
400  drive = ''
401  continue
402  if drive:
403  ret.append(drive)
404  drive = ''
405  if len(item) == 1 and item in string.ascii_letters:
406  drive = item
407  else:
408  ret.append(item)
409  if drive:
410  ret.append(drive)
411  return ret
412 
414  names = sorted(ESCAPES.keys(), key=str.lower)
415  return ', '.join('%s (%s)' % (n, ESCAPES[n]) for n in names)
416 
417  def _raise_help(self):
418  msg = self._usage_usage
419  if self.versionversion:
420  msg = msg.replace('<VERSION>', self.versionversion)
421  def replace_escapes(res):
422  escapes = 'Available escapes: ' + self._get_available_escapes_get_available_escapes()
423  lines = textwrap.wrap(escapes, width=len(res.group(2)))
424  indent = ' ' * len(res.group(1))
425  return '\n'.join(indent + line for line in lines)
426  msg = re.sub('( *)(<-+ESCAPES-+>)', replace_escapes, msg)
427  raise Information(msg)
428 
429  def _raise_version(self):
430  raise Information('%s %s' % (self.namename, self.versionversion))
431 
433  raise FrameworkError("Option '%s' multiple times in usage" % opt)
434 
435 
437 
438  def __init__(self, arg_limits):
439  self._min_args, self._max_args_max_args = self._parse_arg_limits_parse_arg_limits(arg_limits)
440 
441  def _parse_arg_limits(self, arg_limits):
442  if arg_limits is None:
443  return 0, sys.maxsize
444  if is_integer(arg_limits):
445  return arg_limits, arg_limits
446  if len(arg_limits) == 1:
447  return arg_limits[0], sys.maxsize
448  return arg_limits[0], arg_limits[1]
449 
450  def __call__(self, args):
451  if not (self._min_args <= len(args) <= self._max_args_max_args):
452  self._raise_invalid_args_raise_invalid_args(self._min_args, self._max_args_max_args, len(args))
453 
454  def _raise_invalid_args(self, min_args, max_args, arg_count):
455  min_end = plural_or_not(min_args)
456  if min_args == max_args:
457  expectation = "%d argument%s" % (min_args, min_end)
458  elif max_args != sys.maxsize:
459  expectation = "%d to %d arguments" % (min_args, max_args)
460  else:
461  expectation = "at least %d argument%s" % (min_args, min_end)
462  raise DataError("Expected %s, got %d." % (expectation, arg_count))
463 
464 
466 
467  def __init__(self, options):
468  self._options_options = options
469 
470  def process(self, args):
471  while True:
472  path, replace = self._get_index_get_index(args)
473  if not path:
474  break
475  args[replace] = self._get_args_get_args(path)
476  return args
477 
478  def _get_index(self, args):
479  for opt in self._options_options:
480  start = opt + '=' if opt.startswith('--') else opt
481  for index, arg in enumerate(args):
482  normalized_arg = arg.lower() if opt.startswith('--') else arg
483  # Handles `--argumentfile foo` and `-A foo`
484  if normalized_arg == opt and index + 1 < len(args):
485  return args[index+1], slice(index, index+2)
486  # Handles `--argumentfile=foo` and `-Afoo`
487  if normalized_arg.startswith(start):
488  return arg[len(start):], slice(index, index+1)
489  return None, -1
490 
491  def _get_args(self, path):
492  if path.upper() != 'STDIN':
493  content = self._read_from_file_read_from_file(path)
494  else:
495  content = self._read_from_stdin_read_from_stdin()
496  return self._process_file_process_file(content)
497 
498  def _read_from_file(self, path):
499  try:
500  with Utf8Reader(path) as reader:
501  return reader.read()
502  except (IOError, UnicodeError) as err:
503  raise DataError("Opening argument file '%s' failed: %s"
504  % (path, err))
505 
506  def _read_from_stdin(self):
507  return console_decode(sys.__stdin__.read())
508 
509  def _process_file(self, content):
510  args = []
511  for line in content.splitlines():
512  line = line.strip()
513  if line.startswith('-'):
514  args.extend(self._split_option_split_option(line))
515  elif line and not line.startswith('#'):
516  args.append(line)
517  return args
518 
519  def _split_option(self, line):
520  separator = self._get_option_separator_get_option_separator(line)
521  if not separator:
522  return [line]
523  option, value = line.split(separator, 1)
524  if separator == ' ':
525  value = value.strip()
526  return [option, value]
527 
528  def _get_option_separator(self, line):
529  if ' ' not in line and '=' not in line:
530  return None
531  if '=' not in line:
532  return ' '
533  if ' ' not in line:
534  return '='
535  return ' ' if line.index(' ') < line.index('=') else '='
Used when variable does not exist.
Definition: errors.py:67
Can be used when the core framework goes to unexpected state.
Definition: errors.py:59
Used by argument parser with –help or –version.
Definition: errors.py:107
def _raise_invalid_args(self, min_args, max_args, arg_count)
def _create_option(self, short_opts, long_opt, takes_arg, is_multi)
def __init__(self, usage, name=None, version=None, arg_limits=None, validator=None, env_options=None, auto_help=True, auto_version=True, auto_escape=True, auto_pythonpath=True, auto_argumentfile=True)
Available options and tool name are read from the usage.
def parse_args(self, args)
Parse given arguments and return options and positional arguments.
def cmdline2list(args, escaping=False)
def system_decode(string)
Decodes bytes from system (e.g.
Definition: encoding.py:81
def console_decode(string, encoding=CONSOLE_ENCODING, force=False)
Decodes bytes from console encoding to Unicode.
Definition: encoding.py:45
def is_falsy(item)
Opposite of :func:is_truthy.
Definition: robottypes.py:56
def get_full_version(program=None, naked=False)
Definition: version.py:30