Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 4.0.0 (TBD)

- Breaking Changes
- `cmd2` no longer has a dependency on `cmd` and `cmd2.Cmd` no longer inherits from `cmd.Cmd`

## 3.0.0 (December 7, 2025)

### Summary
Expand Down
105 changes: 66 additions & 39 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
"""Variant on standard library's cmd with extra features.

To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you
were using the standard library's cmd, while enjoying the extra features.

Searchable command history (commands: "history")
Run commands from file, save to file, edit commands in file
Multi-line commands
Special-character shortcut commands (beyond cmd's "?" and "!")
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/transcript_example.py)
Bash-style ``select`` available
"""cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python.

cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for
developers to build feature-rich and user-friendly interactive command line applications. It provides a simple API which
is an extension of Python's built-in cmd module. cmd2 provides a wealth of features on top of cmd to make your life easier
and eliminates much of the boilerplate code which would be necessary when using cmd.

Extra features include:
- Searchable command history (commands: "history")
- Run commands from file, save to file, edit commands in file
- Multi-line commands
- Special-character shortcut commands (beyond cmd's "?" and "!")
- 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/transcript_example.py)
- Bash-style ``select`` available

Note, if self.stdout is different than sys.stdout, then redirection with > and |
will only work if `self.poutput()` is used in place of `print`.

- Catherine Devlin, Jan 03 2008 - catherinedevlin.blogspot.com

Git repository on GitHub at https://github.com/python-cmd2/cmd2
GitHub: https://github.com/python-cmd2/cmd2
Documentation: https://cmd2.readthedocs.io/
"""

# This module has many imports, quite a few of which are only
# infrequently utilized. To reduce the initial overhead of
# import this module, many of these imports are lazy-loaded
# i.e. we only import the module when we use it.
import argparse
import cmd
import contextlib
import copy
import functools
Expand Down Expand Up @@ -64,7 +65,7 @@
)

import rich.box
from rich.console import Group
from rich.console import Group, RenderableType
from rich.highlighter import ReprHighlighter
from rich.rule import Rule
from rich.style import Style, StyleType
Expand Down Expand Up @@ -286,7 +287,7 @@ def remove(self, command_method: CommandFunc) -> None:
del self._parsers[full_method_name]


class Cmd(cmd.Cmd):
class Cmd:
"""An easy but powerful framework for writing line-oriented command interpreters.

Extends the Python Standard Library's cmd package by adding a lot of useful features
Expand All @@ -304,6 +305,8 @@ class Cmd(cmd.Cmd):
# List for storing transcript test file names
testfiles: ClassVar[list[str]] = []

DEFAULT_PROMPT = '(Cmd) '

def __init__(
self,
completekey: str = 'tab',
Expand All @@ -326,6 +329,7 @@ def __init__(
auto_load_commands: bool = False,
allow_clipboard: bool = True,
suggest_similar_command: bool = False,
intro: RenderableType = '',
) -> None:
"""Easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.

Expand Down Expand Up @@ -376,6 +380,7 @@ def __init__(
:param suggest_similar_command: If ``True``, ``cmd2`` will attempt to suggest the most
similar command when the user types a command that does
not exist. Default: ``False``.
"param intro: Intro banner to print when starting the application.
"""
# Check if py or ipy need to be disabled in this instance
if not include_py:
Expand All @@ -384,11 +389,28 @@ def __init__(
setattr(self, 'do_ipy', None) # noqa: B010

# initialize plugin system
# needs to be done before we call __init__(0)
# needs to be done before we most of the other stuff below
self._initialize_plugin_system()

# Call super class constructor
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
# Configure a few defaults
self.prompt = Cmd.DEFAULT_PROMPT
self.intro = intro
self.use_rawinput = True

# What to use for standard input
if stdin is not None:
self.stdin = stdin
else:
self.stdin = sys.stdin

# What to use for standard output
if stdout is not None:
self.stdout = stdout
else:
self.stdout = sys.stdout

# Key used for tab completion
self.completekey = completekey

# Attributes which should NOT be dynamically settable via the set command at runtime
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
Expand Down Expand Up @@ -2693,10 +2715,6 @@ def postloop(self) -> None:
def parseline(self, line: str) -> tuple[str, str, str]:
"""Parse the line into a command name and a string containing the arguments.

NOTE: This is an override of a parent class method. It is only used by other parent class methods.

Different from the parent class method, this ignores self.identchars.

:param line: line read by readline
:return: tuple containing (command, args, line)
"""
Expand Down Expand Up @@ -3086,7 +3104,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:

# Initialize the redirection saved state
redir_saved_state = utils.RedirectionSavedState(
cast(TextIO, self.stdout), stdouts_match, self._cur_pipe_proc_reader, self._redirecting
self.stdout, stdouts_match, self._cur_pipe_proc_reader, self._redirecting
)

# The ProcReader for this command
Expand Down Expand Up @@ -3141,7 +3159,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
new_stdout.close()
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
redir_saved_state.redirecting = True
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
cmd_pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)

self.stdout = new_stdout
if stdouts_match:
Expand Down Expand Up @@ -3293,6 +3311,19 @@ def default(self, statement: Statement) -> bool | None: # type: ignore[override
self.perror(err_msg, style=None)
return None

def completedefault(self, *_ignored: list[str]) -> list[str]:
"""Call to complete an input line when no command-specific complete_*() method is available.

By default, it returns an empty list.

"""
return []

def completenames(self, text: str, *_ignored: list[str]) -> list[str]:
"""Help provide tab-completion options for command names."""
dotext = 'do_' + text
return [a[3:] for a in self.get_names() if a.startswith(dotext)]

def _suggest_similar_command(self, command: str) -> str | None:
return suggest_similar(command, self.get_visible_commands())

Expand Down Expand Up @@ -4131,10 +4162,6 @@ def _build_help_parser(cls) -> Cmd2ArgumentParser:
)
return help_parser

# Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
if getattr(cmd.Cmd, 'complete_help', None) is not None:
delattr(cmd.Cmd, 'complete_help')

@with_argparser(_build_help_parser)
def do_help(self, args: argparse.Namespace) -> None:
"""List available commands or provide detailed help for a specific command."""
Expand Down Expand Up @@ -4640,7 +4667,7 @@ def do_shell(self, args: argparse.Namespace) -> None:
**kwargs,
)

proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
proc_reader.wait()

# Save the return code of the application for use in a pyscript
Expand Down Expand Up @@ -5359,7 +5386,7 @@ def _generate_transcript(
transcript += command

# Use a StdSim object to capture output
stdsim = utils.StdSim(cast(TextIO, self.stdout))
stdsim = utils.StdSim(self.stdout)
self.stdout = cast(TextIO, stdsim)

# then run the command and let the output go into our buffer
Expand All @@ -5385,7 +5412,7 @@ def _generate_transcript(
with self.sigint_protection:
# Restore altered attributes to their original state
self.echo = saved_echo
self.stdout = cast(TextIO, saved_stdout)
self.stdout = saved_stdout

# Check if all commands ran
if commands_run < len(history):
Expand Down Expand Up @@ -5880,7 +5907,7 @@ def _report_disabled_command_usage(self, *_args: Any, message_to_print: str, **_
"""
self.perror(message_to_print, style=None)

def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override]
def cmdloop(self, intro: str = '') -> int: # type: ignore[override]
"""Deal with extra features provided by cmd2, this is an outer wrapper around _cmdloop().

_cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with
Expand Down Expand Up @@ -5922,11 +5949,11 @@ def cmdloop(self, intro: str | None = None) -> int: # type: ignore[override]
self._run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
else:
# If an intro was supplied in the method call, allow it to override the default
if intro is not None:
if intro:
self.intro = intro

# Print the intro, if there is one, right after the preloop
if self.intro is not None:
if self.intro:
self.poutput(self.intro)

# And then call _cmdloop() to enter the main loop
Expand Down
2 changes: 1 addition & 1 deletion cmd2/py_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def __call__(self, command: str, *, echo: bool | None = None) -> CommandResult:
)
finally:
with self._cmd2_app.sigint_protection:
self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream)
self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout.inner_stream)
if stdouts_match:
sys.stdout = self._cmd2_app.stdout

Expand Down
2 changes: 1 addition & 1 deletion cmd2/transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def setUp(self) -> None:

# Trap stdout
self._orig_stdout = self.cmdapp.stdout
self.cmdapp.stdout = cast(TextIO, utils.StdSim(cast(TextIO, self.cmdapp.stdout)))
self.cmdapp.stdout = cast(TextIO, utils.StdSim(self.cmdapp.stdout))

def tearDown(self) -> None:
"""Instructions that will be executed after each test method."""
Expand Down
5 changes: 5 additions & 0 deletions docs/migrating/why.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ of [cmd][cmd] will add many features to an application without any further modif
to `cmd2` will also open many additional doors for making it possible for developers to provide a
top-notch interactive command-line experience for their users.

!!! warning

As of version 4.0.0, `cmd2` does not have an actual dependency on `cmd`. `cmd2` is mostly API compatible with `cmd2`.
See [Incompatibilities](./incompatibilities.md) for the few documented incompatibilities.

## Automatic Features

After switching from [cmd][cmd] to `cmd2`, your application will have the following new features and
Expand Down
1 change: 0 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ plugins:
show_if_no_docstring: true
preload_modules:
- argparse
- cmd
inherited_members: true
members_order: source
separate_signature: true
Expand Down
15 changes: 15 additions & 0 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2106,6 +2106,21 @@ def make_app(isatty: bool, empty_input: bool = False):
assert not out


def test_custom_stdout() -> None:
# Create a custom file-like object (e.g., an in-memory string buffer)
custom_output = io.StringIO()

# Instantiate cmd2.Cmd with the custom_output as stdout
my_app = cmd2.Cmd(stdout=custom_output)

# Simulate a command
my_app.onecmd('help')

# Retrieve the output from the custom_output buffer
captured_output = custom_output.getvalue()
assert 'history' in captured_output


def test_read_command_line_eof(base_app, monkeypatch) -> None:
read_input_mock = mock.MagicMock(name='read_input', side_effect=EOFError)
monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)
Expand Down
Loading