From f08038953db3a163cdbf87cf0342a3bf2d078370 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 21 Aug 2025 12:35:53 -0400 Subject: [PATCH 01/13] Updated color.py and argparse_completion.py examples for cmd2 3.0. --- examples/argparse_completion.py | 11 ++++++++--- examples/color.py | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index 961c720ac..52f5972e0 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -3,11 +3,13 @@ import argparse +from rich.style import Style from rich.text import Text from cmd2 import ( Cmd, Cmd2ArgumentParser, + Color, CompletionError, CompletionItem, with_argparser, @@ -18,8 +20,8 @@ class ArgparseCompletion(Cmd): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) + def __init__(self) -> None: + super().__init__(include_ipy=True) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] def choices_provider(self) -> list[str]: @@ -39,7 +41,10 @@ def choices_completion_error(self) -> list[str]: def choices_completion_item(self) -> list[CompletionItem]: """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" - fancy_item = Text("These things can\ncontain newlines and\n") + Text("styled text!!", style="underline bright_yellow") + fancy_item = Text.assemble( + "These things can\ncontain newlines and\n", + Text("styled text!!", style=Style(color=Color.BRIGHT_YELLOW, underline=True)), + ) items = {1: "My item", 2: "Another item", 3: "Yet another item", 4: fancy_item} return [CompletionItem(item_id, [description]) for item_id, description in items.items()] diff --git a/examples/color.py b/examples/color.py index c9cd65b26..e6e2cf26b 100755 --- a/examples/color.py +++ b/examples/color.py @@ -7,12 +7,10 @@ import argparse from rich.style import Style +from rich.text import Text import cmd2 -from cmd2 import ( - Color, - stylize, -) +from cmd2 import Color class CmdLineApp(cmd2.Cmd): @@ -31,17 +29,19 @@ def __init__(self) -> None: def do_taste_the_rainbow(self, args: argparse.Namespace) -> None: """Show all of the colors available within cmd2's Color StrEnum class.""" - color_names = [] - for color_member in Color: - style = Style(bgcolor=color_member) if args.background else Style(color=color_member) - styled_name = stylize(color_member.name, style=style) - if args.paged: - color_names.append(styled_name) - else: - self.poutput(styled_name) + def create_style(color: Color) -> Style: + """Create a foreground or background color Style.""" + if args.background: + return Style(bgcolor=color) + return Style(color=color) + + styled_names = [Text(color.name, style=create_style(color)) for color in Color] + output = Text("\n").join(styled_names) if args.paged: - self.ppaged('\n'.join(color_names)) + self.ppaged(output) + else: + self.poutput(output) if __name__ == '__main__': From df217e52265e7ddcb4742b3d5659992128e12627 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 21 Aug 2025 13:32:47 -0400 Subject: [PATCH 02/13] Added a table to a CompletionItem description in argparse_completion.py example. --- examples/argparse_completion.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py index 52f5972e0..8a67cc3c6 100755 --- a/examples/argparse_completion.py +++ b/examples/argparse_completion.py @@ -3,12 +3,15 @@ import argparse +from rich.box import SIMPLE_HEAD from rich.style import Style +from rich.table import Table from rich.text import Text from cmd2 import ( Cmd, Cmd2ArgumentParser, + Cmd2Style, Color, CompletionError, CompletionItem, @@ -46,7 +49,17 @@ def choices_completion_item(self) -> list[CompletionItem]: Text("styled text!!", style=Style(color=Color.BRIGHT_YELLOW, underline=True)), ) - items = {1: "My item", 2: "Another item", 3: "Yet another item", 4: fancy_item} + table_item = Table("Left Column", "Right Column", box=SIMPLE_HEAD, border_style=Cmd2Style.RULE_LINE) + table_item.add_row("Yes, it's true.", "CompletionItems can") + table_item.add_row("even display description", "data in tables!") + + items = { + 1: "My item", + 2: "Another item", + 3: "Yet another item", + 4: fancy_item, + 5: table_item, + } return [CompletionItem(item_id, [description]) for item_id, description in items.items()] def choices_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]: From 6219f7624d115ed9909d62ad5155785474845fbb Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 21 Aug 2025 13:36:01 -0400 Subject: [PATCH 03/13] Updated documentation for Cmd2Style. --- cmd2/styles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/styles.py b/cmd2/styles.py index 57b786069..77a894d12 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -34,7 +34,7 @@ class Cmd2Style(StrEnum): EXAMPLE = "cmd2.example" # Command line examples in help text HELP_HEADER = "cmd2.help.header" # Help table header text HELP_LEADER = "cmd2.help.leader" # Text right before the help tables are listed - RULE_LINE = "rule.line" # Rich style for horizontal rules + RULE_LINE = "rule.line" # Built-in Rich style for horizontal rules SUCCESS = "cmd2.success" # Success text (used by psuccess()) WARNING = "cmd2.warning" # Warning text (used by pwarning()) From e0d69f70cd7679dcd9c944713f1b076682f19588 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 21 Aug 2025 14:34:19 -0400 Subject: [PATCH 04/13] Updated initialization.py and python_scripting.py examples for cmd2 3.0. Deleted basic.py and pirate.py examples. --- .github/CODEOWNERS | 7 +- .github/CONTRIBUTING.md | 2 +- README.md | 4 -- docs/features/generating_output.md | 17 +---- docs/features/initialization.md | 65 +++++++++++-------- docs/features/prompt.md | 12 ++-- docs/migrating/next_steps.md | 2 +- examples/README.md | 6 -- examples/basic.py | 51 --------------- examples/initialization.py | 36 +++++++---- examples/pirate.py | 100 ----------------------------- examples/python_scripting.py | 14 ++-- 12 files changed, 87 insertions(+), 229 deletions(-) delete mode 100755 examples/basic.py delete mode 100755 examples/pirate.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f1fd9c52..3e963a9ff 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,10 +27,10 @@ # cmd2 code cmd2/__init__.py @kmvanbrunt @tleonhardt -cmd2/ansi.py @kmvanbrunt @tleonhardt cmd2/argparse_*.py @kmvanbrunt @anselor cmd2/clipboard.py @tleonhardt cmd2/cmd2.py @tleonhardt @kmvanbrunt +cmd2/colors.py @tleonhardt @kmvanbrunt cmd2/command_definition.py @anselor cmd2/constants.py @tleonhardt @kmvanbrunt cmd2/decorators.py @kmvanbrunt @anselor @@ -39,8 +39,11 @@ cmd2/history.py @tleonhardt cmd2/parsing.py @kmvanbrunt cmd2/plugin.py @anselor cmd2/py_bridge.py @kmvanbrunt +cmd2/rich_utils.py @kmvanbrunt cmd2/rl_utils.py @kmvanbrunt -cmd2/table_creator.py @kmvanbrunt +cmd2/string_utils.py @kmvanbrunt +cmd2/styles.py @tleonhardt @kmvanbrunt +cmd2/terminal_utils.py @kmvanbrunt cmd2/transcript.py @tleonhardt cmd2/utils.py @tleonhardt @kmvanbrunt diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 875924a7e..7ca430db6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -269,7 +269,7 @@ uv venv --python 3.12 Then you can run commands in this isolated virtual environment using `uv` like so: ```sh -uv run examples/basic.py +uv run examples/hello_cmd2.py ``` Alternatively you can activate the virtual environment using the OS-specific command such as this on diff --git a/README.md b/README.md index 688ed57aa..36601235a 100755 --- a/README.md +++ b/README.md @@ -105,10 +105,6 @@ examples. ## Tutorials -- PyOhio 2019 presentation: - - [video](https://www.youtube.com/watch?v=pebeWrTqIIw) - - [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf) - - [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) - [Cookiecutter](https://github.com/cookiecutter/cookiecutter) Templates from community - Basic cookiecutter template for cmd2 application : https://github.com/jayrod/cookiecutter-python-cmd2 diff --git a/docs/features/generating_output.md b/docs/features/generating_output.md index 9fc1f7f10..beed0c1c0 100644 --- a/docs/features/generating_output.md +++ b/docs/features/generating_output.md @@ -126,21 +126,6 @@ you can pad it appropriately with spaces. However, there are categories of Unico occupy 2 cells, and other that occupy 0. To further complicate matters, you might have included ANSI escape sequences in the output to generate colors on the terminal. -The `cmd2.ansi.style_aware_wcswidth` function solves both of these problems. Pass it a string, and +The `cmd2.string_utils.str_width` function solves both of these problems. Pass it a string, and regardless of which Unicode characters and ANSI text style escape sequences it contains, it will tell you how many characters on the screen that string will consume when printed. - -## Pretty Printing Data Structures - -The `cmd2.Cmd.ppretty` method is similar to the Python -[pprint](https://docs.python.org/3/library/pprint.html) function from the standard `pprint` module. -`cmd2.Cmd.pprint` adds the same conveniences as `cmd2.Cmd.poutput`. - -This method provides a capability to “pretty-print” arbitrary Python data structures in a form which -can be used as input to the interpreter and is easy for humans to read. - -The formatted representation keeps objects on a single line if it can, and breaks them onto multiple -lines if they don’t fit within the allowed width, adjustable by the width parameter defaulting to 80 -characters. - -Dictionaries are sorted by key before the display is computed. diff --git a/docs/features/initialization.md b/docs/features/initialization.md index 478a2eb22..e8a141e81 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -4,31 +4,32 @@ Here is a basic example `cmd2` application which demonstrates many capabilities ```py #!/usr/bin/env python3 - # coding=utf-8 """A simple example cmd2 application demonstrating the following: - 1) Colorizing/stylizing output - 2) Using multiline commands - 3) Persistent history - 4) How to run an initialization script at startup - 5) How to group and categorize commands when displaying them in help - 6) Opting-in to using the ipy command to run an IPython shell - 7) Allowing access to your application in py and ipy - 8) Displaying an intro banner upon starting your application - 9) Using a custom prompt - 10) How to make custom attributes settable at runtime + 1) Colorizing/stylizing output + 2) Using multiline commands + 3) Persistent history + 4) How to run an initialization script at startup + 5) How to group and categorize commands when displaying them in help + 6) Opting-in to using the ipy command to run an IPython shell + 7) Allowing access to your application in py and ipy + 8) Displaying an intro banner upon starting your application + 9) Using a custom prompt + 10) How to make custom attributes settable at runtime. """ + + from rich.style import Style + import cmd2 from cmd2 import ( - Bg, - Fg, - style, + Color, + stylize, ) class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' - def __init__(self): + def __init__(self) -> None: super().__init__( multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', @@ -37,7 +38,10 @@ Here is a basic example `cmd2` application which demonstrates many capabilities ) # Prints an intro banner once upon application startup - self.intro = style('Welcome to cmd2!', fg=Fg.RED, bg=Bg.WHITE, bold=True) + self.intro = stylize( + 'Welcome to cmd2!', + style=Style(color=Color.RED, bgcolor=Color.WHITE, bold=True), + ) # Show this as the prompt when asking for input self.prompt = 'myapp> ' @@ -52,25 +56,34 @@ Here is a basic example `cmd2` application which demonstrates many capabilities self.default_category = 'cmd2 Built-in Commands' # Color to output text in with echo command - self.foreground_color = Fg.CYAN.name.lower() + self.foreground_color = Color.CYAN.value # Make echo_fg settable at runtime - fg_colors = [c.name.lower() for c in Fg] + fg_colors = [c.value for c in Color] self.add_settable( - cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', self, - choices=fg_colors) + cmd2.Settable( + 'foreground_color', + str, + 'Foreground color to use with echo command', + self, + choices=fg_colors, + ) ) @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _): - """Display the intro banner""" + def do_intro(self, _: cmd2.Statement) -> None: + """Display the intro banner.""" self.poutput(self.intro) @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg): - """Example of a multiline command""" - fg_color = Fg[self.foreground_color.upper()] - self.poutput(style(arg, fg=fg_color)) + def do_echo(self, arg: cmd2.Statement) -> None: + """Example of a multiline command.""" + self.poutput( + stylize( + arg, + style=Style(color=self.foreground_color), + ) + ) if __name__ == '__main__': diff --git a/docs/features/prompt.md b/docs/features/prompt.md index 0ae8b1790..6fbf8f226 100644 --- a/docs/features/prompt.md +++ b/docs/features/prompt.md @@ -6,8 +6,8 @@ This prompt can be configured by setting the `cmd2.Cmd.prompt` instance attribute. This contains the string which should be printed as a prompt for user input. See the -[Pirate](https://github.com/python-cmd2/cmd2/blob/main/examples/pirate.py#L39) example for the -simple use case of statically setting the prompt. +[Initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) example +for the simple use case of statically setting the prompt. ## Continuation Prompt @@ -15,16 +15,16 @@ When a user types a [Multiline Command](./multiline_commands.md) it may span mor input. The prompt for the first line of input is specified by the `cmd2.Cmd.prompt` instance attribute. The prompt for subsequent lines of input is defined by the `cmd2.Cmd.continuation_prompt` attribute.See the -[Initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py#L42) -example for a demonstration of customizing the continuation prompt. +[Initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) example +for a demonstration of customizing the continuation prompt. ## Updating the prompt If you wish to update the prompt between commands, you can do so using one of the [Application Lifecycle Hooks](./hooks.md#application-lifecycle-hooks) such as a [Postcommand hook](./hooks.md#postcommand-hooks). See -[PythonScripting](https://github.com/python-cmd2/cmd2/blob/main/examples/python_scripting.py#L38-L55) -for an example of dynamically updating the prompt. +[PythonScripting](https://github.com/python-cmd2/cmd2/blob/main/examples/python_scripting.py) for an +example of dynamically updating the prompt. ## Asynchronous Feedback diff --git a/docs/migrating/next_steps.md b/docs/migrating/next_steps.md index 7d56e2f4b..892e05c78 100644 --- a/docs/migrating/next_steps.md +++ b/docs/migrating/next_steps.md @@ -41,5 +41,5 @@ to `cmd2.Cmd.poutput`, `cmd2.Cmd.perror`, and `cmd2.Cmd.pfeedback`. These method of the built in [Settings](../features/settings.md) to allow the user to view or suppress feedback (i.e. progress or status output). They also properly handle ansi colored output according to user preference. Speaking of colored output, you can use any color library you want, or use the included -`cmd2.ansi.style` function. These and other related topics are covered in +`cmd2.string_utils.stylize` function. These and other related topics are covered in [Generating Output](../features/generating_output.md). diff --git a/examples/README.md b/examples/README.md index aad2072b1..28937c645 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,9 +25,6 @@ each: - [async_printing.py](https://github.com/python-cmd2/cmd2/blob/main/examples/async_printing.py) - Shows how to asynchronously print alerts, update the prompt in realtime, and change the window title -- [basic.py](https://github.com/python-cmd2/cmd2/blob/main/examples/basic.py) - - Shows how to add a command, add help for it, and create persistent command history for your - application - [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) @@ -81,9 +78,6 @@ each: - Shows how to use output pagination within `cmd2` apps via the `ppaged` method - [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/main/examples/persistent_history.py) - Shows how to enable persistent history in your `cmd2` application -- [pirate.py](https://github.com/python-cmd2/cmd2/blob/main/examples/pirate.py) - - Demonstrates many features including colorized output, multiline commands, shorcuts, - defaulting to shell, etc. - [pretty_print.py](https://github.com/python-cmd2/cmd2/blob/main/examples/pretty_print.py) - Demonstrates use of cmd2.Cmd.ppretty() for pretty-printing arbitrary Python data structures like dictionaries. diff --git a/examples/basic.py b/examples/basic.py deleted file mode 100755 index 20ebe20a5..000000000 --- a/examples/basic.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -"""A simple example demonstrating the following: -1) How to add a command -2) How to add help for that command -3) Persistent history -4) How to run an initialization script at startup -5) How to add custom command aliases using the alias command -6) Shell-like capabilities. -""" - -import cmd2 -from cmd2 import ( - Bg, - Fg, - style, -) - - -class BasicApp(cmd2.Cmd): - CUSTOM_CATEGORY = 'My Custom Commands' - - def __init__(self) -> None: - super().__init__( - multiline_commands=['echo'], - persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', - include_ipy=True, - ) - - self.intro = style('Welcome to PyOhio 2019 and cmd2!', fg=Fg.RED, bg=Bg.WHITE, bold=True) + ' 😀' - - # Allow access to your application in py and ipy via self - self.self_in_py = True - - # Set the default category name - self.default_category = 'cmd2 Built-in Commands' - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _) -> None: - """Display the intro banner.""" - self.poutput(self.intro) - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg) -> None: - """Example of a multiline command.""" - self.poutput(arg) - - -if __name__ == '__main__': - app = BasicApp() - app.cmdloop() diff --git a/examples/initialization.py b/examples/initialization.py index 22de3ff20..bee050405 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -12,11 +12,12 @@ 10) How to make custom attributes settable at runtime. """ +from rich.style import Style + import cmd2 from cmd2 import ( - Bg, - Fg, - style, + Color, + stylize, ) @@ -32,7 +33,10 @@ def __init__(self) -> None: ) # Prints an intro banner once upon application startup - self.intro = style('Welcome to cmd2!', fg=Fg.RED, bg=Bg.WHITE, bold=True) + self.intro = stylize( + 'Welcome to cmd2!', + style=Style(color=Color.RED, bgcolor=Color.WHITE, bold=True), + ) # Show this as the prompt when asking for input self.prompt = 'myapp> ' @@ -47,24 +51,34 @@ def __init__(self) -> None: self.default_category = 'cmd2 Built-in Commands' # Color to output text in with echo command - self.foreground_color = Fg.CYAN.name.lower() + self.foreground_color = Color.CYAN.value # Make echo_fg settable at runtime - fg_colors = [c.name.lower() for c in Fg] + fg_colors = [c.value for c in Color] self.add_settable( - cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', self, choices=fg_colors) + cmd2.Settable( + 'foreground_color', + str, + 'Foreground color to use with echo command', + self, + choices=fg_colors, + ) ) @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _) -> None: + def do_intro(self, _: cmd2.Statement) -> None: """Display the intro banner.""" self.poutput(self.intro) @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg) -> None: + def do_echo(self, arg: cmd2.Statement) -> None: """Example of a multiline command.""" - fg_color = Fg[self.foreground_color.upper()] - self.poutput(style(arg, fg=fg_color)) + self.poutput( + stylize( + arg, + style=Style(color=self.foreground_color), + ) + ) if __name__ == '__main__': diff --git a/examples/pirate.py b/examples/pirate.py deleted file mode 100755 index b15dae4f6..000000000 --- a/examples/pirate.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -"""This example is adapted from the pirate8.py example created by Catherine Devlin and -presented as part of her PyCon 2010 talk. - -It demonstrates many features of cmd2. -""" - -import cmd2 -from cmd2 import ( - Fg, -) -from cmd2.constants import ( - MULTILINE_TERMINATOR, -) - -color_choices = [c.name.lower() for c in Fg] - - -class Pirate(cmd2.Cmd): - """A piratical example cmd2 application involving looting and drinking.""" - - def __init__(self) -> None: - """Initialize the base class as well as this one.""" - shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) - shortcuts.update({'~': 'sing'}) - super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts) - - self.default_to_shell = True - self.songcolor = 'blue' - - # Make songcolor settable at runtime - self.add_settable(cmd2.Settable('songcolor', str, 'Color to ``sing``', self, choices=color_choices)) - - # prompts and defaults - self.gold = 0 - self.initial_gold = self.gold - self.prompt = 'arrr> ' - - def precmd(self, line): - """Runs just before a command line is parsed, but after the prompt is presented.""" - self.initial_gold = self.gold - return line - - def postcmd(self, stop, _line): - """Runs right before a command is about to return.""" - if self.gold != self.initial_gold: - self.poutput(f'Now we gots {self.gold} doubloons') - if self.gold < 0: - self.poutput("Off to debtorrr's prison.") - self.exit_code = 1 - stop = True - return stop - - def do_loot(self, _arg) -> None: - """Seize booty from a passing ship.""" - self.gold += 1 - - def do_drink(self, arg) -> None: - """Drown your sorrrows in rrrum. - - drink [n] - drink [n] barrel[s] o' rum. - """ - try: - self.gold -= int(arg) - except ValueError: - if arg: - self.poutput(f'''What's "{arg}"? I'll take rrrum.''') - self.gold -= 1 - - def do_quit(self, _arg) -> bool: - """Quit the application gracefully.""" - self.poutput("Quiterrr!") - return True - - def do_sing(self, arg) -> None: - """Sing a colorful song.""" - self.poutput(cmd2.ansi.style(arg, fg=Fg[self.songcolor.upper()])) - - yo_parser = cmd2.Cmd2ArgumentParser() - yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'") - yo_parser.add_argument('-c', '--commas', action='store_true', help='Intersperse commas') - yo_parser.add_argument('beverage', help='beverage to drink with the chant') - - @cmd2.with_argparser(yo_parser) - def do_yo(self, args) -> None: - """Compose a yo-ho-ho type chant with flexible options.""" - chant = ['yo'] + ['ho'] * args.ho - separator = ', ' if args.commas else ' ' - chant = separator.join(chant) - self.poutput(f'{chant} and a bottle of {args.beverage}') - - -if __name__ == '__main__': - import sys - - # Create an instance of the Pirate derived class and enter the REPL with cmdloop(). - pirate = Pirate() - sys_exit_code = pirate.cmdloop() - print(f'Exiting with code: {sys_exit_code!r}') - sys.exit(sys_exit_code) diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 393e31fdd..0e5c6fc61 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -20,10 +20,14 @@ example for one way in which this can be done. """ +import argparse import os import cmd2 -from cmd2 import ansi +from cmd2 import ( + Color, + stylize, +) class CmdLineApp(cmd2.Cmd): @@ -38,7 +42,7 @@ def __init__(self) -> None: def _set_prompt(self) -> None: """Set prompt so it displays the current working directory.""" self.cwd = os.getcwd() - self.prompt = ansi.style(f'{self.cwd} $ ', fg=ansi.Fg.CYAN) + self.prompt = stylize(f'{self.cwd} $ ', style=Color.CYAN) def postcmd(self, stop: bool, _line: str) -> bool: """Hook method executed just after a command dispatch is finished. @@ -52,7 +56,7 @@ def postcmd(self, stop: bool, _line: str) -> bool: return stop @cmd2.with_argument_list - def do_cd(self, arglist) -> None: + def do_cd(self, arglist: list[str]) -> None: """Change directory. Usage: cd . @@ -88,7 +92,7 @@ def do_cd(self, arglist) -> None: self.last_result = data # Enable tab completion for cd command - def complete_cd(self, text, line, begidx, endidx): + def complete_cd(self, text: str, line: str, begidx: int, endidx: int) -> list[str]: # Tab complete only directories return self.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) @@ -96,7 +100,7 @@ def complete_cd(self, text, line, begidx, endidx): dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") @cmd2.with_argparser(dir_parser, with_unknown_args=True) - def do_dir(self, _args, unknown) -> None: + def do_dir(self, _args: argparse.Namespace, unknown: list[str]) -> None: """List contents of current directory.""" # No arguments for this command if unknown: From 829c7b9a12b78ea5fba51671e82b402c9e4a4c6c Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 21 Aug 2025 15:07:54 -0400 Subject: [PATCH 05/13] Fix typo and slightly re-order a couple things in top-level README --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 36601235a..a21bb1ed9 100755 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ menagerie of simple command line tools created by strangers on github and the gu Unfortunately, when CLIs become significantly complex the ease of command discoverability tends to fade quickly. On the other hand, Web and traditional desktop GUIs are first in class when it comes to easily discovering functionality. The price we pay for beautifully colored displays is complexity -required to aggregate disperate applications into larger systems. `cmd2` fills the niche between +required to aggregate disparate applications into larger systems. `cmd2` fills the niche between high [ease of command discovery](https://clig.dev/#ease-of-discovery) applications and smart workflow automation systems. @@ -105,16 +105,16 @@ examples. ## Tutorials -- [Cookiecutter](https://github.com/cookiecutter/cookiecutter) Templates from community - - Basic cookiecutter template for cmd2 application : - https://github.com/jayrod/cookiecutter-python-cmd2 - - Advanced cookiecutter template with external plugin support : - https://github.com/jayrod/cookiecutter-python-cmd2-ext-plug - [cmd2 example applications](https://github.com/python-cmd2/cmd2/tree/main/examples) - Basic cmd2 examples to demonstrate how to use various features - [Advanced Examples](https://github.com/jayrod/cmd2-example-apps) - More complex examples that demonstrate more featuers about how to put together a complete application +- [Cookiecutter](https://github.com/cookiecutter/cookiecutter) Templates from community + - Basic cookiecutter template for cmd2 application : + https://github.com/jayrod/cookiecutter-python-cmd2 + - Advanced cookiecutter template with external plugin support : + https://github.com/jayrod/cookiecutter-python-cmd2-ext-plug ## Hello World @@ -157,7 +157,6 @@ reproduce the bug. At a minimum, please state the following: | Application Name | Description | Organization or Author | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -| [Pobshell](https://github.com/pdalloz/pobshell) | A Bash‑like shell for live Python objects: `cd`, `ls`, `cat`, `find` and _CLI piping_ for object code, str values & more | [Peter Dalloz](https://www.linkedin.com/in/pdalloz) | | [CephFS Shell](https://github.com/ceph/ceph) | The Ceph File System, or CephFS, is a POSIX-compliant file system built on top of Ceph’s distributed object store | [ceph](https://ceph.com/) | | [garak](https://github.com/NVIDIA/garak) | LLM vulnerability scanner that checks if an LLM can be made to fail in a way we don't want | [NVIDIA](https://github.com/NVIDIA) | | [medusa](https://github.com/Ch0pin/medusa) | Binary instrumentation framework that that automates processes for the dynamic analysis of Android and iOS Applications | [Ch0pin](https://github.com/Ch0pin) | @@ -172,6 +171,7 @@ reproduce the bug. At a minimum, please state the following: | [tomcatmanager](https://github.com/tomcatmanager/tomcatmanager) | A command line tool and python library for managing a tomcat server | [tomcatmanager](https://github.com/tomcatmanager) | | [Falcon Toolkit](https://github.com/CrowdStrike/Falcon-Toolkit) | Unleash the power of the CrowdStrike Falcon Platform at the CLI | [CrowdStrike](https://github.com/CrowdStrike) | | [EXPLIoT](https://gitlab.com/expliot_framework/expliot) | Internet of Things Security Testing and Exploitation framework | [expliot_framework](https://gitlab.com/expliot_framework/) | +| [Pobshell](https://github.com/pdalloz/pobshell) | A Bash‑like shell for live Python objects: `cd`, `ls`, `cat`, `find` and _CLI piping_ for object code, str values & more | [Peter Dalloz](https://www.linkedin.com/in/pdalloz) | Possibly defunct but still good examples From 88501efef542e739a90842a5514c9a74db5d16af Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 21 Aug 2025 15:32:30 -0400 Subject: [PATCH 06/13] Fix initialization.py example so it uses a valid startup script and delete redundant alias_startup.py example --- docs/features/startup_commands.md | 4 ++-- examples/README.md | 5 +---- examples/alias_startup.py | 27 --------------------------- examples/initialization.py | 7 ++++++- 4 files changed, 9 insertions(+), 34 deletions(-) delete mode 100755 examples/alias_startup.py diff --git a/docs/features/startup_commands.md b/docs/features/startup_commands.md index b695c43dd..d318a6367 100644 --- a/docs/features/startup_commands.md +++ b/docs/features/startup_commands.md @@ -47,8 +47,8 @@ class StartupApp(cmd2.Cmd): ``` This text file should contain a [Command Script](./scripting.md#command-scripts). See the -[AliasStartup](https://github.com/python-cmd2/cmd2/blob/main/examples/alias_startup.py) example for -a demonstration. +[initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) example +for a demonstration. You can silence a startup script's output by setting `silence_startup_script` to True: diff --git a/examples/README.md b/examples/README.md index 28937c645..d8c5010fb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,9 +11,6 @@ application, if you are looking for that then see Here is the list of examples in alphabetical order by filename along with a brief description of each: -- [alias_startup.py](https://github.com/python-cmd2/cmd2/blob/main/examples/alias_startup.py) - - Demonstrates how to add custom command aliases and how to run an initialization script at - startup - [arg_decorators.py](https://github.com/python-cmd2/cmd2/blob/main/examples/arg_decorators.py) - Demonstrates how to use the `cmd2.with_argparser` decorator to specify command arguments using [argparse](https://docs.python.org/3/library/argparse.html) @@ -62,7 +59,7 @@ each: - [hooks.py](https://github.com/python-cmd2/cmd2/blob/main/examples/hooks.py) - Shows how to use various `cmd2` application lifecycle hooks - [initialization.py](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) - - Shows how to colorize output, use multiline command, add persistent history, and more + - Shows how to colorize output, use multiline command, add persistent history, and much more - [migrating.py](https://github.com/python-cmd2/cmd2/blob/main/examples/migrating.py) - A simple `cmd` application that you can migrate to `cmd2` by changing one line - [modular_commands_basic.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands_basic.py) diff --git a/examples/alias_startup.py b/examples/alias_startup.py deleted file mode 100755 index f6e401a0c..000000000 --- a/examples/alias_startup.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -"""A simple example demonstrating the following: -1) How to add custom command aliases using the alias command -2) How to run an initialization script at startup. -""" - -import os - -import cmd2 - - -class AliasAndStartup(cmd2.Cmd): - """Example cmd2 application where we create commands that just print the arguments they are called with.""" - - def __init__(self) -> None: - alias_script = os.path.join(os.path.dirname(__file__), '.cmd2rc') - super().__init__(startup_script=alias_script) - - def do_nothing(self, args) -> None: - """This command does nothing and produces no output.""" - - -if __name__ == '__main__': - import sys - - app = AliasAndStartup() - sys.exit(app.cmdloop()) diff --git a/examples/initialization.py b/examples/initialization.py index bee050405..1b541968e 100755 --- a/examples/initialization.py +++ b/examples/initialization.py @@ -12,6 +12,8 @@ 10) How to make custom attributes settable at runtime. """ +import pathlib + from rich.style import Style import cmd2 @@ -25,10 +27,13 @@ class BasicApp(cmd2.Cmd): CUSTOM_CATEGORY = 'My Custom Commands' def __init__(self) -> None: + # Startup script that defines a couple aliases for running shell commands + alias_script = pathlib.Path(__file__).absolute().parent / '.cmd2rc' + super().__init__( multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', + startup_script=alias_script, include_ipy=True, ) From 0e8a42a91c679fede13d24b2f6ae164ec6be905a Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 21 Aug 2025 16:07:19 -0400 Subject: [PATCH 07/13] Edit initialization.md to auto-load code from the initialization.py example instead of manually copying it by hand --- docs/features/initialization.md | 93 +++------------------------------ 1 file changed, 6 insertions(+), 87 deletions(-) diff --git a/docs/features/initialization.md b/docs/features/initialization.md index e8a141e81..805ee4018 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -2,94 +2,13 @@ Here is a basic example `cmd2` application which demonstrates many capabilities which you may wish to utilize while initializing the app: -```py - #!/usr/bin/env python3 - """A simple example cmd2 application demonstrating the following: - 1) Colorizing/stylizing output - 2) Using multiline commands - 3) Persistent history - 4) How to run an initialization script at startup - 5) How to group and categorize commands when displaying them in help - 6) Opting-in to using the ipy command to run an IPython shell - 7) Allowing access to your application in py and ipy - 8) Displaying an intro banner upon starting your application - 9) Using a custom prompt - 10) How to make custom attributes settable at runtime. - """ +!!! example - from rich.style import Style - - import cmd2 - from cmd2 import ( - Color, - stylize, - ) - - - class BasicApp(cmd2.Cmd): - CUSTOM_CATEGORY = 'My Custom Commands' - - def __init__(self) -> None: - super().__init__( - multiline_commands=['echo'], - persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', - include_ipy=True, - ) - - # Prints an intro banner once upon application startup - self.intro = stylize( - 'Welcome to cmd2!', - style=Style(color=Color.RED, bgcolor=Color.WHITE, bold=True), - ) - - # Show this as the prompt when asking for input - self.prompt = 'myapp> ' - - # Used as prompt for multiline commands after the first line - self.continuation_prompt = '... ' - - # Allow access to your application in py and ipy via self - self.self_in_py = True - - # Set the default category name - self.default_category = 'cmd2 Built-in Commands' - - # Color to output text in with echo command - self.foreground_color = Color.CYAN.value - - # Make echo_fg settable at runtime - fg_colors = [c.value for c in Color] - self.add_settable( - cmd2.Settable( - 'foreground_color', - str, - 'Foreground color to use with echo command', - self, - choices=fg_colors, - ) - ) - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _: cmd2.Statement) -> None: - """Display the intro banner.""" - self.poutput(self.intro) - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg: cmd2.Statement) -> None: - """Example of a multiline command.""" - self.poutput( - stylize( - arg, - style=Style(color=self.foreground_color), - ) - ) - - - if __name__ == '__main__': - app = BasicApp() - app.cmdloop() -``` + ```py + {% + include "../../examples/initialization.py" + %} + ``` ## Cmd class initializer From 3f8c9432ab55d596abf070ee207d89dbf221c7d6 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 21 Aug 2025 16:15:51 -0400 Subject: [PATCH 08/13] Changed the example title to show examples/initialization.py to make it clearer --- docs/features/initialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/initialization.md b/docs/features/initialization.md index 805ee4018..f14da2e40 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -2,7 +2,7 @@ Here is a basic example `cmd2` application which demonstrates many capabilities which you may wish to utilize while initializing the app: -!!! example +!!! example "examples/initialization.py" ```py {% From d39c64392e244e1f0d294cdb038c57c4b29643cc Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 23 Aug 2025 15:50:08 -0400 Subject: [PATCH 09/13] Merge redundant examples The following 4 examples all demonstrate various aspects of using `argparse` for command argument processing and have been merged into a single comprehensive example called `argparse_example.py`: - arg_decorators.py - decorator_example.py - arg_print.py - subcommands.py `first_app.py` and `initialization.py` had a lot of overlap in demonstrating basic features of cmd2. I combined them into a single `getting_started.py` example --- .github/CONTRIBUTING.md | 2 +- cmd2/cmd2.py | 2 +- cmd2/transcript.py | 2 +- .../{first_app.md => getting_started.md} | 18 +- docs/examples/index.md | 2 +- docs/features/argument_processing.md | 13 +- docs/features/initialization.md | 4 +- docs/features/os.md | 18 +- docs/features/prompt.md | 4 +- docs/features/startup_commands.md | 2 +- docs/features/transcripts.md | 4 +- docs/overview/index.md | 4 +- examples/README.md | 26 +- examples/arg_decorators.py | 60 ----- examples/arg_print.py | 67 ----- examples/argparse_example.py | 233 ++++++++++++++++++ examples/cmd_as_argument.py | 2 +- examples/decorator_example.py | 113 --------- examples/first_app.py | 58 ----- .../{initialization.py => getting_started.py} | 7 +- examples/subcommands.py | 116 --------- .../{example.py => transcript_example.py} | 6 +- examples/transcripts/exampleSession.txt | 2 +- examples/transcripts/transcript_regex.txt | 2 +- mkdocs.yml | 2 +- tests/test_completion.py | 102 +++++++- tests/transcripts/regex_set.txt | 2 +- 27 files changed, 393 insertions(+), 480 deletions(-) rename docs/examples/{first_app.md => getting_started.md} (97%) delete mode 100755 examples/arg_decorators.py delete mode 100755 examples/arg_print.py create mode 100755 examples/argparse_example.py delete mode 100755 examples/decorator_example.py delete mode 100755 examples/first_app.py rename examples/{initialization.py => getting_started.py} (93%) delete mode 100755 examples/subcommands.py rename examples/{example.py => transcript_example.py} (91%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7ca430db6..81c8d0872 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -329,7 +329,7 @@ environment is set up and working properly. You can also run the example app and see a prompt that says "(Cmd)" running the command: ```sh -$ uv run examples/example.py +$ uv run examples/getting_started.py ``` You can type `help` to get help or `quit` to quit. If you see that, then congratulations – you're diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 7ee983381..6d8c5359a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -10,7 +10,7 @@ Settable environment parameters Parsing commands with `argparse` argument parsers (flags) Redirection to file or paste buffer (clipboard) with > or >> -Easy transcript-based testing of applications (see examples/example.py) +Easy transcript-based testing of applications (see examples/transcript_example.py) Bash-style ``select`` available Note, if self.stdout is different than sys.stdout, then redirection with > and | diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 50a6fd61a..430ad8cef 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -34,7 +34,7 @@ class Cmd2TestCase(unittest.TestCase): that will execute the commands in a transcript file and expect the results shown. - See example.py + See transcript_example.py """ cmdapp: Optional['Cmd'] = None diff --git a/docs/examples/first_app.md b/docs/examples/getting_started.md similarity index 97% rename from docs/examples/first_app.md rename to docs/examples/getting_started.md index 86efd70ff..0ab7289eb 100644 --- a/docs/examples/first_app.md +++ b/docs/examples/getting_started.md @@ -1,6 +1,6 @@ -# First Application +# Getting Started -Here's a quick walkthrough of a simple application which demonstrates 8 features of `cmd2`: +Here's a quick walkthrough of a simple application which demonstrates 10 features of `cmd2`: - [Settings](../features/settings.md) - [Commands](../features/commands.md) @@ -14,17 +14,17 @@ Here's a quick walkthrough of a simple application which demonstrates 8 features If you don't want to type as we go, here is the complete source (you can click to expand and then click the **Copy** button in the top-right): -??? example +!!! example "getting_started.py" ```py {% - include "../../examples/first_app.py" + include "../../examples/getting_started.py" %} ``` ## Basic Application -First we need to create a new `cmd2` application. Create a new file `first_app.py` with the +First we need to create a new `cmd2` application. Create a new file `getting_started.py` with the following contents: ```py @@ -47,7 +47,7 @@ We have a new class `FirstApp` which is a subclass of [cmd2.Cmd][]. When we tell file like this: ```shell -$ python first_app.py +$ python getting_started.py ``` it creates an instance of our class, and calls the `cmd2.Cmd.cmdloop` method. This method accepts @@ -77,7 +77,7 @@ In that initializer, the first thing to do is to make sure we initialize `cmd2`. run the script, and enter the `set` command to see the settings, like this: ```shell -$ python first_app.py +$ python getting_started.py (Cmd) set ``` @@ -88,8 +88,8 @@ you will see our `maxrepeats` setting show up with it's default value of `3`. Now we will create our first command, called `speak` which will echo back whatever we tell it to say. We are going to use an [argument processor](../features/argument_processing.md) so the `speak` command can shout and talk piglatin. We will also use some built in methods for -[generating output](../features/generating_output.md). Add this code to `first_app.py`, so that the -`speak_parser` attribute and the `do_speak()` method are part of the `CmdLineApp()` class: +[generating output](../features/generating_output.md). Add this code to `getting_started.py`, so +that the `speak_parser` attribute and the `do_speak()` method are part of the `CmdLineApp()` class: ```py speak_parser = cmd2.Cmd2ArgumentParser() diff --git a/docs/examples/index.md b/docs/examples/index.md index 23001e973..6aad5a595 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -2,7 +2,7 @@ -- [First Application](first_app.md) +- [Getting Started](getting_started.md) - [Alternate Event Loops](alternate_event_loops.md) - [List of cmd2 examples](examples.md) diff --git a/docs/features/argument_processing.md b/docs/features/argument_processing.md index 9750a596e..33db6723f 100644 --- a/docs/features/argument_processing.md +++ b/docs/features/argument_processing.md @@ -14,10 +14,10 @@ following for you: These features are all provided by the `@with_argparser` decorator which is importable from `cmd2`. -See the either the [argprint](https://github.com/python-cmd2/cmd2/blob/main/examples/arg_print.py) -or [decorator](https://github.com/python-cmd2/cmd2/blob/main/examples/decorator_example.py) example -to learn more about how to use the various `cmd2` argument processing decorators in your `cmd2` -applications. +See the +[argparse_example](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py) +example to learn more about how to use the various `cmd2` argument processing decorators in your +`cmd2` applications. `cmd2` provides the following [decorators](../api/decorators.md) for assisting with parsing arguments passed to commands: @@ -286,8 +286,9 @@ argparse sub-parsers. You may add multiple layers of subcommands for your command. `cmd2` will automatically traverse and tab complete subcommands for all commands using argparse. -See the [subcommands](https://github.com/python-cmd2/cmd2/blob/main/examples/subcommands.py) example -to learn more about how to use subcommands in your `cmd2` application. +See the +[argparse_example](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py) +example to learn more about how to use subcommands in your `cmd2` application. ## Argparse Extensions diff --git a/docs/features/initialization.md b/docs/features/initialization.md index f14da2e40..5fe6e008d 100644 --- a/docs/features/initialization.md +++ b/docs/features/initialization.md @@ -2,11 +2,11 @@ Here is a basic example `cmd2` application which demonstrates many capabilities which you may wish to utilize while initializing the app: -!!! example "examples/initialization.py" +!!! example "examples/getting_started.py" ```py {% - include "../../examples/initialization.py" + include "../../examples/getting_started.py" %} ``` diff --git a/docs/features/os.md b/docs/features/os.md index d1da31bf5..313b988bb 100644 --- a/docs/features/os.md +++ b/docs/features/os.md @@ -69,23 +69,23 @@ 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/example.py` included in the source code -of `cmd2`: +this technique. Here's a demonstration using the `examples/transcript_example.py` included in the +source code of `cmd2`: - $ echo "speak -p some words" | python examples/example.py + $ echo "speak -p some words" | python examples/transcript_example.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/example.py + c:\cmd2> type somecmds.txt | python.exe examples/transcript_example.py omesay ordsway By default, `cmd2` programs also look for commands pass as arguments from the operating system shell, and execute those commands before entering the command loop: - $ python examples/example.py help + $ python examples/transcript_example.py help Documented commands (use 'help -v' for verbose/'help ' for details): =========================================================================== @@ -99,8 +99,8 @@ example, you might have a command inside your `cmd2` program which itself accept 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 example/example.py speak -p hello there - python example.py speak -p hello there + $ python example/transcript_example.py speak -p hello there + python transcript_example.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 @@ -122,7 +122,7 @@ Check the source code of this example, especially the `main()` function, to see Alternatively you can simply wrap the command plus arguments in quotes (either single or double quotes): - $ python example/example.py "speak -p hello there" + $ python example/transcript_example.py "speak -p hello there" ellohay heretay (Cmd) @@ -148,6 +148,6 @@ quits while returning an exit code: Here is another example using `quit`: - $ python example/example.py "speak -p hello there" quit + $ python example/transcript_example.py "speak -p hello there" quit ellohay heretay $ diff --git a/docs/features/prompt.md b/docs/features/prompt.md index 6fbf8f226..ad385fbcd 100644 --- a/docs/features/prompt.md +++ b/docs/features/prompt.md @@ -6,7 +6,7 @@ This prompt can be configured by setting the `cmd2.Cmd.prompt` instance attribute. This contains the string which should be printed as a prompt for user input. See the -[Initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) example +[getting_started](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) example for the simple use case of statically setting the prompt. ## Continuation Prompt @@ -15,7 +15,7 @@ When a user types a [Multiline Command](./multiline_commands.md) it may span mor input. The prompt for the first line of input is specified by the `cmd2.Cmd.prompt` instance attribute. The prompt for subsequent lines of input is defined by the `cmd2.Cmd.continuation_prompt` attribute.See the -[Initialization](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) example +[getting_started](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) example for a demonstration of customizing the continuation prompt. ## Updating the prompt diff --git a/docs/features/startup_commands.md b/docs/features/startup_commands.md index d318a6367..1bd563abc 100644 --- a/docs/features/startup_commands.md +++ b/docs/features/startup_commands.md @@ -16,7 +16,7 @@ program. `cmd2` interprets each argument as a separate command, so you should en 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/example.py "say hello" "say Gracie" quit + $ python examples/transcript_example.py "say hello" "say Gracie" quit hello Gracie diff --git a/docs/features/transcripts.md b/docs/features/transcripts.md index 037fc5dda..1368b13e3 100644 --- a/docs/features/transcripts.md +++ b/docs/features/transcripts.md @@ -40,7 +40,7 @@ testing as your `cmd2` application changes. ## Creating Manually -Here's a transcript created from `python examples/example.py`: +Here's a transcript created from `python examples/transcript_example.py`: ```text (Cmd) say -r 3 Goodnight, Gracie @@ -155,7 +155,7 @@ Once you have created a transcript, it's easy to have your application play it b output. From within the `examples/` directory: ```text -$ python example.py --test transcript_regex.txt +$ python transcript_example.py --test transcript_regex.txt . ---------------------------------------------------------------------- Ran 1 test in 0.013s diff --git a/docs/overview/index.md b/docs/overview/index.md index 8038b9c1a..a8cb8ee21 100644 --- a/docs/overview/index.md +++ b/docs/overview/index.md @@ -11,8 +11,8 @@ if this library is a good fit for your needs. - [Installation Instructions](installation.md) - how to install `cmd2` and associated optional dependencies -- [First Application](../examples/first_app.md) - a sample application showing 8 key features of - `cmd2` +- [Getting Started Application](../examples/getting_started.md) - a sample application showing many + key features of `cmd2` - [Integrate cmd2 Into Your Project](integrating.md) - adding `cmd2` to your project - [Alternatives](alternatives.md) - other python packages that might meet your needs - [Resources](resources.md) - related links and other materials diff --git a/examples/README.md b/examples/README.md index d8c5010fb..85f222772 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,14 +11,11 @@ application, if you are looking for that then see Here is the list of examples in alphabetical order by filename along with a brief description of each: -- [arg_decorators.py](https://github.com/python-cmd2/cmd2/blob/main/examples/arg_decorators.py) - - Demonstrates how to use the `cmd2.with_argparser` decorator to specify command arguments using - [argparse](https://docs.python.org/3/library/argparse.html) -- [arg_print.py](https://github.com/python-cmd2/cmd2/blob/main/examples/arg_print.py) - - Demonstrates how arguments and options get parsed and passed to commands and shows how - shortcuts work - [argparse_completion.py](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_completion.py) - Shows how to integrate tab-completion with argparse-based commands +- [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/argparse_example.py) + - Demonstrates how to use the `cmd2.with_argparser` decorator to specify command arguments using + [argparse](https://docs.python.org/3/library/argparse.html) - [async_printing.py](https://github.com/python-cmd2/cmd2/blob/main/examples/async_printing.py) - Shows how to asynchronously print alerts, update the prompt in realtime, and change the window title @@ -30,8 +27,6 @@ each: - Show the numerous colors available to use in your cmd2 applications - [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py) - Demonstrates how to create your own custom `Cmd2ArgumentParser` -- [decorator_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/decorator_example.py) - - Shows how to use cmd2's various argparse decorators to processes command-line arguments - [default_categories.py](https://github.com/python-cmd2/cmd2/blob/main/examples/default_categories.py) - Demonstrates usage of `@with_default_category` decorator to group and categorize commands and `CommandSet` use @@ -43,13 +38,10 @@ each: - [event_loops.py](https://github.com/python-cmd2/cmd2/blob/main/examples/event_loops.py) - Shows how to integrate a `cmd2` application with an external event loop which isn't managed by `cmd2` -- [example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/example.py) - - This example is intended to demonstrate `cmd2's` build-in transcript testing capability - [exit_code.py](https://github.com/python-cmd2/cmd2/blob/main/examples/exit_code.py) - Show how to emit a non-zero exit code from your `cmd2` application when it exits -- [first_app.py](https://github.com/python-cmd2/cmd2/blob/main/examples/first_app.py) - - Short application that demonstrates 8 key features: Settings, Commands, Argument Parsing, - Generating Output, Help, Shortcuts, Multiple Commands, and History +- [getting_started.py](https://github.com/python-cmd2/cmd2/blob/main/examples/getting_started.py) + - Short application that demonstrates many key features of cmd2 - [hello_cmd2.py](https://github.com/python-cmd2/cmd2/blob/main/examples/hello_cmd2.py) - Completely bare-bones `cmd2` application suitable for rapid testing and debugging of `cmd2` itself @@ -58,8 +50,6 @@ each: command - [hooks.py](https://github.com/python-cmd2/cmd2/blob/main/examples/hooks.py) - Shows how to use various `cmd2` application lifecycle hooks -- [initialization.py](https://github.com/python-cmd2/cmd2/blob/main/examples/initialization.py) - - Shows how to colorize output, use multiline command, add persistent history, and much more - [migrating.py](https://github.com/python-cmd2/cmd2/blob/main/examples/migrating.py) - A simple `cmd` application that you can migrate to `cmd2` by changing one line - [modular_commands_basic.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands_basic.py) @@ -90,13 +80,11 @@ each: - [remove_settable.py](https://github.com/python-cmd2/cmd2/blob/main/examples/remove_settable.py) - Shows how to remove any of the built-in cmd2 `Settables` you do not want in your cmd2 application -- [subcommands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/subcommands.py) - - Shows how to use `argparse` to easily support sub-commands within your cmd2 commands -- [table_creation.py](https://github.com/python-cmd2/cmd2/blob/main/examples/table_creation.py) - - Contains various examples of using cmd2's table creation capabilities - [tmux_launch.sh](https://github.com/python-cmd2/cmd2/blob/main/examples/tmux_launch.sh) - Shell script that launches two applications using tmux in different windows/tabs - [tmux_split.sh](https://github.com/python-cmd2/cmd2/blob/main/examples/tmux_split.sh) - Shell script that launches two applications using tmux in a split pane view +- [transcript_example.py](https://github.com/python-cmd2/cmd2/blob/main/examples/transcript_example.py) + - This example is intended to demonstrate `cmd2's` build-in transcript testing capability - [unicode_commands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/unicode_commands.py) - Shows that cmd2 supports unicode everywhere, including within command names diff --git a/examples/arg_decorators.py b/examples/arg_decorators.py deleted file mode 100755 index 5fe262d4c..000000000 --- a/examples/arg_decorators.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -"""An example demonstrating how use one of cmd2's argument parsing decorators.""" - -import argparse -import os - -import cmd2 - - -class ArgparsingApp(cmd2.Cmd): - def __init__(self) -> None: - super().__init__(include_ipy=True) - self.intro = 'cmd2 has awesome decorators to make it easy to use Argparse to parse command arguments' - - # do_fsize parser - fsize_parser = cmd2.Cmd2ArgumentParser(description='Obtain the size of a file') - fsize_parser.add_argument('-c', '--comma', action='store_true', help='add comma for thousands separator') - fsize_parser.add_argument('-u', '--unit', choices=['MB', 'KB'], help='unit to display size in') - fsize_parser.add_argument('file_path', help='path of file', completer=cmd2.Cmd.path_complete) - - @cmd2.with_argparser(fsize_parser) - def do_fsize(self, args: argparse.Namespace) -> None: - """Obtain the size of a file.""" - expanded_path = os.path.expanduser(args.file_path) - - try: - size = os.path.getsize(expanded_path) - except OSError as ex: - self.perror(f"Error retrieving size: {ex}") - return - - if args.unit == 'KB': - size /= 1024 - elif args.unit == 'MB': - size /= 1024 * 1024 - else: - args.unit = 'bytes' - size = round(size, 2) - - if args.comma: - size = f'{size:,}' - self.poutput(f'{size} {args.unit}') - - # do_pow parser - pow_parser = cmd2.Cmd2ArgumentParser() - pow_parser.add_argument('base', type=int) - pow_parser.add_argument('exponent', type=int, choices=range(-5, 6)) - - @cmd2.with_argparser(pow_parser) - def do_pow(self, args: argparse.Namespace) -> None: - """Raise an integer to a small integer exponent, either positive or negative. - - :param args: argparse arguments - """ - self.poutput(f'{args.base} ** {args.exponent} == {args.base**args.exponent}') - - -if __name__ == '__main__': - app = ArgparsingApp() - app.cmdloop() diff --git a/examples/arg_print.py b/examples/arg_print.py deleted file mode 100755 index 506e92250..000000000 --- a/examples/arg_print.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python -"""A simple example demonstrating the following: - 1) How arguments and options get parsed and passed to commands - 2) How to change what syntax gets parsed as a comment and stripped from the arguments. - -This is intended to serve as a live demonstration so that developers can -experiment with and understand how command and argument parsing work. - -It also serves as an example of how to create shortcuts. -""" - -import cmd2 - - -class ArgumentAndOptionPrinter(cmd2.Cmd): - """Example cmd2 application where we create commands that just print the arguments they are called with.""" - - def __init__(self) -> None: - # Create command shortcuts which are typically 1 character abbreviations which can be used in place of a command - shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) - shortcuts.update({'$': 'aprint', '%': 'oprint'}) - super().__init__(shortcuts=shortcuts) - - def do_aprint(self, statement) -> None: - """Print the argument string this basic command is called with.""" - self.poutput(f'aprint was called with argument: {statement!r}') - self.poutput(f'statement.raw = {statement.raw!r}') - self.poutput(f'statement.argv = {statement.argv!r}') - self.poutput(f'statement.command = {statement.command!r}') - - @cmd2.with_argument_list - def do_lprint(self, arglist) -> None: - """Print the argument list this basic command is called with.""" - self.poutput(f'lprint was called with the following list of arguments: {arglist!r}') - - @cmd2.with_argument_list(preserve_quotes=True) - def do_rprint(self, arglist) -> None: - """Print the argument list this basic command is called with (with quotes preserved).""" - self.poutput(f'rprint was called with the following list of arguments: {arglist!r}') - - oprint_parser = cmd2.Cmd2ArgumentParser() - oprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - oprint_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - oprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - oprint_parser.add_argument('words', nargs='+', help='words to print') - - @cmd2.with_argparser(oprint_parser) - def do_oprint(self, args) -> None: - """Print the options and argument list this options command was called with.""" - self.poutput(f'oprint was called with the following\n\toptions: {args!r}') - - pprint_parser = cmd2.Cmd2ArgumentParser() - pprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - pprint_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - pprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - - @cmd2.with_argparser(pprint_parser, with_unknown_args=True) - def do_pprint(self, args, unknown) -> None: - """Print the options and argument list this options command was called with.""" - self.poutput(f'oprint was called with the following\n\toptions: {args!r}\n\targuments: {unknown}') - - -if __name__ == '__main__': - import sys - - app = ArgumentAndOptionPrinter() - sys.exit(app.cmdloop()) diff --git a/examples/argparse_example.py b/examples/argparse_example.py new file mode 100755 index 000000000..24d694184 --- /dev/null +++ b/examples/argparse_example.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""A comprehensive example demonstrating various aspects of using `argparse` for command argument processing. + +Demonstrates basic usage of the `cmd2.with_argparser` decorator for passing a `cmd2.Cmd2ArgumentParser` to a `do_*` command +method. The `fsize` and `pow` commands demonstrate various different types of arguments, actions, choices, and completers that +can be used. + +The `print_args` and `print_unknown` commands display how argparse arguments are passed to commands in the cases that unknown +arguments are not captured and are captured, respectively. + +The `base` and `alternate` commands show an easy way for a single command to have many subcommands, each of which take +different arguments and provides separate contextual help. + +Lastly, this example shows how you can also use `argparse` to parse command-line arguments when launching a cmd2 application. +""" + +import argparse +import os + +import cmd2 +from cmd2.string_utils import stylize + +# Command categories +ARGPARSE_USAGE = 'Argparse Basic Usage' +ARGPARSE_PRINTING = 'Argparse Printing' +ARGPARSE_SUBCOMMANDS = 'Argparse Subcommands' + + +class ArgparsingApp(cmd2.Cmd): + def __init__(self, color) -> None: + super().__init__(include_ipy=True) + self.intro = stylize( + 'cmd2 has awesome decorators to make it easy to use Argparse to parse command arguments', style=color + ) + + ## ------ Basic examples of using argparse for command argument parsing ----- + + # do_fsize parser + fsize_parser = cmd2.Cmd2ArgumentParser(description='Obtain the size of a file') + fsize_parser.add_argument('-c', '--comma', action='store_true', help='add comma for thousands separator') + fsize_parser.add_argument('-u', '--unit', choices=['MB', 'KB'], help='unit to display size in') + fsize_parser.add_argument('file_path', help='path of file', completer=cmd2.Cmd.path_complete) + + @cmd2.with_argparser(fsize_parser) + @cmd2.with_category(ARGPARSE_USAGE) + def do_fsize(self, args: argparse.Namespace) -> None: + """Obtain the size of a file.""" + expanded_path = os.path.expanduser(args.file_path) + + try: + size = os.path.getsize(expanded_path) + except OSError as ex: + self.perror(f"Error retrieving size: {ex}") + return + + if args.unit == 'KB': + size /= 1024 + elif args.unit == 'MB': + size /= 1024 * 1024 + else: + args.unit = 'bytes' + size = round(size, 2) + + if args.comma: + size = f'{size:,}' + self.poutput(f'{size} {args.unit}') + + # do_pow parser + pow_parser = cmd2.Cmd2ArgumentParser() + pow_parser.add_argument('base', type=int) + pow_parser.add_argument('exponent', type=int, choices=range(-5, 6)) + + @cmd2.with_argparser(pow_parser) + @cmd2.with_category(ARGPARSE_USAGE) + def do_pow(self, args: argparse.Namespace) -> None: + """Raise an integer to a small integer exponent, either positive or negative. + + :param args: argparse arguments + """ + self.poutput(f'{args.base} ** {args.exponent} == {args.base**args.exponent}') + + ## ------ Examples displaying how argparse arguments are passed to commands by printing them out ----- + + argprint_parser = cmd2.Cmd2ArgumentParser() + argprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + argprint_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + argprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + argprint_parser.add_argument('words', nargs='+', help='words to print') + + @cmd2.with_argparser(argprint_parser) + @cmd2.with_category(ARGPARSE_PRINTING) + def do_print_args(self, args) -> None: + """Print the arpgarse argument list this command was called with.""" + self.poutput(f'print_args was called with the following\n\targuments: {args!r}') + + unknownprint_parser = cmd2.Cmd2ArgumentParser() + unknownprint_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + unknownprint_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + unknownprint_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + + @cmd2.with_argparser(unknownprint_parser, with_unknown_args=True) + @cmd2.with_category(ARGPARSE_PRINTING) + def do_print_unknown(self, args, unknown) -> None: + """Print the arpgarse argument list this command was called with, including unknown arguments.""" + self.poutput(f'print_unknown was called with the following arguments\n\tknown: {args!r}\n\tunknown: {unknown}') + + ## ------ Examples demonstrating how to use argparse subcommands ----- + + sport_item_strs = ('Bat', 'Basket', 'Basketball', 'Football', 'Space Ball') + + # create the top-level parser for the base command + base_parser = cmd2.Cmd2ArgumentParser() + base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" subcommand + parser_foo = base_subparsers.add_parser('foo', help='foo help') + parser_foo.add_argument('-x', type=int, default=1, help='integer') + parser_foo.add_argument('y', type=float, help='float') + parser_foo.add_argument('input_file', type=str, help='Input File') + + # create the parser for the "bar" subcommand + parser_bar = base_subparsers.add_parser('bar', help='bar help') + + bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') + parser_bar.add_argument('z', help='string') + + bar_subparsers.add_parser('apple', help='apple help') + bar_subparsers.add_parser('artichoke', help='artichoke help') + bar_subparsers.add_parser('cranberries', help='cranberries help') + + # create the parser for the "sport" subcommand + parser_sport = base_subparsers.add_parser('sport', help='sport help') + sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) + + # create the top-level parser for the alternate command + # The alternate command doesn't provide its own help flag + base2_parser = cmd2.Cmd2ArgumentParser(add_help=False) + base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" subcommand + parser_foo2 = base2_subparsers.add_parser('foo', help='foo help') + parser_foo2.add_argument('-x', type=int, default=1, help='integer') + parser_foo2.add_argument('y', type=float, help='float') + parser_foo2.add_argument('input_file', type=str, help='Input File') + + # create the parser for the "bar" subcommand + parser_bar2 = base2_subparsers.add_parser('bar', help='bar help') + + bar2_subparsers = parser_bar2.add_subparsers(title='layer3', help='help for 3rd layer of commands') + parser_bar2.add_argument('z', help='string') + + bar2_subparsers.add_parser('apple', help='apple help') + bar2_subparsers.add_parser('artichoke', help='artichoke help') + bar2_subparsers.add_parser('cranberries', help='cranberries help') + + # create the parser for the "sport" subcommand + parser_sport2 = base2_subparsers.add_parser('sport', help='sport help') + sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) + + # subcommand functions for the base command + def base_foo(self, args) -> None: + """Foo subcommand of base command.""" + self.poutput(args.x * args.y) + + def base_bar(self, args) -> None: + """Bar subcommand of base command.""" + self.poutput(f'(({args.z}))') + + def base_sport(self, args) -> None: + """Sport subcommand of base command.""" + self.poutput(f'Sport is {args.sport}') + + # Set handler functions for the subcommands + parser_foo.set_defaults(func=base_foo) + parser_bar.set_defaults(func=base_bar) + parser_sport.set_defaults(func=base_sport) + + @cmd2.with_argparser(base_parser) + @cmd2.with_category(ARGPARSE_SUBCOMMANDS) + def do_base(self, args) -> None: + """Base command help.""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('base') + + @cmd2.with_argparser(base2_parser) + @cmd2.with_category(ARGPARSE_SUBCOMMANDS) + def do_alternate(self, args) -> None: + """Alternate command help.""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('alternate') + + +if __name__ == '__main__': + import sys + + from cmd2.colors import Color + + # You can do your custom Argparse parsing here to meet your application's needs + parser = cmd2.Cmd2ArgumentParser(description='Process the arguments however you like.') + + # Add an argument which we will pass to the app to change some behavior + parser.add_argument( + '-c', + '--color', + choices=[Color.RED, Color.ORANGE1, Color.YELLOW, Color.GREEN, Color.BLUE, Color.PURPLE, Color.VIOLET, Color.WHITE], + help='Color of intro text', + ) + + # Parse the arguments + args, unknown_args = parser.parse_known_args() + + color = Color.WHITE + if args.color: + color = args.color + + # Perform surgery on sys.argv to remove the arguments which have already been processed by argparse + sys.argv = sys.argv[:1] + unknown_args + + # Instantiate your cmd2 application + app = ArgparsingApp(color) + + # And run your cmd2 application + sys.exit(app.cmdloop()) diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index dd265074c..b9db4acd5 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 is very similar to example.py, but had additional +This example is very similar to transcript_example.py, but had additional code in main() that shows how to accept a command from the command line at invocation: diff --git a/examples/decorator_example.py b/examples/decorator_example.py deleted file mode 100755 index 736c729e7..000000000 --- a/examples/decorator_example.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python -"""A sample application showing how to use cmd2's argparse decorators to -process command line arguments for your application. - -Thanks to cmd2's built-in transcript testing capability, it also -serves as a test suite when used with the exampleSession.txt transcript. - -Running `python decorator_example.py -t exampleSession.txt` will run -all the commands in the transcript against decorator_example.py, -verifying that the output produced matches the transcript. -""" - -import argparse - -import cmd2 - - -class CmdLineApp(cmd2.Cmd): - """Example cmd2 application.""" - - def __init__(self, ip_addr=None, port=None, transcript_files=None) -> None: - shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) - shortcuts.update({'&': 'speak'}) - super().__init__(transcript_files=transcript_files, multiline_commands=['orate'], shortcuts=shortcuts) - - self.maxrepeats = 3 - # Make maxrepeats settable at runtime - self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) - - # Example of args set from the command-line (but they aren't being used here) - self._ip = ip_addr - self._port = port - - # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist - # self.default_to_shell = True # noqa: ERA001 - - speak_parser = cmd2.Cmd2ArgumentParser() - speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - speak_parser.add_argument('words', nargs='+', help='words to say') - - @cmd2.with_argparser(speak_parser) - def do_speak(self, args: argparse.Namespace) -> None: - """Repeats what you tell me to.""" - words = [] - for word in args.words: - if args.piglatin: - word = f'{word[1:]}{word[0]}ay' - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for _ in range(min(repetitions, self.maxrepeats)): - self.poutput(' '.join(words)) - - do_say = do_speak # now "say" is a synonym for "speak" - do_orate = do_speak # another synonym, but this one takes multi-line input - - tag_parser = cmd2.Cmd2ArgumentParser() - tag_parser.add_argument('tag', help='tag') - tag_parser.add_argument('content', nargs='+', help='content to surround with tag') - - @cmd2.with_argparser(tag_parser) - def do_tag(self, args: argparse.Namespace) -> None: - """Create an html tag.""" - # The Namespace always includes the Statement object created when parsing the command line - statement = args.cmd2_statement.get() - - self.poutput(f"The command line you ran was: {statement.command_and_args}") - self.poutput("It generated this tag:") - self.poutput('<{0}>{1}'.format(args.tag, ' '.join(args.content))) - - @cmd2.with_argument_list - def do_tagg(self, arglist: list[str]) -> None: - """Version of creating an html tag using arglist instead of argparser.""" - if len(arglist) >= 2: - tag = arglist[0] - content = arglist[1:] - self.poutput('<{0}>{1}'.format(tag, ' '.join(content))) - else: - self.perror("tagg requires at least 2 arguments") - - -if __name__ == '__main__': - import sys - - # You can do your custom Argparse parsing here to meet your application's needs - parser = cmd2.Cmd2ArgumentParser(description='Process the arguments however you like.') - - # Add a few arguments which aren't really used, but just to get the gist - parser.add_argument('-p', '--port', type=int, help='TCP port') - parser.add_argument('-i', '--ip', type=str, help='IPv4 address') - - # Add an argument which enables transcript testing - args, unknown_args = parser.parse_known_args() - - port = None - if args.port: - port = args.port - - ip_addr = None - if args.ip: - ip_addr = args.ip - - # Perform surgery on sys.argv to remove the arguments which have already been processed by argparse - sys.argv = sys.argv[:1] + unknown_args - - # Instantiate your cmd2 application - c = CmdLineApp() - - # And run your cmd2 application - sys.exit(c.cmdloop()) diff --git a/examples/first_app.py b/examples/first_app.py deleted file mode 100755 index c82768a37..000000000 --- a/examples/first_app.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python -"""A simple application using cmd2 which demonstrates 8 key features: - -* Settings -* Commands -* Argument Parsing -* Generating Output -* Help -* Shortcuts -* Multiline Commands -* History -""" - -import cmd2 - - -class FirstApp(cmd2.Cmd): - """A simple cmd2 application.""" - - def __init__(self) -> None: - shortcuts = cmd2.DEFAULT_SHORTCUTS - shortcuts.update({'&': 'speak'}) - super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) - - # Make maxrepeats settable at runtime - self.maxrepeats = 3 - self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) - - speak_parser = cmd2.Cmd2ArgumentParser() - speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - speak_parser.add_argument('words', nargs='+', help='words to say') - - @cmd2.with_argparser(speak_parser) - def do_speak(self, args) -> None: - """Repeats what you tell me to.""" - words = [] - for word in args.words: - if args.piglatin: - word = f'{word[1:]}{word[0]}ay' - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for _ in range(min(repetitions, self.maxrepeats)): - # .poutput handles newlines, and accommodates output redirection too - self.poutput(' '.join(words)) - - # orate is a synonym for speak which takes multiline input - do_orate = do_speak - - -if __name__ == '__main__': - import sys - - c = FirstApp() - sys.exit(c.cmdloop()) diff --git a/examples/initialization.py b/examples/getting_started.py similarity index 93% rename from examples/initialization.py rename to examples/getting_started.py index 1b541968e..ad24fde39 100755 --- a/examples/initialization.py +++ b/examples/getting_started.py @@ -10,6 +10,7 @@ 8) Displaying an intro banner upon starting your application 9) Using a custom prompt 10) How to make custom attributes settable at runtime. +11) Shortcuts for commands """ import pathlib @@ -30,11 +31,15 @@ def __init__(self) -> None: # Startup script that defines a couple aliases for running shell commands alias_script = pathlib.Path(__file__).absolute().parent / '.cmd2rc' + # Create a shortcut for one of our commands + shortcuts = cmd2.DEFAULT_SHORTCUTS + shortcuts.update({'&': 'intro'}) super().__init__( + include_ipy=True, multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', + shortcuts=shortcuts, startup_script=alias_script, - include_ipy=True, ) # Prints an intro banner once upon application startup diff --git a/examples/subcommands.py b/examples/subcommands.py deleted file mode 100755 index b2768cffe..000000000 --- a/examples/subcommands.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -"""A simple example demonstrating how to use Argparse to support subcommands. - -This example shows an easy way for a single command to have many subcommands, each of which takes different arguments -and provides separate contextual help. -""" - -import cmd2 - -sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - -# create the top-level parser for the base command -base_parser = cmd2.Cmd2ArgumentParser() -base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') - -# create the parser for the "foo" subcommand -parser_foo = base_subparsers.add_parser('foo', help='foo help') -parser_foo.add_argument('-x', type=int, default=1, help='integer') -parser_foo.add_argument('y', type=float, help='float') -parser_foo.add_argument('input_file', type=str, help='Input File') - -# create the parser for the "bar" subcommand -parser_bar = base_subparsers.add_parser('bar', help='bar help') - -bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') -parser_bar.add_argument('z', help='string') - -bar_subparsers.add_parser('apple', help='apple help') -bar_subparsers.add_parser('artichoke', help='artichoke help') -bar_subparsers.add_parser('cranberries', help='cranberries help') - -# create the parser for the "sport" subcommand -parser_sport = base_subparsers.add_parser('sport', help='sport help') -sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) - - -# create the top-level parser for the alternate command -# The alternate command doesn't provide its own help flag -base2_parser = cmd2.Cmd2ArgumentParser(add_help=False) -base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help') - -# create the parser for the "foo" subcommand -parser_foo2 = base2_subparsers.add_parser('foo', help='foo help') -parser_foo2.add_argument('-x', type=int, default=1, help='integer') -parser_foo2.add_argument('y', type=float, help='float') -parser_foo2.add_argument('input_file', type=str, help='Input File') - -# create the parser for the "bar" subcommand -parser_bar2 = base2_subparsers.add_parser('bar', help='bar help') - -bar2_subparsers = parser_bar2.add_subparsers(title='layer3', help='help for 3rd layer of commands') -parser_bar2.add_argument('z', help='string') - -bar2_subparsers.add_parser('apple', help='apple help') -bar2_subparsers.add_parser('artichoke', help='artichoke help') -bar2_subparsers.add_parser('cranberries', help='cranberries help') - -# create the parser for the "sport" subcommand -parser_sport2 = base2_subparsers.add_parser('sport', help='sport help') -sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) - - -class SubcommandsExample(cmd2.Cmd): - """Example cmd2 application where we a base command which has a couple subcommands - and the "sport" subcommand has tab completion enabled. - """ - - def __init__(self) -> None: - super().__init__() - - # subcommand functions for the base command - def base_foo(self, args) -> None: - """Foo subcommand of base command.""" - self.poutput(args.x * args.y) - - def base_bar(self, args) -> None: - """Bar subcommand of base command.""" - self.poutput(f'(({args.z}))') - - def base_sport(self, args) -> None: - """Sport subcommand of base command.""" - self.poutput(f'Sport is {args.sport}') - - # Set handler functions for the subcommands - parser_foo.set_defaults(func=base_foo) - parser_bar.set_defaults(func=base_bar) - parser_sport.set_defaults(func=base_sport) - - @cmd2.with_argparser(base_parser) - def do_base(self, args) -> None: - """Base command help.""" - func = getattr(args, 'func', None) - if func is not None: - # Call whatever subcommand function was selected - func(self, args) - else: - # No subcommand was provided, so call help - self.do_help('base') - - @cmd2.with_argparser(base2_parser) - def do_alternate(self, args) -> None: - """Alternate command help.""" - func = getattr(args, 'func', None) - if func is not None: - # Call whatever subcommand function was selected - func(self, args) - else: - # No subcommand was provided, so call help - self.do_help('alternate') - - -if __name__ == '__main__': - import sys - - app = SubcommandsExample() - sys.exit(app.cmdloop()) diff --git a/examples/example.py b/examples/transcript_example.py similarity index 91% rename from examples/example.py rename to examples/transcript_example.py index 20918152e..06b06c2d7 100755 --- a/examples/example.py +++ b/examples/transcript_example.py @@ -2,10 +2,10 @@ """A sample application for cmd2. Thanks to cmd2's built-in transcript testing capability, it also serves as a -test suite for example.py when used with the transcript_regex.txt transcript. +test suite for transcript_example.py when used with the transcript_regex.txt transcript. -Running `python example.py -t transcript_regex.txt` will run all the commands in -the transcript against example.py, verifying that the output produced matches +Running `python transcript_example.py -t transcript_regex.txt` will run all the commands in +the transcript against transcript_example.py, verifying that the output produced matches the transcript. """ diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt index 85b985d31..84ff1e3f6 100644 --- a/examples/transcripts/exampleSession.txt +++ b/examples/transcripts/exampleSession.txt @@ -1,4 +1,4 @@ -# Run this transcript with "python decorator_example.py -t exampleSession.txt" +# Run this transcript with "python transcript_example.py -t exampleSession.txt" # Anything between two forward slashes, /, is interpreted as a regular expression (regex). # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt index 3065aae52..1eef14276 100644 --- a/examples/transcripts/transcript_regex.txt +++ b/examples/transcripts/transcript_regex.txt @@ -1,4 +1,4 @@ -# Run this transcript with "python example.py -t transcript_regex.txt" +# Run this transcript with "python transcript_example.py -t transcript_regex.txt" # Anything between two forward slashes, /, is interpreted as a regular expression (regex). # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious diff --git a/mkdocs.yml b/mkdocs.yml index be5275a2b..df42da4a6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -189,7 +189,7 @@ nav: - features/transcripts.md - Examples: - examples/index.md - - examples/first_app.md + - examples/getting_started.md - examples/alternate_event_loops.md - examples/examples.md - Plugins: diff --git a/tests/test_completion.py b/tests/test_completion.py index 95e0f314e..f98e3b967 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -14,7 +14,6 @@ import cmd2 from cmd2 import utils -from examples.subcommands import SubcommandsExample from .conftest import ( complete_tester, @@ -22,6 +21,107 @@ run_cmd, ) + +class SubcommandsExample(cmd2.Cmd): + """Example cmd2 application where we a base command which has a couple subcommands + and the "sport" subcommand has tab completion enabled. + """ + + sport_item_strs = ('Bat', 'Basket', 'Basketball', 'Football', 'Space Ball') + + # create the top-level parser for the base command + base_parser = cmd2.Cmd2ArgumentParser() + base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" subcommand + parser_foo = base_subparsers.add_parser('foo', help='foo help') + parser_foo.add_argument('-x', type=int, default=1, help='integer') + parser_foo.add_argument('y', type=float, help='float') + parser_foo.add_argument('input_file', type=str, help='Input File') + + # create the parser for the "bar" subcommand + parser_bar = base_subparsers.add_parser('bar', help='bar help') + + bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') + parser_bar.add_argument('z', help='string') + + bar_subparsers.add_parser('apple', help='apple help') + bar_subparsers.add_parser('artichoke', help='artichoke help') + bar_subparsers.add_parser('cranberries', help='cranberries help') + + # create the parser for the "sport" subcommand + parser_sport = base_subparsers.add_parser('sport', help='sport help') + sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) + + # create the top-level parser for the alternate command + # The alternate command doesn't provide its own help flag + base2_parser = cmd2.Cmd2ArgumentParser(add_help=False) + base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help') + + # create the parser for the "foo" subcommand + parser_foo2 = base2_subparsers.add_parser('foo', help='foo help') + parser_foo2.add_argument('-x', type=int, default=1, help='integer') + parser_foo2.add_argument('y', type=float, help='float') + parser_foo2.add_argument('input_file', type=str, help='Input File') + + # create the parser for the "bar" subcommand + parser_bar2 = base2_subparsers.add_parser('bar', help='bar help') + + bar2_subparsers = parser_bar2.add_subparsers(title='layer3', help='help for 3rd layer of commands') + parser_bar2.add_argument('z', help='string') + + bar2_subparsers.add_parser('apple', help='apple help') + bar2_subparsers.add_parser('artichoke', help='artichoke help') + bar2_subparsers.add_parser('cranberries', help='cranberries help') + + # create the parser for the "sport" subcommand + parser_sport2 = base2_subparsers.add_parser('sport', help='sport help') + sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) + + def __init__(self) -> None: + super().__init__() + + # subcommand functions for the base command + def base_foo(self, args) -> None: + """Foo subcommand of base command.""" + self.poutput(args.x * args.y) + + def base_bar(self, args) -> None: + """Bar subcommand of base command.""" + self.poutput(f'(({args.z}))') + + def base_sport(self, args) -> None: + """Sport subcommand of base command.""" + self.poutput(f'Sport is {args.sport}') + + # Set handler functions for the subcommands + parser_foo.set_defaults(func=base_foo) + parser_bar.set_defaults(func=base_bar) + parser_sport.set_defaults(func=base_sport) + + @cmd2.with_argparser(base_parser) + def do_base(self, args) -> None: + """Base command help.""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('base') + + @cmd2.with_argparser(base2_parser) + def do_alternate(self, args) -> None: + """Alternate command help.""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('alternate') + + # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato', 'Cheese "Pizza"'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index adaa68e2f..225a5b008 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -1,4 +1,4 @@ -# Run this transcript with "python example.py -t transcript_regex.txt" +# Run this transcript with "python transcript_example.py -t transcript_regex.txt" # The regex for allow_style will match any setting for the previous value. # The regex for editor will match whatever program you use. # Regexes on prompts just make the trailing space obvious From d5f5c024acbc0dacef3c5f39d6353fa02b741598 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 23 Aug 2025 17:11:59 -0400 Subject: [PATCH 10/13] Create a new command_sets.py example command_sets.py merges three previous examples: - modular_commands_basic.py - modular_commands_dynamic.py - modular_subcommands.py --- examples/README.md | 12 ++- ...modular_subcommands.py => command_sets.py} | 73 +++++++++++---- ...r_commands_main.py => modular_commands.py} | 8 +- examples/modular_commands_basic.py | 35 -------- examples/modular_commands_dynamic.py | 88 ------------------- 5 files changed, 66 insertions(+), 150 deletions(-) rename examples/{modular_subcommands.py => command_sets.py} (52%) rename examples/{modular_commands_main.py => modular_commands.py} (89%) delete mode 100755 examples/modular_commands_basic.py delete mode 100755 examples/modular_commands_dynamic.py diff --git a/examples/README.md b/examples/README.md index 85f222772..aea040bd8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -25,6 +25,10 @@ each: - Demonstrates how to accept and parse 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) + - Example that demonstrates the `CommandSet` features for modularizing commands and demonstrates + all main capabilities including basic CommandSets, dynamic loading an unloading, using + subcommands, etc. - [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py) - Demonstrates how to create your own custom `Cmd2ArgumentParser` - [default_categories.py](https://github.com/python-cmd2/cmd2/blob/main/examples/default_categories.py) @@ -52,15 +56,9 @@ each: - Shows how to use various `cmd2` application lifecycle hooks - [migrating.py](https://github.com/python-cmd2/cmd2/blob/main/examples/migrating.py) - A simple `cmd` application that you can migrate to `cmd2` by changing one line -- [modular_commands_basic.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands_basic.py) - - Demonstrates based `CommandSet` usage -- [modular_commands_dynamic.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands_dynamic.py) - - Demonstrates dynamic `CommandSet` loading and unloading -- [modular_commands_main.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands_main.py) +- [modular_commands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_commands.py) - Complex example demonstrating a variety of methods to load `CommandSets` using a mix of command decorators -- [modular_subcommands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/modular_subcommands.py) - - Shows how to dynamically add and remove subcommands at runtime using `CommandSets` - [paged_output.py](https://github.com/python-cmd2/cmd2/blob/main/examples/paged_output.py) - Shows how to use output pagination within `cmd2` apps via the `ppaged` method - [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/main/examples/persistent_history.py) diff --git a/examples/modular_subcommands.py b/examples/command_sets.py similarity index 52% rename from examples/modular_subcommands.py rename to examples/command_sets.py index f1dbd024c..320e71932 100755 --- a/examples/modular_subcommands.py +++ b/examples/command_sets.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 -"""A simple example demonstrating modular subcommand loading through CommandSets. +"""This example revolves around the CommandSet feature for modularizing commands. -In this example, there are loadable CommandSets defined. Each CommandSet has 1 subcommand defined that will be -attached to the 'cut' command. +It attempts to cover basic usage as well as more complex usage including dynamic loading and unloading of CommandSets, using +CommandSets to add subcommands, as well as how to categorize command in CommandSets. Here we have kept the implementation for +most commands trivial because the intent is to focus on the CommandSet feature set. -The cut command is implemented with the `do_cut` function that has been tagged as an argparse command. +The `AutoLoadCommandSet` is a basic command set which is loaded automatically at application startup and stays loaded until +application exit. Ths is the simplest case of simply modularizing command definitions to different classes and/or files. -The `load` and `unload` command will load and unload the CommandSets. The available top level commands as well as -subcommands to the `cut` command will change depending on which CommandSets are loaded. +The `LoadableFruits` and `LoadableVegetables` CommandSets are dynamically loadable and un-loadable at runtime using the `load` +and `unload` commands. This demonstrates the ability to load and unload CommandSets based on application state. Each of these +also loads a subcommand of the `cut` command. """ import argparse @@ -20,15 +23,39 @@ with_default_category, ) +COMMANDSET_BASIC = "Basic CommandSet" +COMMANDSET_DYNAMIC = "Dynamic CommandSet" +COMMANDSET_LOAD_UNLOAD = "Loading and Unloading CommandSets" +COMMANDSET_SUBCOMMAND = "Subcommands with CommandSet" -@with_default_category('Fruits') + +@with_default_category(COMMANDSET_BASIC) +class AutoLoadCommandSet(CommandSet): + def __init__(self) -> None: + super().__init__() + + def do_hello(self, _: cmd2.Statement) -> None: + """Prints hello.""" + self._cmd.poutput('Hello') + + def do_world(self, _: cmd2.Statement) -> None: + """Prints World.""" + self._cmd.poutput('World') + + +@with_default_category(COMMANDSET_DYNAMIC) class LoadableFruits(CommandSet): def __init__(self) -> None: super().__init__() def do_apple(self, _: cmd2.Statement) -> None: + """Prints Apple.""" self._cmd.poutput('Apple') + def do_banana(self, _: cmd2.Statement) -> None: + """Prints Banana""" + self._cmd.poutput('Banana') + banana_description = "Cut a banana" banana_parser = cmd2.Cmd2ArgumentParser(description=banana_description) banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) @@ -39,38 +66,48 @@ def cut_banana(self, ns: argparse.Namespace) -> None: self._cmd.poutput('cutting banana: ' + ns.direction) -@with_default_category('Vegetables') +@with_default_category(COMMANDSET_DYNAMIC) class LoadableVegetables(CommandSet): def __init__(self) -> None: super().__init__() def do_arugula(self, _: cmd2.Statement) -> None: + "Prints Arguula." self._cmd.poutput('Arugula') + def do_bokchoy(self, _: cmd2.Statement) -> None: + """Prints Bok Choy.""" + self._cmd.poutput('Bok Choy') + bokchoy_description = "Cut some bokchoy" bokchoy_parser = cmd2.Cmd2ArgumentParser(description=bokchoy_description) bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower()) - def cut_bokchoy(self, _: argparse.Namespace) -> None: - self._cmd.poutput('Bok Choy') + def cut_bokchoy(self, ns: argparse.Namespace) -> None: + self._cmd.poutput('Bok Choy: ' + ns.style) -class ExampleApp(cmd2.Cmd): +class CommandSetApp(cmd2.Cmd): """CommandSets are automatically loaded. Nothing needs to be done.""" - def __init__(self, *args, **kwargs) -> None: - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, auto_load_commands=False, **kwargs) + def __init__(self) -> None: + # This prevents all CommandSets from auto-loading, which is necessary if you don't want some to load at startup + super().__init__(auto_load_commands=False) + + self.register_command_set(AutoLoadCommandSet()) + # Store the dyanmic CommandSet classes for ease of loading and unloading self._fruits = LoadableFruits() self._vegetables = LoadableVegetables() + self.intro = 'The CommandSet feature allows defining commands in multiple files and the dynamic load/unload at runtime' + load_parser = cmd2.Cmd2ArgumentParser() load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) @with_argparser(load_parser) - @with_category('Command Loading') + @with_category(COMMANDSET_LOAD_UNLOAD) def do_load(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': try: @@ -87,6 +124,7 @@ def do_load(self, ns: argparse.Namespace) -> None: self.poutput('Vegetables already loaded') @with_argparser(load_parser) + @with_category(COMMANDSET_LOAD_UNLOAD) def do_unload(self, ns: argparse.Namespace) -> None: if ns.cmds == 'fruits': self.unregister_command_set(self._fruits) @@ -100,8 +138,9 @@ def do_unload(self, ns: argparse.Namespace) -> None: cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') @with_argparser(cut_parser) + @with_category(COMMANDSET_SUBCOMMAND) def do_cut(self, ns: argparse.Namespace) -> None: - # Call handler for whatever subcommand was selected + """Intended to be used with dyanmically loaded subcommands specifically.""" handler = ns.cmd2_handler.get() if handler is not None: handler(ns) @@ -112,5 +151,5 @@ def do_cut(self, ns: argparse.Namespace) -> None: if __name__ == '__main__': - app = ExampleApp() + app = CommandSetApp() app.cmdloop() diff --git a/examples/modular_commands_main.py b/examples/modular_commands.py similarity index 89% rename from examples/modular_commands_main.py rename to examples/modular_commands.py index 2fba205ec..582d1605c 100755 --- a/examples/modular_commands_main.py +++ b/examples/modular_commands.py @@ -1,6 +1,7 @@ #!/usr/bin/env python -"""A complex example demonstrating a variety of methods to load CommandSets using a mix of command decorators -with examples of how to integrate tab completion with argparse-based commands. +"""A complex example demonstrating a variety of methods to load CommandSets using a mix of command decorators. + +Includes examples of how to integrate tab completion with argparse-based commands. """ import argparse @@ -26,6 +27,7 @@ class WithCommandSets(Cmd): def __init__(self, command_sets: Iterable[CommandSet] | None = None) -> None: + """Cmd2 application to demonstrate a variety of methods for loading CommandSets.""" super().__init__(command_sets=command_sets) self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] @@ -54,7 +56,7 @@ def choices_provider(self) -> list[str]: @with_argparser(example_parser) def do_example(self, _: argparse.Namespace) -> None: - """The example command.""" + """An example command.""" self.poutput("I do nothing") diff --git a/examples/modular_commands_basic.py b/examples/modular_commands_basic.py deleted file mode 100755 index c681a389a..000000000 --- a/examples/modular_commands_basic.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""Simple example demonstrating basic CommandSet usage.""" - -import cmd2 -from cmd2 import ( - CommandSet, - with_default_category, -) - - -@with_default_category('My Category') -class AutoLoadCommandSet(CommandSet): - def __init__(self) -> None: - super().__init__() - - def do_hello(self, _: cmd2.Statement) -> None: - self._cmd.poutput('Hello') - - def do_world(self, _: cmd2.Statement) -> None: - self._cmd.poutput('World') - - -class ExampleApp(cmd2.Cmd): - """CommandSets are automatically loaded. Nothing needs to be done.""" - - def __init__(self) -> None: - super().__init__() - - def do_something(self, _arg) -> None: - self.poutput('this is the something command') - - -if __name__ == '__main__': - app = ExampleApp() - app.cmdloop() diff --git a/examples/modular_commands_dynamic.py b/examples/modular_commands_dynamic.py deleted file mode 100755 index 163c9dc8a..000000000 --- a/examples/modular_commands_dynamic.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 -"""Simple example demonstrating dynamic CommandSet loading and unloading. - -There are 2 CommandSets defined. ExampleApp sets the `auto_load_commands` flag to false. - -The `load` and `unload` commands will load and unload the CommandSets. The available commands will change depending -on which CommandSets are loaded -""" - -import argparse - -import cmd2 -from cmd2 import ( - CommandSet, - with_argparser, - with_category, - with_default_category, -) - - -@with_default_category('Fruits') -class LoadableFruits(CommandSet): - def __init__(self) -> None: - super().__init__() - - def do_apple(self, _: cmd2.Statement) -> None: - self._cmd.poutput('Apple') - - def do_banana(self, _: cmd2.Statement) -> None: - self._cmd.poutput('Banana') - - -@with_default_category('Vegetables') -class LoadableVegetables(CommandSet): - def __init__(self) -> None: - super().__init__() - - def do_arugula(self, _: cmd2.Statement) -> None: - self._cmd.poutput('Arugula') - - def do_bokchoy(self, _: cmd2.Statement) -> None: - self._cmd.poutput('Bok Choy') - - -class ExampleApp(cmd2.Cmd): - """CommandSets are loaded via the `load` and `unload` commands.""" - - def __init__(self, *args, **kwargs) -> None: - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, auto_load_commands=False, **kwargs) - - self._fruits = LoadableFruits() - self._vegetables = LoadableVegetables() - - load_parser = cmd2.Cmd2ArgumentParser() - load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) - - @with_argparser(load_parser) - @with_category('Command Loading') - def do_load(self, ns: argparse.Namespace) -> None: - if ns.cmds == 'fruits': - try: - self.register_command_set(self._fruits) - self.poutput('Fruits loaded') - except ValueError: - self.poutput('Fruits already loaded') - - if ns.cmds == 'vegetables': - try: - self.register_command_set(self._vegetables) - self.poutput('Vegetables loaded') - except ValueError: - self.poutput('Vegetables already loaded') - - @with_argparser(load_parser) - def do_unload(self, ns: argparse.Namespace) -> None: - if ns.cmds == 'fruits': - self.unregister_command_set(self._fruits) - self.poutput('Fruits unloaded') - - if ns.cmds == 'vegetables': - self.unregister_command_set(self._vegetables) - self.poutput('Vegetables unloaded') - - -if __name__ == '__main__': - app = ExampleApp() - app.cmdloop() From 39b877a845cce50f4f9ea821158367176a1fb63d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 23 Aug 2025 17:27:20 -0400 Subject: [PATCH 11/13] Added type hints to argparse_example.py and fixed other ruff issues --- examples/argparse_example.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/argparse_example.py b/examples/argparse_example.py index 24d694184..dedad6c9c 100755 --- a/examples/argparse_example.py +++ b/examples/argparse_example.py @@ -27,7 +27,8 @@ class ArgparsingApp(cmd2.Cmd): - def __init__(self, color) -> None: + def __init__(self, color: str) -> None: + """Cmd2 application for demonstrating the use of argparse for command argument parsing.""" super().__init__(include_ipy=True) self.intro = stylize( 'cmd2 has awesome decorators to make it easy to use Argparse to parse command arguments', style=color @@ -54,16 +55,15 @@ def do_fsize(self, args: argparse.Namespace) -> None: return if args.unit == 'KB': - size /= 1024 + size //= 1024 elif args.unit == 'MB': - size /= 1024 * 1024 + size //= 1024 * 1024 else: args.unit = 'bytes' size = round(size, 2) - if args.comma: - size = f'{size:,}' - self.poutput(f'{size} {args.unit}') + size_str = f'{size:,}' if args.comma else f'{size}' + self.poutput(f'{size_str} {args.unit}') # do_pow parser pow_parser = cmd2.Cmd2ArgumentParser() @@ -89,7 +89,7 @@ def do_pow(self, args: argparse.Namespace) -> None: @cmd2.with_argparser(argprint_parser) @cmd2.with_category(ARGPARSE_PRINTING) - def do_print_args(self, args) -> None: + def do_print_args(self, args: argparse.Namespace) -> None: """Print the arpgarse argument list this command was called with.""" self.poutput(f'print_args was called with the following\n\targuments: {args!r}') @@ -100,7 +100,7 @@ def do_print_args(self, args) -> None: @cmd2.with_argparser(unknownprint_parser, with_unknown_args=True) @cmd2.with_category(ARGPARSE_PRINTING) - def do_print_unknown(self, args, unknown) -> None: + def do_print_unknown(self, args: argparse.Namespace, unknown: list[str]) -> None: """Print the arpgarse argument list this command was called with, including unknown arguments.""" self.poutput(f'print_unknown was called with the following arguments\n\tknown: {args!r}\n\tunknown: {unknown}') @@ -158,15 +158,15 @@ def do_print_unknown(self, args, unknown) -> None: sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs) # subcommand functions for the base command - def base_foo(self, args) -> None: + def base_foo(self, args: argparse.Namespace) -> None: """Foo subcommand of base command.""" self.poutput(args.x * args.y) - def base_bar(self, args) -> None: + def base_bar(self, args: argparse.Namespace) -> None: """Bar subcommand of base command.""" self.poutput(f'(({args.z}))') - def base_sport(self, args) -> None: + def base_sport(self, args: argparse.Namespace) -> None: """Sport subcommand of base command.""" self.poutput(f'Sport is {args.sport}') @@ -177,7 +177,7 @@ def base_sport(self, args) -> None: @cmd2.with_argparser(base_parser) @cmd2.with_category(ARGPARSE_SUBCOMMANDS) - def do_base(self, args) -> None: + def do_base(self, args: argparse.Namespace) -> None: """Base command help.""" func = getattr(args, 'func', None) if func is not None: @@ -189,7 +189,7 @@ def do_base(self, args) -> None: @cmd2.with_argparser(base2_parser) @cmd2.with_category(ARGPARSE_SUBCOMMANDS) - def do_alternate(self, args) -> None: + def do_alternate(self, args: argparse.Namespace) -> None: """Alternate command help.""" func = getattr(args, 'func', None) if func is not None: From 8dd77d0e71cd6cd598a2e61e4271b42df6d533f5 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 23 Aug 2025 17:33:09 -0400 Subject: [PATCH 12/13] Fixed comments in examples/command_sets.py --- examples/command_sets.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/command_sets.py b/examples/command_sets.py index 320e71932..ed51c6f4b 100755 --- a/examples/command_sets.py +++ b/examples/command_sets.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""This example revolves around the CommandSet feature for modularizing commands. +"""Example revolving around the CommandSet feature for modularizing commands. It attempts to cover basic usage as well as more complex usage including dynamic loading and unloading of CommandSets, using CommandSets to add subcommands, as well as how to categorize command in CommandSets. Here we have kept the implementation for @@ -32,28 +32,30 @@ @with_default_category(COMMANDSET_BASIC) class AutoLoadCommandSet(CommandSet): def __init__(self) -> None: + """CommandSet class for auto-loading commands at startup.""" super().__init__() def do_hello(self, _: cmd2.Statement) -> None: - """Prints hello.""" + """Print hello.""" self._cmd.poutput('Hello') def do_world(self, _: cmd2.Statement) -> None: - """Prints World.""" + """Print World.""" self._cmd.poutput('World') @with_default_category(COMMANDSET_DYNAMIC) class LoadableFruits(CommandSet): def __init__(self) -> None: + """CommandSet class for dynamically loading commands related to fruits.""" super().__init__() def do_apple(self, _: cmd2.Statement) -> None: - """Prints Apple.""" + """Print Apple.""" self._cmd.poutput('Apple') def do_banana(self, _: cmd2.Statement) -> None: - """Prints Banana""" + """Print Banana.""" self._cmd.poutput('Banana') banana_description = "Cut a banana" @@ -69,14 +71,15 @@ def cut_banana(self, ns: argparse.Namespace) -> None: @with_default_category(COMMANDSET_DYNAMIC) class LoadableVegetables(CommandSet): def __init__(self) -> None: + """CommandSet class for dynamically loading commands related to vegetables.""" super().__init__() def do_arugula(self, _: cmd2.Statement) -> None: - "Prints Arguula." + "Print Arguula." self._cmd.poutput('Arugula') def do_bokchoy(self, _: cmd2.Statement) -> None: - """Prints Bok Choy.""" + """Print Bok Choy.""" self._cmd.poutput('Bok Choy') bokchoy_description = "Cut some bokchoy" @@ -85,6 +88,7 @@ def do_bokchoy(self, _: cmd2.Statement) -> None: @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser, help=bokchoy_description.lower()) def cut_bokchoy(self, ns: argparse.Namespace) -> None: + """Cut bokchoy.""" self._cmd.poutput('Bok Choy: ' + ns.style) @@ -92,6 +96,7 @@ class CommandSetApp(cmd2.Cmd): """CommandSets are automatically loaded. Nothing needs to be done.""" def __init__(self) -> None: + """Cmd2 application for demonstrating the CommandSet features.""" # This prevents all CommandSets from auto-loading, which is necessary if you don't want some to load at startup super().__init__(auto_load_commands=False) @@ -109,6 +114,7 @@ def __init__(self) -> None: @with_argparser(load_parser) @with_category(COMMANDSET_LOAD_UNLOAD) def do_load(self, ns: argparse.Namespace) -> None: + """Load a CommandSet at runtime.""" if ns.cmds == 'fruits': try: self.register_command_set(self._fruits) @@ -126,6 +132,7 @@ def do_load(self, ns: argparse.Namespace) -> None: @with_argparser(load_parser) @with_category(COMMANDSET_LOAD_UNLOAD) def do_unload(self, ns: argparse.Namespace) -> None: + """Unload a CommandSet at runtime.""" if ns.cmds == 'fruits': self.unregister_command_set(self._fruits) self.poutput('Fruits unloaded') From 8e0a67e2889be4f7056ff33684144baea116d7d9 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 23 Aug 2025 17:37:52 -0400 Subject: [PATCH 13/13] Fixed strings and types in getting_started.py --- examples/getting_started.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/getting_started.py b/examples/getting_started.py index ad24fde39..19c57aa0d 100755 --- a/examples/getting_started.py +++ b/examples/getting_started.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -"""A simple example cmd2 application demonstrating the following: +"""A simple example cmd2 application demonstrating many common features. + +Features demonstrated include all of the following: 1) Colorizing/stylizing output 2) Using multiline commands 3) Persistent history @@ -25,9 +27,12 @@ class BasicApp(cmd2.Cmd): + """Cmd2 application to demonstrate many common features.""" + CUSTOM_CATEGORY = 'My Custom Commands' def __init__(self) -> None: + """Initialize the cmd2 application.""" # Startup script that defines a couple aliases for running shell commands alias_script = pathlib.Path(__file__).absolute().parent / '.cmd2rc' @@ -39,7 +44,7 @@ def __init__(self) -> None: multiline_commands=['echo'], persistent_history_file='cmd2_history.dat', shortcuts=shortcuts, - startup_script=alias_script, + startup_script=str(alias_script), ) # Prints an intro banner once upon application startup @@ -82,7 +87,7 @@ def do_intro(self, _: cmd2.Statement) -> None: @cmd2.with_category(CUSTOM_CATEGORY) def do_echo(self, arg: cmd2.Statement) -> None: - """Example of a multiline command.""" + """Multiline command.""" self.poutput( stylize( arg,