diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d7c470b..eb449a5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 4.2.2 (August 25, 2026) + +- Documentation Improvements + - Improved documentation in attempt at making some recommended best-practices more discoverable + - Fixed the broken `cmd_as_argument` example + ## 4.2.1 (August 22, 2026) - Enhancements @@ -1894,7 +1900,7 @@ time reading the [rich documentation](https://rich.readthedocs.io/). - **with_argument_list** decorator to change argument type from str to List[str] - **do\_\*** commands get a single argument which is a list of strings, as pre-parsed by shlex.split() - - **with_arparser** decorator for strict argparse-based argument parsing of command + - **with_argparser** decorator for strict argparse-based argument parsing of command arguments - **do\_\*** commands get a single argument which is the output of argparse.parse_args() - **with_argparser_and_unknown_args** decorator for argparse-based argument parsing, but diff --git a/docs/features/os.md b/docs/features/os.md index 114e38dec..444822fd6 100644 --- a/docs/features/os.md +++ b/docs/features/os.md @@ -75,25 +75,25 @@ Either of these methods will launch your program and enter the `cmd2` command lo user to enter commands, which are then executed by your program. You may want to execute commands in your program without prompting the user for any input. There are -several ways you might accomplish this task. The easiest one is to pipe commands and their arguments -into your program via standard input. You don't need to do anything to your program in order to use -this technique. Here's a demonstration using the `examples/cmd_as_argument.py` included in the -source code of `cmd2`: +several ways you might accomplish this task. One is to pipe commands and their arguments into your +program via standard input. You don't need to do anything to your program in order to use this +technique. Here's a demonstration using the `examples/cmd_as_argument.py` included in the source +code of `cmd2`: - $ echo "speak -p some words" | python examples/cmd_as_argument.py + $ echo "speak -p some words" | uv run examples/cmd_as_argument.py omesay ordsway Using this same approach you could create a text file containing the commands you would like to run, one command per line in the file. Say your file was called `somecmds.txt`. To run the commands in the text file using your `cmd2` program (from a Windows command prompt): - c:\cmd2> type somecmds.txt | python.exe examples/cmd_as_argument.py + c:\cmd2> type somecmds.txt | uv run examples/cmd_as_argument.py omesay ordsway By default, `cmd2` programs also look for commands passed as arguments from the operating system shell, and execute those commands before entering the command loop: - $ python examples/cmd_as_argument.py help + $ uv run examples/cmd_as_argument.py help Documented Commands ─────────────────── @@ -105,35 +105,23 @@ shell, and execute those commands before entering the command loop: You may need more control over command line arguments passed from the operating system shell. For example, you might have a command inside your `cmd2` program which itself accepts arguments, and maybe even option strings. Say you wanted to run the `speak` command from the operating system -shell, but have it say it in pig latin: - - $ python examples/cmd_as_argument.py speak -p hello there - python cmd_as_argument.py speak -p hello there - usage: speak [-h] [-p] [-s] [-r REPEAT] words [words ...] - speak: error: the following arguments are required: words - *** Unknown syntax: -p - *** Unknown syntax: hello - *** Unknown syntax: there - (Cmd) - -Uh-oh, that's not what we wanted. `cmd2` treated `-p`, `hello`, and `there` as commands, which don't -exist in that program, thus the syntax errors. - -There is an easy way around this, which is demonstrated in -[cmd_as_argument.py](https://github.com/python-cmd2/cmd2/blob/main/examples/cmd_as_argument.py) -example. By setting `allow_cli_args=False` you can do your own argument parsing of the command line: +shell, but have it say it in pig latin - just group the command and its arguments inside either +double or single quotes: - $ python examples/cmd_as_argument.py speak -p hello there + $ uv run examples/cmd_as_argument.py "speak -p hello there" ellohay heretay + (Cmd) -Check the source code of this example, especially the `main()` function, to see the technique. +If you want to start your application using a custom `argparse` parser to collect high-level +application arguments but still want to be able to pass extra unknown arguments as commands at +invocation, then see the +[argparse_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py) +example. Using this methodology you can call it like so: -Alternatively you can simply wrap the command plus arguments in quotes (either single or double -quotes): + $ uv run examples/argparse_example.py -c blue help - $ python examples/cmd_as_argument.py "speak -p hello there" - ellohay heretay - (Cmd) +Check the source code of this example, especially the `if __name__ == "__main__":` block, to see the +technique. ### Automating cmd2 apps from other CLI/CLU tools @@ -150,13 +138,13 @@ This is easily achieved by combining the following capabilities of `cmd2`: Here is a simple example which doesn't require the quit command since the custom `exit` command quits while returning an exit code: - $ python examples/exit_code.py "exit 23" + $ uv run examples/exit_code.py "exit 23" 'examples/exit_code.py' exiting with code: 23 $ echo $? 23 Here is another example using `quit`: - $ python examples/cmd_as_argument.py "speak -p hello there" quit + $ uv run examples/cmd_as_argument.py "speak -p hello there" quit ellohay heretay $ diff --git a/docs/features/startup_commands.md b/docs/features/startup_commands.md index 7bf65f4dc..fc44d505c 100644 --- a/docs/features/startup_commands.md +++ b/docs/features/startup_commands.md @@ -11,12 +11,12 @@ initializing so they are guaranteed to run before any _Commands At Invocation_. ## Commands At Invocation -You can send commands to your app as you invoke it by including them as extra arguments to the -program. `cmd2` interprets each argument as a separate command, so you should enclose each command -in quotation marks if it is more than a one-word command. You can use either single or double quotes -for this purpose. +By default, users can send commands to your app as you invoke it by including them as extra +arguments to the program. `cmd2` interprets each argument as a separate command, so you should +enclose each command in quotation marks if it is more than a one-word command. You can use either +single or double quotes for this purpose. - $ python examples/cmd_as_argument.py "say hello" "say Gracie" quit + $ uv run examples/cmd_as_argument.py "say hello" "say Gracie" quit hello Gracie @@ -26,7 +26,9 @@ application and easily used in automation. !!! note - If you wish to disable cmd2's consumption of command-line arguments, you can do so by setting the `allow_cli_args` argument of your [cmd2.Cmd][] class instance to `False`. This would be useful, for example, if you wish to use something like [argparse](https://docs.python.org/3/library/argparse.html) to parse the overall command line arguments for your application: + If you wish to disable cmd2's consumption of command-line arguments, you can do so by setting the `allow_cli_args` argument of your [cmd2.Cmd][] class instance to `False`. + This would be useful, for example, if you wish to use something like [argparse](https://docs.python.org/3/library/argparse.html) to parse the overall command line arguments + for your application: ```py from cmd2 import Cmd @@ -35,6 +37,17 @@ application and easily used in automation. super().__init__(allow_cli_args=False) ``` +!!! tip + + If you want to use something like [argparse](https://docs.python.org/3/library/argparse.html) to parse the overall command line arguments for your application + but still want to be able to pass any extra arguments to your application as commands, then see the `if __name__ == "__main__":` block at the end of the + [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py) example. + + This can be run like so: + ```sh + uv run examples/argparse_example.py -c blue help quit + ``` + ## Startup Script You can execute commands from an initialization script by passing a file path to the diff --git a/examples/README.md b/examples/README.md index 46ba97f23..3c85b03df 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,6 +17,8 @@ each: - Comprehensive example demonstrating various aspects of using [argparse](https://docs.python.org/3/library/argparse.html) for command argument processing via the `cmd2.with_argparser` decorator + - Also demonstrates how to use a custom `argparse` parser when invoking your application and + still pass commands at invocation - [async_call.py](https://github.com/python-cmd2/cmd2/blob/main/examples/async_call.py) - Shows how to make a call to an async function from a cmd2 command. - [async_commands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/async_commands.py) @@ -26,8 +28,8 @@ each: title - [basic_completion.py](https://github.com/python-cmd2/cmd2/blob/main/examples/basic_completion.py) - Show how to enable custom tab completion by assigning a completer function to `do_*` commands -- [cmd2_as_argument.py](https://github.com/python-cmd2/cmd2/blob/main/examples/cmd_as_argument.py) - - Demonstrates how to accept and parse command-line arguments when invoking a cmd2 application +- [cmd_as_argument.py](https://github.com/python-cmd2/cmd2/blob/main/examples/cmd_as_argument.py) + - Demonstrates how to accept command-line arguments when invoking a cmd2 application - [color.py](https://github.com/python-cmd2/cmd2/blob/main/examples/color.py) - Show the numerous colors available to use in your cmd2 applications - [command_sets.py](https://github.com/python-cmd2/cmd2/blob/main/examples/command_sets.py) diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 39c3189c2..2899970fd 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -1,13 +1,17 @@ #!/usr/bin/env python """A sample application for cmd2. -This example has additional code in main() that shows how to accept a command from -the command line at invocation: +This example relies on the `allow_cli_args` init parameter being `True` by default which allows passing commands on the +command line to execute when the application is invoked. -$ python cmd_as_argument.py speak -p hello there +This can be run like so: +$ python cmd_as_argument.py "speak -p hello there" help + +By default, the application will enter the interactive shell mode after executing the commands passed in on the command line. +You can have it exit after executing by providing `quit` as the last command. +Commands and arguments can be grouped together by including in quotes. """ -import argparse import secrets import cmd2 @@ -24,7 +28,7 @@ def __init__(self) -> None: shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) shortcuts.update({"&": "speak"}) # Set include_ipy to True to enable the "ipy" command which runs an interactive IPython shell - super().__init__(allow_cli_args=False, include_ipy=True, multiline_commands=["orate"], shortcuts=shortcuts) + super().__init__(allow_cli_args=True, include_ipy=True, multiline_commands=["orate"], shortcuts=shortcuts) self.self_in_py = True self.maxrepeats = 3 @@ -45,8 +49,8 @@ def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for w in args.words: - word = w.copy() - if args.piglatin: + word = w.strip() + if args.piglatin and word: word = f"{word[1:]}{word[0]}ay" if args.shout: word = word.upper() @@ -80,30 +84,8 @@ def do_mumble(self, args) -> None: self.poutput(" ".join(output)) -def main(argv=None): - """Run when invoked from the operating system shell.""" - parser = cmd2.Cmd2ArgumentParser(description="Commands as arguments") - command_help = "optional command to run, if no command given, enter an interactive shell" - parser.add_argument("command", nargs="?", help=command_help) - arg_help = "optional arguments for command" - parser.add_argument("command_args", nargs=argparse.REMAINDER, help=arg_help) - - args = parser.parse_args(argv) - - c = CmdLineApp() - - sys_exit_code = 0 - if args.command: - # we have a command, run it and then exit - c.onecmd_plus_hooks("{} {}".format(args.command, " ".join(args.command_args))) - else: - # we have no command, drop into interactive mode - sys_exit_code = c.cmdloop() - - return sys_exit_code - - if __name__ == "__main__": import sys - sys.exit(main()) + app = CmdLineApp() + sys.exit(app.cmdloop())