From f5ca5369d104c131996d72888dbd2f4ae82f6141 Mon Sep 17 00:00:00 2001 From: Lakhan Yelgate <39281685+lyelgate@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:03:39 +0530 Subject: [PATCH] Support help as a command or action, like --help `linode-cli help linodes` and `linode-cli linodes help` failed with "Unrecognized command help" and "Action not found for command linodes: help". Treat `help` given as the command or the action like the --help flag, so both show the same help as `linode-cli linodes --help`; `linode-cli help` shows the default help and `linode-cli help linodes create` the help for an action. Only the command and action positions are rewritten, since `help` elsewhere can be a value, such as a bucket name in `linode-cli obj mb help`. argv is changed in place, so plugins, which read it directly, see the --help flag too. Fixes #868 --- linodecli/__init__.py | 15 ++++++-- linodecli/arg_helpers.py | 30 ++++++++++++++-- tests/unit/test_arg_helpers.py | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/linodecli/__init__.py b/linodecli/__init__.py index b7283b9f7..07e0e00da 100755 --- a/linodecli/__init__.py +++ b/linodecli/__init__.py @@ -16,7 +16,12 @@ from linodecli import plugins from linodecli.exit_codes import ExitCodes -from .arg_helpers import register_args, register_plugin, remove_plugin +from .arg_helpers import ( + help_subcommand_to_flag, + register_args, + register_plugin, + remove_plugin, +) from .cli import CLI from .completion import get_completions from .configuration import ENV_TOKEN_NAME @@ -68,7 +73,13 @@ def main(): # pylint: disable=too-many-branches,too-many-statements formatter_class=SortingHelpFormatter, description="The Linode Command Line Interface.\n\nAliases: lin, linode", ) - parsed, args = register_args(parser).parse_known_args() + parser = register_args(parser) + parsed, args = parser.parse_known_args() + + # argv is changed in place so that plugins, which read it directly, + # also see the --help flag + if help_subcommand_to_flag(argv, parsed): + parsed, args = parser.parse_known_args() cli.output_handler.configure(parsed, cli.suppress_warnings) diff --git a/linodecli/arg_helpers.py b/linodecli/arg_helpers.py index 2a6d7c9a0..5180f8532 100644 --- a/linodecli/arg_helpers.py +++ b/linodecli/arg_helpers.py @@ -6,10 +6,10 @@ """ import sys -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from configparser import ConfigParser from importlib import import_module -from typing import Dict, Tuple +from typing import Dict, List, Tuple from linodecli import plugins from linodecli.helpers import ( @@ -100,6 +100,32 @@ def register_args(parser: ArgumentParser) -> ArgumentParser: # TODO: maybe move to plugins/__init__.py +def help_subcommand_to_flag(args: List[str], parsed: Namespace) -> bool: + """ + Replaces `help` given as the command or the action with the --help flag, + modifying args in place, so `linode-cli help linodes` and + `linode-cli linodes help` work like `linode-cli linodes --help`. + + `help` in any other position is left alone, since it can be a value, + such as a bucket name. + + :param args: The full argument list, including the program name. + :param parsed: The arguments parsed from args. + + :returns: Whether args was changed. + """ + if parsed.command == "help": + position = args.index("help") + elif parsed.command is not None and parsed.action == "help": + position = args.index("help", args.index(parsed.command) + 1) + else: + return False + + del args[position] + args.append("--help") + return True + + def register_plugin( module: str, config: ConfigParser, ops: Dict[str, str] ) -> Tuple[str, int]: diff --git a/tests/unit/test_arg_helpers.py b/tests/unit/test_arg_helpers.py index 3743061c2..09c669a30 100644 --- a/tests/unit/test_arg_helpers.py +++ b/tests/unit/test_arg_helpers.py @@ -1,5 +1,9 @@ #!/usr/local/bin/python3 +import argparse + +import pytest + from linodecli import arg_helpers @@ -149,3 +153,62 @@ def test_remove_plugin_not_available(self, mocked_config): msg, code = arg_helpers.remove_plugin("testing.plugin", mocked_config) assert "not a registered plugin" in msg assert code == 14 + + +def _rewrite_help(args): + parser = arg_helpers.register_args( + argparse.ArgumentParser("linode-cli", add_help=False) + ) + parsed, _ = parser.parse_known_args(args[1:]) + changed = arg_helpers.help_subcommand_to_flag(args, parsed) + return args, changed + + +@pytest.mark.parametrize( + "args, expected", + [ + (["linode-cli", "help"], ["linode-cli", "--help"]), + ( + ["linode-cli", "help", "linodes"], + ["linode-cli", "linodes", "--help"], + ), + ( + ["linode-cli", "help", "linodes", "create"], + ["linode-cli", "linodes", "create", "--help"], + ), + ( + ["linode-cli", "linodes", "help"], + ["linode-cli", "linodes", "--help"], + ), + ( + ["linode-cli", "--json", "linodes", "help"], + ["linode-cli", "--json", "linodes", "--help"], + ), + (["linode-cli", "obj", "help"], ["linode-cli", "obj", "--help"]), + ], +) +def test_help_subcommand_to_flag(args, expected): + original = args + args, changed = _rewrite_help(args) + + assert changed + assert args == expected + # argv must be changed in place, as linodecli holds a reference to it + assert args is original + + +@pytest.mark.parametrize( + "args", + [ + ["linode-cli", "linodes", "list"], + ["linode-cli", "linodes", "view", "help"], + ["linode-cli", "obj", "mb", "help"], + ["linode-cli", "--help"], + ], +) +def test_help_subcommand_to_flag_leaves_values_alone(args): + expected = list(args) + args, changed = _rewrite_help(args) + + assert not changed + assert args == expected