From 4af7b06be9ae37a851430ab70be67aa88c3e627b Mon Sep 17 00:00:00 2001 From: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:38:47 +0700 Subject: [PATCH] Print help caused by a command line error on stderr Argument parsing errors printed the full help message to stdout while only the one line error went to stderr. A mistyped command therefore wrote several kilobytes of help into redirected or piped output, e.g. `b2 sync > out.txt` left 9.6 kB of help text in out.txt. Help requested explicitly with --help/--help-all keeps going to stdout; only the help that accompanies an error moves to stderr. --- b2/_internal/arg_parser.py | 4 +++- changelog.d/493.fixed.md | 1 + test/unit/console_tool/test_help.py | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog.d/493.fixed.md diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index 52a27245a..61a559f53 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -146,7 +146,9 @@ def _get_short_description(self) -> str: return '' def error(self, message): - self.print_help() + # Help printed because of a command line syntax error is part of the error + # report, not of the command output, hence it goes to stderr. + self.print_help(sys.stderr) self.exit(2, f'\n{self.prog}: error: {message}\n') diff --git a/changelog.d/493.fixed.md b/changelog.d/493.fixed.md new file mode 100644 index 000000000..e667be3b5 --- /dev/null +++ b/changelog.d/493.fixed.md @@ -0,0 +1 @@ +Print help triggered by a command line syntax error on stderr instead of stdout, so that a mistyped command no longer pollutes redirected or piped output. diff --git a/test/unit/console_tool/test_help.py b/test/unit/console_tool/test_help.py index 0d72c1b8c..8c7ec51c7 100644 --- a/test/unit/console_tool/test_help.py +++ b/test/unit/console_tool/test_help.py @@ -41,3 +41,29 @@ def test_help(b2_cli, flag, included, excluded, capsys): found.add(e) assert found.issuperset(included), f'expected {included!r} in {out!r}' assert found.isdisjoint(excluded), f'expected {excluded!r} not in {out!r}' + + +@pytest.mark.parametrize( + 'argv', + [ + pytest.param(['--nonexistent-flag'], id='unrecognized-argument'), + pytest.param(['sync'], id='missing-required-arguments'), + pytest.param(['sync', '--nonexistent-flag'], id='unrecognized-subcommand-argument'), + ], +) +def test_help_on_command_line_error_goes_to_stderr(b2_cli, argv, capsys): + """Help printed because of a command line error belongs on stderr, not stdout.""" + b2_cli.run(argv, expected_status=2, expected_stdout=None) + + captured = capsys.readouterr() + assert captured.out == '' + assert 'error:' in captured.err + assert '-h, --help' in captured.err + + +def test_help_on_request_goes_to_stdout(b2_cli, capsys): + b2_cli.run(['--help'], expected_stdout=None) + + captured = capsys.readouterr() + assert '-h, --help' in captured.out + assert captured.err == ''