Robot Framework
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
17 import glob
18 import os
19 import re
20 import shlex
21 import sys
22 import string
23 import warnings
24 
25 from robot.errors import DataError, Information, FrameworkError
26 from robot.version import get_full_version
27 
28 from .encoding import console_decode, system_decode
29 from .filereader import FileReader
30 from .misc import plural_or_not
31 from .robottypes import is_falsy, is_integer, is_string
32 
33 
34 def cmdline2list(args, escaping=False):
35  lexer = shlex.shlex(args, posix=True)
36  if is_falsy(escaping):
37  lexer.escape = ''
38  lexer.escapedquotes = '"\''
39  lexer.commenters = ''
40  lexer.whitespace_split = True
41  try:
42  return list(lexer)
43  except ValueError as err:
44  raise ValueError("Parsing '%s' failed: %s" % (args, err))
45 
46 
48 
51  _opt_line_re = re.compile(r'''
52  ^\s{1,4} # 1-4 spaces in the beginning of the line
53  ((-\S\s)*) # all possible short options incl. spaces (group 1)
54  --(\S{2,}) # required long option (group 3)
55  (\s\S+)? # optional value (group 4)
56  (\s\*)? # optional '*' telling option allowed multiple times (group 5)
57  ''', re.VERBOSE)
58 
59 
64  def __init__(self, usage, name=None, version=None, arg_limits=None,
65  validator=None, env_options=None, auto_help=True,
66  auto_version=True, auto_pythonpath='DEPRECATED',
67  auto_argumentfile=True):
68  if not usage:
69  raise FrameworkError('Usage cannot be empty')
70  self.namename = name or usage.splitlines()[0].split(' -- ')[0].strip()
71  self.versionversion = version or get_full_version()
72  self._usage_usage = usage
73  self._arg_limit_validator_arg_limit_validator = ArgLimitValidator(arg_limits)
74  self._validator_validator = validator
75  self._auto_help_auto_help = auto_help
76  self._auto_version_auto_version = auto_version
77  # TODO: Change DeprecationWarning to more loud UserWarning in RF 6.1.
78  if auto_pythonpath == 'DEPRECATED':
79  auto_pythonpath = False
80  else:
81  warnings.warn("ArgumentParser option 'auto_pythonpath' is deprecated "
82  "since Robot Framework 5.0.", DeprecationWarning)
83  self._auto_pythonpath_auto_pythonpath = auto_pythonpath
84  self._auto_argumentfile_auto_argumentfile = auto_argumentfile
85  self._env_options_env_options = env_options
86  self._short_opts_short_opts = ''
87  self._long_opts_long_opts = []
88  self._multi_opts_multi_opts = []
89  self._flag_opts_flag_opts = []
90  self._short_to_long_short_to_long = {}
91  self._expected_args_expected_args = ()
92  self._create_options_create_options(usage)
93 
94 
134  def parse_args(self, args):
135  args = self._get_env_options_get_env_options() + list(args)
136  args = [system_decode(a) for a in args]
137  if self._auto_argumentfile_auto_argumentfile:
138  args = self._process_possible_argfile_process_possible_argfile(args)
139  opts, args = self._parse_args_parse_args(args)
140  if self._auto_argumentfile_auto_argumentfile and opts.get('argumentfile'):
141  raise DataError("Using '--argumentfile' option in shortened format "
142  "like '--argumentf' is not supported.")
143  opts, args = self._handle_special_options_handle_special_options(opts, args)
144  self._arg_limit_validator_arg_limit_validator(args)
145  if self._validator_validator:
146  opts, args = self._validator_validator(opts, args)
147  return opts, args
148 
149  def _get_env_options(self):
150  if self._env_options_env_options:
151  options = os.getenv(self._env_options_env_options)
152  if options:
153  return cmdline2list(options)
154  return []
155 
156  def _handle_special_options(self, opts, args):
157  if self._auto_help_auto_help and opts.get('help'):
158  self._raise_help_raise_help()
159  if self._auto_version_auto_version and opts.get('version'):
160  self._raise_version_raise_version()
161  if self._auto_pythonpath_auto_pythonpath and opts.get('pythonpath'):
162  sys.path = self._get_pythonpath_get_pythonpath(opts['pythonpath']) + sys.path
163  for auto, opt in [(self._auto_help_auto_help, 'help'),
164  (self._auto_version_auto_version, 'version'),
165  (self._auto_pythonpath_auto_pythonpath, 'pythonpath'),
166  (self._auto_argumentfile_auto_argumentfile, 'argumentfile')]:
167  if auto and opt in opts:
168  opts.pop(opt)
169  return opts, args
170 
171  def _parse_args(self, args):
172  args = [self._lowercase_long_option_lowercase_long_option(a) for a in args]
173  try:
174  opts, args = getopt.getopt(args, self._short_opts_short_opts, self._long_opts_long_opts)
175  except getopt.GetoptError as err:
176  raise DataError(err.msg)
177  return self._process_opts_process_opts(opts), self._glob_args_glob_args(args)
178 
179  def _lowercase_long_option(self, opt):
180  if not opt.startswith('--'):
181  return opt
182  if '=' not in opt:
183  return opt.lower()
184  opt, value = opt.split('=', 1)
185  return '%s=%s' % (opt.lower(), value)
186 
187  def _process_possible_argfile(self, args):
188  options = ['--argumentfile']
189  for short_opt, long_opt in self._short_to_long_short_to_long.items():
190  if long_opt == 'argumentfile':
191  options.append('-'+short_opt)
192  return ArgFileParser(options).process(args)
193 
194  def _process_opts(self, opt_tuple):
195  opts = self._get_default_opts_get_default_opts()
196  for name, value in opt_tuple:
197  name = self._get_name_get_name(name)
198  if name in self._multi_opts_multi_opts:
199  opts[name].append(value)
200  elif name in self._flag_opts_flag_opts:
201  opts[name] = True
202  elif name.startswith('no') and name[2:] in self._flag_opts_flag_opts:
203  opts[name[2:]] = False
204  else:
205  opts[name] = value
206  return opts
207 
208  def _get_default_opts(self):
209  defaults = {}
210  for opt in self._long_opts_long_opts:
211  opt = opt.rstrip('=')
212  if opt.startswith('no') and opt[2:] in self._flag_opts_flag_opts:
213  continue
214  defaults[opt] = [] if opt in self._multi_opts_multi_opts else None
215  return defaults
216 
217  def _glob_args(self, args):
218  temp = []
219  for path in args:
220  paths = sorted(glob.glob(path))
221  if paths:
222  temp.extend(paths)
223  else:
224  temp.append(path)
225  return temp
226 
227  def _get_name(self, name):
228  name = name.lstrip('-')
229  try:
230  return self._short_to_long_short_to_long[name]
231  except KeyError:
232  return name
233 
234  def _create_options(self, usage):
235  for line in usage.splitlines():
236  res = self._opt_line_re_opt_line_re.match(line)
237  if res:
238  self._create_option_create_option(short_opts=[o[1] for o in res.group(1).split()],
239  long_opt=res.group(3).lower(),
240  takes_arg=bool(res.group(4)),
241  is_multi=bool(res.group(5)))
242 
243  def _create_option(self, short_opts, long_opt, takes_arg, is_multi):
244  self._verify_long_not_already_used_verify_long_not_already_used(long_opt, not takes_arg)
245  for sopt in short_opts:
246  if sopt in self._short_to_long_short_to_long:
247  self._raise_option_multiple_times_in_usage_raise_option_multiple_times_in_usage('-' + sopt)
248  self._short_to_long_short_to_long[sopt] = long_opt
249  if is_multi:
250  self._multi_opts_multi_opts.append(long_opt)
251  if takes_arg:
252  long_opt += '='
253  short_opts = [sopt+':' for sopt in short_opts]
254  else:
255  if long_opt.startswith('no'):
256  long_opt = long_opt[2:]
257  self._long_opts_long_opts.append('no' + long_opt)
258  self._flag_opts_flag_opts.append(long_opt)
259  self._long_opts_long_opts.append(long_opt)
260  self._short_opts_short_opts += (''.join(short_opts))
261 
262  def _verify_long_not_already_used(self, opt, flag=False):
263  if flag:
264  if opt.startswith('no'):
265  opt = opt[2:]
266  self._verify_long_not_already_used_verify_long_not_already_used(opt)
267  self._verify_long_not_already_used_verify_long_not_already_used('no' + opt)
268  elif opt in [o.rstrip('=') for o in self._long_opts_long_opts]:
269  self._raise_option_multiple_times_in_usage_raise_option_multiple_times_in_usage('--' + opt)
270 
271  def _get_pythonpath(self, paths):
272  if is_string(paths):
273  paths = [paths]
274  temp = []
275  for path in self._split_pythonpath_split_pythonpath(paths):
276  temp.extend(glob.glob(path))
277  return [os.path.abspath(path) for path in temp if path]
278 
279  def _split_pythonpath(self, paths):
280  # paths may already contain ':' as separator
281  tokens = ':'.join(paths).split(':')
282  if os.sep == '/':
283  return tokens
284  # Fix paths split like 'c:\temp' -> 'c', '\temp'
285  ret = []
286  drive = ''
287  for item in tokens:
288  item = item.replace('/', '\\')
289  if drive and item.startswith('\\'):
290  ret.append('%s:%s' % (drive, item))
291  drive = ''
292  continue
293  if drive:
294  ret.append(drive)
295  drive = ''
296  if len(item) == 1 and item in string.ascii_letters:
297  drive = item
298  else:
299  ret.append(item)
300  if drive:
301  ret.append(drive)
302  return ret
303 
304  def _raise_help(self):
305  usage = self._usage_usage
306  if self.versionversion:
307  usage = usage.replace('<VERSION>', self.versionversion)
308  raise Information(usage)
309 
310  def _raise_version(self):
311  raise Information('%s %s' % (self.namename, self.versionversion))
312 
314  raise FrameworkError("Option '%s' multiple times in usage" % opt)
315 
316 
318 
319  def __init__(self, arg_limits):
320  self._min_args, self._max_args_max_args = self._parse_arg_limits_parse_arg_limits(arg_limits)
321 
322  def _parse_arg_limits(self, arg_limits):
323  if arg_limits is None:
324  return 0, sys.maxsize
325  if is_integer(arg_limits):
326  return arg_limits, arg_limits
327  if len(arg_limits) == 1:
328  return arg_limits[0], sys.maxsize
329  return arg_limits[0], arg_limits[1]
330 
331  def __call__(self, args):
332  if not (self._min_args <= len(args) <= self._max_args_max_args):
333  self._raise_invalid_args_raise_invalid_args(self._min_args, self._max_args_max_args, len(args))
334 
335  def _raise_invalid_args(self, min_args, max_args, arg_count):
336  min_end = plural_or_not(min_args)
337  if min_args == max_args:
338  expectation = "%d argument%s" % (min_args, min_end)
339  elif max_args != sys.maxsize:
340  expectation = "%d to %d arguments" % (min_args, max_args)
341  else:
342  expectation = "at least %d argument%s" % (min_args, min_end)
343  raise DataError("Expected %s, got %d." % (expectation, arg_count))
344 
345 
347 
348  def __init__(self, options):
349  self._options_options = options
350 
351  def process(self, args):
352  while True:
353  path, replace = self._get_index_get_index(args)
354  if not path:
355  break
356  args[replace] = self._get_args_get_args(path)
357  return args
358 
359  def _get_index(self, args):
360  for opt in self._options_options:
361  start = opt + '=' if opt.startswith('--') else opt
362  for index, arg in enumerate(args):
363  normalized_arg = arg.lower() if opt.startswith('--') else arg
364  # Handles `--argumentfile foo` and `-A foo`
365  if normalized_arg == opt and index + 1 < len(args):
366  return args[index+1], slice(index, index+2)
367  # Handles `--argumentfile=foo` and `-Afoo`
368  if normalized_arg.startswith(start):
369  return arg[len(start):], slice(index, index+1)
370  return None, -1
371 
372  def _get_args(self, path):
373  if path.upper() != 'STDIN':
374  content = self._read_from_file_read_from_file(path)
375  else:
376  content = self._read_from_stdin_read_from_stdin()
377  return self._process_file_process_file(content)
378 
379  def _read_from_file(self, path):
380  try:
381  with FileReader(path) as reader:
382  return reader.read()
383  except (IOError, UnicodeError) as err:
384  raise DataError("Opening argument file '%s' failed: %s"
385  % (path, err))
386 
387  def _read_from_stdin(self):
388  return console_decode(sys.__stdin__.read())
389 
390  def _process_file(self, content):
391  args = []
392  for line in content.splitlines():
393  line = line.strip()
394  if line.startswith('-'):
395  args.extend(self._split_option_split_option(line))
396  elif line and not line.startswith('#'):
397  args.append(line)
398  return args
399 
400  def _split_option(self, line):
401  separator = self._get_option_separator_get_option_separator(line)
402  if not separator:
403  return [line]
404  option, value = line.split(separator, 1)
405  if separator == ' ':
406  value = value.strip()
407  return [option, value]
408 
409  def _get_option_separator(self, line):
410  if ' ' not in line and '=' not in line:
411  return None
412  if '=' not in line:
413  return ' '
414  if ' ' not in line:
415  return '='
416  return ' ' if line.index(' ') < line.index('=') else '='
Can be used when the core framework goes to unexpected state.
Definition: errors.py:53
Used by argument parser with –help or –version.
Definition: errors.py:107
def _raise_invalid_args(self, min_args, max_args, arg_count)
def _verify_long_not_already_used(self, opt, flag=False)
def _create_option(self, short_opts, long_opt, takes_arg, is_multi)
def parse_args(self, args)
Parse given arguments and return options and positional arguments.
def __init__(self, usage, name=None, version=None, arg_limits=None, validator=None, env_options=None, auto_help=True, auto_version=True, auto_pythonpath='DEPRECATED', auto_argumentfile=True)
Available options and tool name are read from the usage.
Utility to ease reading different kind of files.
Definition: filereader.py:40
def cmdline2list(args, escaping=False)
def console_decode(string, encoding=CONSOLE_ENCODING)
Decodes bytes from console encoding to Unicode.
Definition: encoding.py:39
def system_decode(string)
Definition: encoding.py:82
def plural_or_not(item)
Definition: misc.py:73
def is_falsy(item)
Opposite of :func:is_truthy.
Definition: robottypes.py:169
def get_full_version(program=None, naked=False)
Definition: version.py:30