diff --git a/fire/core.py b/fire/core.py index 8e23e76b..46e075f4 100644 --- a/fire/core.py +++ b/fire/core.py @@ -69,6 +69,11 @@ def main(argv): from fire import value_types from fire.console import console_io +# Single-character aliases for Fire's own flags (defined in parser.CreateParser). +# Shortcut expansion must not map these characters to user-defined arguments, +# because doing so would silently shadow Fire's built-in options. +_FIRE_RESERVED_SINGLE_CHAR_FLAGS = frozenset({'h', 'i', 'v', 't'}) + def Fire(component=None, command=None, name=None, serialize=None): """This function, Fire, is the main entrypoint for Python Fire. @@ -890,14 +895,20 @@ def _ParseKeywordArgs(args, fn_spec): keyword = key elif len(key) == 1: # This may be a shortcut flag. - matching_fn_args = [arg for arg in fn_args if arg[0] == key] - if len(matching_fn_args) == 1: - keyword = matching_fn_args[0] - elif len(matching_fn_args) > 1: - raise FireError( - f"The argument '{argument}' is ambiguous as it could " - f"refer to any of the following arguments: {matching_fn_args}" - ) + # Do not expand single-character flags that collide with Fire's own + # reserved flags (-h/--help, -i/--interactive, -v/--verbose, + # -t/--trace). Those should only be consumed via the '-- --flag' + # separator syntax so that they are not silently hijacked by a user + # function argument whose name starts with the same letter. + if key not in _FIRE_RESERVED_SINGLE_CHAR_FLAGS: + matching_fn_args = [arg for arg in fn_args if arg[0] == key] + if len(matching_fn_args) == 1: + keyword = matching_fn_args[0] + elif len(matching_fn_args) > 1: + raise FireError( + f"The argument '{argument}' is ambiguous as it could " + f"refer to any of the following arguments: {matching_fn_args}" + ) # Determine the value. if not keyword: diff --git a/fire/core_test.py b/fire/core_test.py index f48d6e2d..f77f74f8 100644 --- a/fire/core_test.py +++ b/fire/core_test.py @@ -107,6 +107,13 @@ def testHelpWithNamespaceCollision(self): with self.assertOutputMatches(stdout='False', stderr=None): core.Fire(tc.function_with_help, command=['False']) + def testHelpFlagNotConsumedAsShortcutForHArg(self): + # -h must show help, not be silently expanded to --headless=True. + # Before the fix, running with '-h' would set headless=True instead of + # showing help because 'headless' starts with 'h'. + with self.assertRaisesFireExit(0, 'INFO:.*SYNOPSIS.*headless'): + core.Fire(tc.function_with_headless, command=['-h']) + def testInvalidParameterRaisesFireExit(self): with self.assertRaisesFireExit(2, 'runmisspelled'): core.Fire(tc.Kwargs, command=['props', '--a=1', '--b=2', 'runmisspelled']) diff --git a/fire/test_components.py b/fire/test_components.py index 887a0dc6..da57cd09 100644 --- a/fire/test_components.py +++ b/fire/test_components.py @@ -43,6 +43,11 @@ def function_with_help(help=True): # pylint: disable=redefined-builtin return help +def function_with_headless(headless=False): + """A function whose argument starts with 'h', used to test -h collision.""" + return headless + + class Empty: pass