3939 from ..helpers import format_file_size
4040 from ..helpers import remove_surrogates , text_to_json
4141 from ..helpers import DatetimeWrapper , replace_placeholders
42- from ..helpers .argparsing import flatten_namespace , ArgumentTypeError , ArgumentParser , Namespace , SUPPRESS
42+ from ..helpers .argparsing import flatten_namespace , ArgumentTypeError , ArgumentParser , SUPPRESS
4343 from ..helpers import is_slow_msgpack , is_supported_msgpack , sysinfo
4444 from ..helpers import signal_handler , raising_signal_handler , SigHup , SigTerm
4545 from ..helpers import ErrorIgnoringTextIOWrapper
@@ -178,62 +178,45 @@ def preprocess_args(self, args):
178178
179179 class CommonOptions :
180180 """
181- Support class to allow specifying common options directly after the top-level command.
181+ Support class to allow specifying common options at multiple levels of the command hierarchy .
182182
183- Normally options can only be specified on the parser defining them, which means
184- that generally speaking *all* options go after all sub-commands. This is annoying
185- for common options in scripts, e.g. --remote-path or logging options.
183+ Common options (e.g. --log-level, --repo) can be placed anywhere in the command line:
186184
187- This class allows adding the same set of options to both the top-level parser
188- and the final sub-command parsers (but not intermediary sub-commands, at least for now).
185+ borg --info create ... # before the subcommand
186+ borg create --info ... # after the subcommand
187+ borg --info debug info --debug # at both levels of a two-level command
189188
190- It does so by giving every option's target name (" dest") a suffix indicating its level
191- -- no two options in the parser hierarchy can have the same target --
192- then, after parsing the command line, multiple definitions are resolved .
189+ Each parser level registers the same options with the same dest names.
190+ Defaults are only provided on the top-level parser; all sub-parsers use SUPPRESS so
191+ that unset options don't appear in the namespace at all .
193192
194- Defaults are handled by only setting them on the top-level parser and setting
195- a sentinel object in all sub-parsers, which then allows one to discern which parser
196- supplied the option .
193+ flatten_namespace() handles precedence: it walks sub-namespaces depth-first, so the
194+ most-specific (innermost) value wins. For append-action options (e.g. --debug-topic)
195+ it merges lists from all levels .
197196 """
198197
199- def __init__ (self , define_common_options , suffix_precedence ):
198+ def __init__ (self , define_common_options ):
200199 """
201200 *define_common_options* should be a callable taking one argument, which
202- will be a argparse.Parser.add_argument-like function.
201+ will be an argparse.Parser.add_argument-like function.
203202
204203 *define_common_options* will be called multiple times, and should call
205204 the passed function to define common options exactly the same way each time.
206-
207- *suffix_precedence* should be a tuple of the suffixes that will be used.
208- It is ordered from lowest precedence to highest precedence:
209- An option specified on the parser belonging to index 0 is overridden if the
210- same option is specified on any parser with a higher index.
211205 """
212206 self .define_common_options = define_common_options
213- self .suffix_precedence = suffix_precedence
214-
215- # Maps suffixes to sets of target names.
216- # E.g. common_options["_subcommand"] = {..., "log_level", ...}
217- self .common_options = dict ()
218- # Set of options with the 'append' action.
219- self .append_options = set ()
220207 # This is the sentinel object that replaces all default values in parsers
221208 # below the top-level parser.
222209 self .default_sentinel = object ()
223210
224- def add_common_group (self , parser , suffix , provide_defaults = False ):
211+ def add_common_group (self , parser , provide_defaults = False ):
225212 """
226213 Add common options to *parser*.
227214
228- *provide_defaults* must only be True exactly once in a parser hierarchy,
229- at the top level, and False on all lower levels. The default is chosen
230- accordingly.
231-
232- *suffix* indicates the suffix to use internally. It also indicates
233- which precedence the *parser* has for common options. See *suffix_precedence*
234- of __init__.
215+ *provide_defaults* must be True exactly once in a parser hierarchy (the top-level
216+ parser) and False on all sub-parsers. Sub-parsers get SUPPRESS as the default so
217+ that an unspecified option produces no attribute, leaving the top-level default intact
218+ after flatten_namespace() merges the namespaces.
235219 """
236- assert suffix in self .suffix_precedence
237220
238221 def add_argument (* args , ** kwargs ):
239222 if "dest" in kwargs :
@@ -248,20 +231,9 @@ def add_argument(*args, **kwargs):
248231 "append" ,
249232 )
250233 is_append = kwargs ["action" ] == "append"
251- if is_append :
252- self .append_options .add (kwargs ["dest" ])
253- assert (
254- kwargs ["default" ] == []
255- ), "The default is explicitly constructed as an empty list in resolve()"
256- else :
257- self .common_options .setdefault (suffix , set ()).add (kwargs ["dest" ])
258- kwargs ["dest" ] += suffix
259234 if not provide_defaults :
260- # Interpolate help now, in case the %(default)d (or so ) is mentioned,
235+ # Interpolate help now, in case %(default)d (or similar ) is mentioned,
261236 # to avoid producing incorrect help output.
262- # Assumption: Interpolated output can safely be interpolated again,
263- # which should always be the case.
264- # Note: We control all inputs.
265237 kwargs ["help" ] = kwargs ["help" ] % kwargs
266238 if not is_append :
267239 kwargs ["default" ] = SUPPRESS
@@ -271,66 +243,23 @@ def add_argument(*args, **kwargs):
271243 common_group = parser .add_argument_group ("Common options" )
272244 self .define_common_options (add_argument )
273245
274- def resolve (self , args : Namespace ): # Namespace has "in" but otherwise is not like a dict.
275- """
276- Resolve the multiple definitions of each common option to the final value.
277- """
278- for suffix in self .suffix_precedence :
279- # From highest level to lowest level, so the "most-specific" option wins, e.g.
280- # "borg --debug create --info" shall result in --info being effective.
281- for dest in self .common_options .get (suffix , []):
282- # map_from is this suffix' option name, e.g. log_level_subcommand
283- # map_to is the target name, e.g. log_level
284- map_from = dest + suffix
285- map_to = dest
286- # Retrieve value; depending on the action it may not exist, but usually does
287- # (store_const/store_true/store_false), either because the action implied a default
288- # or a default is explicitly supplied.
289- # Note that defaults on lower levels are replaced with default_sentinel.
290- # Only the top level has defaults.
291- value = getattr (args , map_from , self .default_sentinel )
292- if value is not self .default_sentinel :
293- # value was indeed specified on this level. Transfer value to target,
294- # and un-clobber the args (for tidiness - you *cannot* use the suffixed
295- # names for other purposes, obviously).
296- setattr (args , map_to , value )
297- try :
298- delattr (args , map_from )
299- except AttributeError :
300- pass
301-
302- # Options with an "append" action need some special treatment. Instead of
303- # overriding values, all specified values are merged together.
304- for dest in self .append_options :
305- option_value = []
306- for suffix in self .suffix_precedence :
307- # Find values of this suffix, if any, and add them to the final list
308- extend_from = dest + suffix
309- if extend_from in args :
310- values = getattr (args , extend_from )
311- delattr (args , extend_from )
312- option_value .extend (values )
313- setattr (args , dest , option_value )
314-
315246 def build_parser (self ):
316247 from ._common import define_common_options
317248
318249 parser = ArgumentParser (prog = self .prog , description = "Borg - Deduplicated Backups" , add_help = False )
319250 # paths and patterns must have an empty list as default everywhere
320- parser .common_options = self .CommonOptions (
321- define_common_options , suffix_precedence = ("_maincommand" , "_midcommand" , "_subcommand" )
322- )
251+ parser .common_options = self .CommonOptions (define_common_options )
323252 parser .add_argument (
324253 "-V" , "--version" , action = "version" , version = "%(prog)s " + __version__ , help = "show version number and exit"
325254 )
326255 parser .add_argument ("--cockpit" , dest = "cockpit" , action = "store_true" , help = "Start the Borg TUI" )
327- parser .common_options .add_common_group (parser , "_maincommand" , provide_defaults = True )
256+ parser .common_options .add_common_group (parser , provide_defaults = True )
328257
329258 common_parser = ArgumentParser (add_help = False , prog = self .prog )
330- parser .common_options .add_common_group (common_parser , "_subcommand" )
259+ parser .common_options .add_common_group (common_parser )
331260
332261 mid_common_parser = ArgumentParser (add_help = False , prog = self .prog )
333- parser .common_options .add_common_group (mid_common_parser , "_midcommand" )
262+ parser .common_options .add_common_group (mid_common_parser )
334263
335264 if parser .prog == "borgfs" :
336265 return self .build_parser_borgfs (parser )
@@ -415,7 +344,6 @@ def parse_args(self, args=None):
415344 if getattr (args , list_attr , None ) is None :
416345 setattr (args , list_attr , [])
417346
418- parser .common_options .resolve (args )
419347 func = self .get_func (args , parser )
420348 if func == self .do_create and args .paths and args .paths_from_stdin :
421349 parser .error ("Must not pass PATH with --paths-from-stdin." )
0 commit comments