From d4b5f8b946f82de1b95a68eb99925761d423cb45 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 16:46:35 -0400 Subject: [PATCH 1/8] Fix broken example and improve some documentation - Fix broken `cmd_as_argument.py` example whose `speak` command was crashing because `str` has no `copy` method since strings are immutable in Python. - Update the startup_commands.md documentation to make it more discoverable what the recommended solution is for mixing commands at invocation with a custom argparse parser. --- docs/features/startup_commands.md | 25 +++++++++++++----- examples/cmd_as_argument.py | 42 +++++++++---------------------- 2 files changed, 31 insertions(+), 36 deletions(-) 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/cmd_as_argument.py b/examples/cmd_as_argument.py index 39c3189c2..38c4f73b6 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 reliese 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,7 +49,7 @@ def do_speak(self, args) -> None: """Repeats what you tell me to.""" words = [] for w in args.words: - word = w.copy() + word = w.strip() if args.piglatin: word = f"{word[1:]}{word[0]}ay" if args.shout: @@ -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()) From d29b5143c5c66194eb66b5725d07ae7247811dae Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 17:04:02 -0400 Subject: [PATCH 2/8] Fix typo --- examples/cmd_as_argument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 38c4f73b6..e3b66e8c8 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """A sample application for cmd2. -This example reliese on the `allow_cli_args` init parameter being `True` by default which allows passing commands on the +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. This can be run like so: From 396e9a169e05329416abbe3fa93986139b1681d4 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 17:33:56 -0400 Subject: [PATCH 3/8] Fixed outdated documentation in os.md --- docs/features/os.md | 54 ++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/docs/features/os.md b/docs/features/os.md index 114e38dec..fe1d95cf4 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 `arparse` 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 $ From a8f61f8d4094d96351c4b20336f3395a972a01ba Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 17:45:50 -0400 Subject: [PATCH 4/8] Fix argparse -> argparse typo --- CHANGELOG.md | 2 +- docs/features/os.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d7c470b..617f4a961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1894,7 +1894,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 fe1d95cf4..883952a6e 100644 --- a/docs/features/os.md +++ b/docs/features/os.md @@ -112,7 +112,7 @@ double or single quotes: ellohay heretay (Cmd) -If you want to start your application using a custom `arparse` parser to collect high-level +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) From 7a9d945f3b1122db4c9a6a48b516088a7f49f453 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 17:55:37 -0400 Subject: [PATCH 5/8] Fix `cmd2_as_argument.py` typo -> `cmd_as_argument.py` Also: - Make one feature of `argparse_example.py` more discoverable --- examples/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index 46ba97f23..d037800f0 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 `arparse` 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) From e0067f6ff359c52e692e5e60c88f8b08048d2738 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 18:18:14 -0400 Subject: [PATCH 6/8] Fix another typo --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index d037800f0..3c85b03df 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,7 +17,7 @@ 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 `arparse` parser when invoking your application and + - 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. From c9cd13b6c87a9890af049c8ba923df9abd917535 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 18:28:49 -0400 Subject: [PATCH 7/8] Updated changelog with notes for patch release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 617f4a961..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 From 4bc36741a1c94477ff0f660c19bde4a5ccc4486c Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Aug 2026 18:39:43 -0400 Subject: [PATCH 8/8] Fix an edge case in cmd_as_argument example for empty string after strip Also: - Make some documentation more consistent with the example it was referring to --- docs/features/os.md | 2 +- examples/cmd_as_argument.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/os.md b/docs/features/os.md index 883952a6e..444822fd6 100644 --- a/docs/features/os.md +++ b/docs/features/os.md @@ -120,7 +120,7 @@ example. Using this methodology you can call it like so: $ uv run examples/argparse_example.py -c blue help -Check the source code of this example, especially the `if __name__ == '__main__':` block, to see the +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 diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index e3b66e8c8..2899970fd 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -50,7 +50,7 @@ def do_speak(self, args) -> None: words = [] for w in args.words: word = w.strip() - if args.piglatin: + if args.piglatin and word: word = f"{word[1:]}{word[0]}ay" if args.shout: word = word.upper()