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