Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ prompt is displayed.
treated as command output and sent to `self.stdout`, allowing them to be captured.
- Verbose help table descriptions are no longer generated from help function output. The system
now relies exclusively on command function docstrings.
- Removed `feedback_to_output` settable and changed `cmd2.Cmd.pfeedback` to always print to
`self.stdout`
- Enhancements
- New `cmd2.Cmd` parameters
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These
Expand Down Expand Up @@ -163,6 +165,11 @@ prompt is displayed.
- `Cmd2Style.COMPLETION_MENU_META` - Style for "meta" information shown alongside a
completion
- `Cmd2Style.COMPLETION_MENU_META_CURRENT`- Style for meta info of current item
- Updated `set` command to consolidate its confirmation output into a single, colorized line.
The confirmation now uses `pfeedback()`, allowing it to be silenced when the `quiet` settable
is enabled.
- `alias` and `macro` subcommands for `create` and `delete` now output their non-essential
success case output using `pfeedback`

## 3.5.1 (April 24, 2026)

Expand Down
70 changes: 28 additions & 42 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,6 @@ def __init__(
self.debug = False
self.echo = False
self.editor = self.DEFAULT_EDITOR
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
self.quiet = False # Do not suppress nonessential output
self.scripts_add_to_history = True # Scripts and pyscripts add commands to history
self.timing = False # Prints elapsed time for each command
Expand Down Expand Up @@ -1370,7 +1369,6 @@ def allow_style_type(value: str) -> ru.AllowStyle:
self.add_settable(Settable("debug", bool, "Show full traceback on exception", self))
self.add_settable(Settable("echo", bool, "Echo command issued into output", self))
self.add_settable(Settable("editor", str, "Program used by 'edit'", self))
self.add_settable(Settable("feedback_to_output", bool, "Include nonessentials in '|' and '>' results", self))
self.add_settable(
Settable(
"max_completion_table_items",
Expand Down Expand Up @@ -1754,40 +1752,23 @@ def pfeedback(
rich_print_kwargs: Mapping[str, Any] | None = None,
**kwargs: Any, # noqa: ARG002
) -> None:
"""Print nonessential feedback.

The output can be silenced with the `quiet` setting and its inclusion in redirected output
is controlled by the `feedback_to_output` setting.
"""Print nonessential feedback where the output can be silenced with the `quiet` setting.

For details on the parameters, refer to the `print_to` method documentation.
"""
if not self.quiet:
if self.feedback_to_output:
self.poutput(
*objects,
sep=sep,
end=end,
style=style,
soft_wrap=soft_wrap,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
rich_print_kwargs=rich_print_kwargs,
)
else:
self.perror(
*objects,
sep=sep,
end=end,
style=style,
soft_wrap=soft_wrap,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
rich_print_kwargs=rich_print_kwargs,
)
self.poutput(
*objects,
sep=sep,
end=end,
style=style,
soft_wrap=soft_wrap,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
rich_print_kwargs=rich_print_kwargs,
)

def ppaged(
self,
Expand Down Expand Up @@ -2992,7 +2973,7 @@ def onecmd_plus_hooks(
stop = self.postcmd(stop, statement)

if self.timing:
self.pfeedback(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}")
self.perror(f"Elapsed: {datetime.datetime.now(tz=datetime.timezone.utc) - timestart}", style=None)
finally:
# Get sigint protection while we restore stuff
with self.sigint_protection:
Expand Down Expand Up @@ -3862,7 +3843,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:

# Set the alias
result = "overwritten" if args.name in self.aliases else "created"
self.poutput(f"Alias '{args.name}' {result}")
self.pfeedback(f"Alias '{args.name}' {result}")

self.aliases[args.name] = value
self.last_result = True
Expand Down Expand Up @@ -3891,15 +3872,15 @@ def _alias_delete(self, args: argparse.Namespace) -> None:

if args.all:
self.aliases.clear()
self.poutput("All aliases deleted")
self.pfeedback("All aliases deleted")
elif not args.names:
self.perror("Either --all or alias name(s) must be specified")
self.last_result = False
else:
for cur_name in utils.remove_duplicates(args.names):
if cur_name in self.aliases:
del self.aliases[cur_name]
self.poutput(f"Alias '{cur_name}' deleted")
self.pfeedback(f"Alias '{cur_name}' deleted")
else:
self.perror(f"Alias '{cur_name}' does not exist")

Expand Down Expand Up @@ -4028,7 +4009,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser:
"When the macro is called, the provided arguments are resolved and the assembled command is run. For example:",
"\n\n",
(" my_macro beef broccoli", Cmd2Style.COMMAND_LINE),
(" ───> ", Style(bold=True)),
(" ─> ", Style(bold=True)),
("make_dinner --meat beef --veggie broccoli", Cmd2Style.COMMAND_LINE),
)
macro_create_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_create_description)
Expand Down Expand Up @@ -4150,7 +4131,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:

# Set the macro
result = "overwritten" if args.name in self.macros else "created"
self.poutput(f"Macro '{args.name}' {result}")
self.pfeedback(f"Macro '{args.name}' {result}")

self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, args=macro_args)
self.last_result = True
Expand Down Expand Up @@ -4179,15 +4160,15 @@ def _macro_delete(self, args: argparse.Namespace) -> None:

if args.all:
self.macros.clear()
self.poutput("All macros deleted")
self.pfeedback("All macros deleted")
elif not args.names:
self.perror("Either --all or macro name(s) must be specified")
self.last_result = False
else:
for cur_name in utils.remove_duplicates(args.names):
if cur_name in self.macros:
del self.macros[cur_name]
self.poutput(f"Macro '{cur_name}' deleted")
self.pfeedback(f"Macro '{cur_name}' deleted")
else:
self.perror(f"Macro '{cur_name}' does not exist")

Expand Down Expand Up @@ -4737,12 +4718,17 @@ def do_set(self, args: argparse.Namespace) -> None:
if args.value:
# Try to update the settable's value
try:
orig_value = settable.value
settable.value = su.strip_quotes(args.value)
except ValueError as ex:
self.perror(f"Error setting {args.param}: {ex}")
else:
self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {settable.value!r}")
# Create the feedback message using Rich Text for color
feedback_msg = Text.assemble(
f"{args.param} ─> ",
(f"{settable.value!r}", "green"),
)
self.pfeedback(feedback_msg)

Comment thread
tleonhardt marked this conversation as resolved.
self.last_result = True
return

Expand Down
1 change: 0 additions & 1 deletion docs/features/builtin_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ application:
debug False Show full traceback on exception
echo False Echo command issued into output
editor vim Program used by 'edit'
feedback_to_output False Include nonessentials in '|' and '>' results
max_column_completion_results 7 Maximum number of completion results to display in a single column
max_completion_table_items 50 Maximum number of completion results allowed for a completion table to appear
quiet False Don't print nonessential feedback
Expand Down
12 changes: 6 additions & 6 deletions docs/features/generating_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ output.

You may have the need to display information to the user which is not intended to be part of the
generated output. This could be debugging information or status information about the progress of
long running commands. It's not output, it's not error messages, it's feedback. If you use the
long running commands. It's not output, it's not error messages, it's status. If you use the
[Timing](./settings.md#timing) setting, the output of how long it took the command to run will be
output as feedback. You can use the [pfeedback][cmd2.Cmd.pfeedback] method to produce this type of
output, and several [Settings](./settings.md) control how it is handled.
output as to `stderr` without any styling. You can use the [perror][cmd2.Cmd.perror] method to
produce this type of output by passing it `style=None`.

If the [quiet](./settings.md#quiet) setting is `True`, then calling `cmd2.Cmd.pfeedback` produces no
output. If [quiet](./settings.md#quiet) is `False`, the
[feedback_to_output](./settings.md#feedback_to_output) setting is consulted to determine whether to
send the output to `stdout` or `stderr`.
output. If [quiet](./settings.md#quiet) is `False`,the `pfeedback` method sends output to `stdout`.
Hence, `pfeedback` is useful for non-essential output that you want the ability to silence when
`quiet` is `True`.

## Exceptions

Expand Down
1 change: 0 additions & 1 deletion docs/features/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ Here are instance attributes of `cmd2.Cmd` which developers might wish to overri
- **editor**: text editor program to use with _edit_ command (e.g. `vim`)
- **exclude_from_history**: commands to exclude from the _history_ command
- **exit_code**: this determines the value returned by `cmdloop()` when exiting the application
- **feedback_to_output**: if `True`, send nonessential output to stdout, if `False` send them to stderr (Default: `False`)
- **help_error**: the error that prints when no help information can be found
- **hidden_commands**: commands to exclude from the help menu and tab completion
- **last_result**: stores results from the last command run to enable usage of results in a Python script or interactive console. Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
Expand Down
13 changes: 2 additions & 11 deletions docs/features/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,6 @@ the prompt.
Similar to the `EDITOR` shell variable, this setting contains the name of the program which should
be run by the [edit](./builtin_commands.md#edit) command.

### feedback_to_output

Controls whether feedback generated with the `cmd2.Cmd.pfeedback` method is sent to `self.stdout` or
`sys.stderr`. If `False` the output will be sent to `sys.stderr`

If `True` the output is sent to `stdout` (which is often the screen but may be
[redirected](./redirection.md#output-redirection-and-pipes)). The feedback output will be mixed in
with and indistinguishable from output generated with `cmd2.Cmd.poutput`.

### max_completion_table_items

The maximum number of items to display in a completion table. A completion table is a special kind
Expand All @@ -74,8 +65,8 @@ appear.

### quiet

If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the
[feedback_to_output](#feedback_to_output) setting controls where the output is sent.
If `True`, output generated by calling `cmd2.Cmd.pfeedback` is suppressed. If `False`, the output is
sent to `stdout`.

### scripts_add_to_history

Expand Down
75 changes: 24 additions & 51 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,17 @@ def test_base_set(base_app) -> None:


def test_set(base_app) -> None:
out, _err = run_cmd(base_app, "set quiet True")
expected = normalize(
"""
quiet - was: False
now: True
"""
)
assert out == expected
out, err = run_cmd(base_app, "set quiet True")
assert not out
assert base_app.last_result is True

# Test quiet respect
out, err = run_cmd(base_app, "set timing False")
assert not out
assert not err
assert base_app.last_result is True

# Show one settable (this always goes to out)
line_found = False
out, _err = run_cmd(base_app, "set quiet")
for line in out:
Expand All @@ -180,6 +181,7 @@ def test_set(base_app) -> None:
assert line_found
assert len(base_app.last_result) == 1
assert base_app.last_result["quiet"] is True
base_app.quiet = False


def test_set_val_empty(base_app) -> None:
Expand Down Expand Up @@ -236,8 +238,8 @@ def test_set_allow_style(base_app, new_val, is_valid, expected) -> None:
# Verify the results
assert expected == ru.ALLOW_STYLE
if is_valid:
assert not err
assert out
assert not err


def test_set_traceback_show_locals(base_app: cmd2.Cmd) -> None:
Expand Down Expand Up @@ -275,9 +277,11 @@ def test_set_with_choices(base_app) -> None:
base_app.add_settable(fake_settable)

# Try a valid choice
_out, err = run_cmd(base_app, f"set fake {fake_choices[1]}")
out, err = run_cmd(base_app, f"set fake {fake_choices[1]}")
assert base_app.last_result is True
assert not err
assert out[0].startswith("fake")
assert out[0].endswith(f"─> {fake_choices[1]!r}")

# Try an invalid choice
_out, err = run_cmd(base_app, "set fake bad_value")
Expand All @@ -301,15 +305,10 @@ def onchange_app():


def test_set_onchange_hook(onchange_app) -> None:
out, _err = run_cmd(onchange_app, "set quiet True")
expected = normalize(
"""
You changed quiet
quiet - was: False
now: True
"""
)
assert out == expected
out, err = run_cmd(onchange_app, "set quiet True")
assert out == ["You changed quiet"]
# quiet: False -> True is not shown because quiet is now True
assert not err
assert onchange_app.last_result is True


Expand Down Expand Up @@ -727,8 +726,7 @@ def test_output_redirection_to_too_long_filename(redirection_app) -> None:
assert "Failed to redirect" in err[0]


def test_feedback_to_output_true(redirection_app) -> None:
redirection_app.feedback_to_output = True
def test_feedback(redirection_app) -> None:
f, filename = tempfile.mkstemp(prefix="cmd2_test", suffix=".txt")
os.close(f)

Expand All @@ -741,22 +739,6 @@ def test_feedback_to_output_true(redirection_app) -> None:
os.remove(filename)


def test_feedback_to_output_false(redirection_app) -> None:
redirection_app.feedback_to_output = False
f, filename = tempfile.mkstemp(prefix="feedback_to_output", suffix=".txt")
os.close(f)

try:
_out, err = run_cmd(redirection_app, f"print_feedback > {filename}")

with open(filename) as f:
content = f.read().splitlines()
assert not content
assert "feedback" in err
finally:
os.remove(filename)


def test_disallow_redirection(redirection_app: RedirectionApp, capsys: pytest.CaptureFixture[str]) -> None:
# Set allow_redirection to False
redirection_app.allow_redirection = False
Expand Down Expand Up @@ -873,14 +855,9 @@ def test_allow_clipboard(base_app) -> None:


def test_base_timing(base_app) -> None:
base_app.feedback_to_output = False
out, err = run_cmd(base_app, "set timing True")
expected = normalize(
"""timing - was: False
now: True
"""
)
assert out == expected
assert out[0].startswith("timing")
assert out[0].endswith("─> True")

if sys.platform == "win32":
assert err[0].startswith("Elapsed: 0:00:00")
Expand All @@ -899,13 +876,9 @@ def test_base_debug(base_app) -> None:

# Set debug true
out, err = run_cmd(base_app, "set debug True")
expected = normalize(
"""
debug - was: False
now: True
"""
)
assert out == expected
assert not err
assert out[0].startswith("debug")
assert out[0].endswith("─> True")

# Verify that we now see the exception traceback
out, err = run_cmd(base_app, "edit")
Expand Down
Loading
Loading