Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
65 changes: 48 additions & 17 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
# 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 @@ -286,7 +285,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 +303,9 @@ class Cmd(cmd.Cmd):
# List for storing transcript test file names
testfiles: ClassVar[list[str]] = []

DEFAULT_PROMPT = '(Cmd) '
IDENTCHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'

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

Expand Down Expand Up @@ -376,6 +379,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 +388,29 @@ 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.identchars = Cmd.IDENTCHARS
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 @@ -3086,7 +3108,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 +3163,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 +3315,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 +4166,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 +4671,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 +5390,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 +5416,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 +5911,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 +5953,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`. It is API compatible, but
the `cmd2.Cmd` class no longer inherits from `cmd.Cmd`.

## 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
Loading