1######################################################################
2#
3# File: b2/parse_args.py
4#
5# Copyright 2018 Backblaze Inc. All Rights Reserved.
6#
7# License https://www.backblaze.com/using_b2_code.html
8#
9######################################################################
10
11import logging
12
13import six
14
15from .utils import repr_dict_deterministically
16
17logger = logging.getLogger(__name__)
18
19
20class Arguments(object):
21    """
22    An object to stick attributes on.
23    """
24
25    def __repr__(self):
26        return '%s(%s)' % (
27            self.__class__.__name__,
28            repr_dict_deterministically(self.__dict__),
29        )
30
31
32def check_for_duplicate_args(args_dict):
33    """
34    Checks that no argument name is listed in multiple places.
35
36    Raises a ValueError if there is a problem.
37
38    This args_dict has a problem because 'required' and 'optional'
39    both contain 'a':
40
41       {
42          'option_args': ['b', 'c'],
43          'required': ['a', 'd']
44          'optional': ['a', 'e']
45       }
46    """
47    categories = sorted(six.iterkeys(args_dict))
48    for index_a, category_a in enumerate(categories):
49        for category_b in categories[index_a + 1:]:
50            names_a = args_dict[category_a]
51            names_b = args_dict[category_b]
52            for common_name in set(names_a) & set(names_b):
53                raise ValueError(
54                    "argument '%s' is in both '%s' an '%s'" % (common_name, category_a, category_b)
55                )
56
57
58def parse_arg_list(
59    arg_list, option_flags, option_args, list_args, optional_before, required, optional, arg_parser
60):
61    """
62    Converts a list of string arguments to an Arguments object, with
63    one attribute per parameter.
64
65    The value of every parameter is set in the returned Arguments object,
66    even for parameters not specified on the command line.
67
68    Option Flags set boolean values that default to False.  When the
69    option is present on the command line, the value is set to True.
70
71    Option Args have values provided on the command line.  The default
72    if not present is None.
73
74    List Args act like Option Args, but can be specified more than
75    once, and their values are collected into a list.  Default is [].
76
77    Required positional parameters must be present, and do not have
78    a double-dash name preceding them.
79
80    Optional positional parameters are just like required parameters,
81    but don't have to be there and default to None.
82
83    Arg Parser is a dict that maps from a parameter name to a function
84    tha converts the string argument into the value needed by the
85    program.  These parameters can be Option Args, List Args, Required,
86    or Optional.
87
88    :param arg_list sys.argv[1:], or equivalent
89    :param option_flags: Names of options that are boolean flags.
90    :param option_args: Names of options that have values.
91    :param list_args: Names of options whose values are collected into a list.
92    :param optional_before: Names of option positional params that come before the required ones.
93    :param required: Names of positional params that must be there.
94    :param optional: Names of optional params.
95    :param arg_parser: Map from param name to parser for values.
96    :return: An Argument object, or None if there was any error parsing.
97    """
98
99    # Sanity check the inputs.
100    check_for_duplicate_args(
101        {
102            'option_flags': option_flags,
103            'option_args': option_args,
104            'optional_before': optional_before,
105            'required': required,
106            'optional': optional
107        }
108    )
109
110    # Create an object to hold the arguments.
111    result = Arguments()
112
113    # Set the default value for everything that has a default value.
114    for name in option_flags:
115        setattr(result, name, False)
116    for name in option_args:
117        setattr(result, name, None)
118    for name in list_args:
119        setattr(result, name, [])
120    for name in optional:
121        setattr(result, name, None)
122
123    # Make a function for parsing argument values
124    def parse_arg(name, arg_list):
125        value = arg_list.pop(0)
126        if name in arg_parser:
127            value = arg_parser[name](value)
128        return value
129
130    # Parse the '--' options
131    while len(arg_list) != 0 and arg_list[0].startswith('--'):
132        option = arg_list.pop(0)[2:]
133        if option in option_flags:
134            logger.debug('option %s is properly recognized as OPTION_FLAGS', option)
135            setattr(result, option, True)
136        elif option in option_args:
137            if len(arg_list) == 0:
138                logger.debug(
139                    'option %s is recognized as OPTION_ARGS and there are no more arguments on arg_list to parse',
140                    option
141                )
142                return None
143            else:
144                logger.debug('option %s is properly recognized as OPTION_ARGS', option)
145                setattr(result, option, parse_arg(option, arg_list))
146        elif option in list_args:
147            if len(arg_list) == 0:
148                logger.debug(
149                    'option %s is recognized as LIST_ARGS and there are no more arguments on arg_list to parse',
150                    option
151                )
152                return None
153            else:
154                logger.debug('option %s is properly recognized as LIST_ARGS', option)
155                getattr(result, option).append(parse_arg(option, arg_list))
156        else:
157            logger.error('option %s is of unknown type!', option)
158            return None
159
160    # Handle optional positional parameters that come first.
161    # We assume that if there are optional parameters, the
162    # ones that come before take precedence over the ones
163    # that come after the required arguments.
164    for arg_name in optional_before:
165        if len(required) < len(arg_list):
166            setattr(result, arg_name, parse_arg(arg_name, arg_list))
167        else:
168            setattr(result, arg_name, None)
169
170    # Parse the positional parameters
171    for arg_name in required:
172        if len(arg_list) == 0:
173            logger.debug('lack of required positional argument: %s', arg_name)
174            return None
175        setattr(result, arg_name, parse_arg(arg_name, arg_list))
176    for arg_name in optional:
177        if len(arg_list) != 0:
178            setattr(result, arg_name, parse_arg(arg_name, arg_list))
179
180    # Anything left is a problem
181    if len(arg_list) != 0:
182        logger.debug('option parser failed to consume this: %s', arg_list)
183        return None
184
185    # Return the Arguments object
186    return result
187