diff --git a/craft_cli/helptexts.py b/craft_cli/helptexts.py index b66ef8f..77c06bc 100644 --- a/craft_cli/helptexts.py +++ b/craft_cli/helptexts.py @@ -276,6 +276,7 @@ def _build_plain_command_help( self, usage: str, overview: str, + parameters: list[tuple[str, str]], options: list[tuple[str, str]], other_command_names: list[str], ) -> list[str]: @@ -285,6 +286,7 @@ def _build_plain_command_help( - usage - summary + - positional arguments (only if parameters are not empty) - options - other related commands - footer @@ -306,6 +308,13 @@ def _build_plain_command_help( # column alignment is dictated by longest options title max_title_len = max(len(title) for title, text in options) + if parameters: + # command positional arguments + positional_args_lines = ["Positional arguments:"] + for title, text in parameters: + positional_args_lines.extend(_build_item_plain(title, text, max_title_len)) + textblocks.append("\n".join(positional_args_lines)) + # command options option_lines = ["Options:"] for title, text in options: @@ -330,6 +339,7 @@ def _build_markdown_command_help( self, usage: str, overview: str, + parameters: list[tuple[str, str]], options: list[tuple[str, str]], other_command_names: list[str], ) -> list[str]: @@ -339,6 +349,7 @@ def _build_markdown_command_help( - usage - summary + - positional arguments (only if parameters are not empty) - options - other related commands - footer @@ -359,6 +370,17 @@ def _build_markdown_command_help( overview = process_overview_for_markdown(overview) textblocks.append(f"## Summary:\n\n{overview}") + if parameters: + parameters_lines = [ + "## Positional arguments:", + "| | |", + "|-|-|", + ] + for title, text in parameters: + parameters_lines.append(f"| `{title}` | {text} |") + + textblocks.append("\n".join(parameters_lines)) + option_lines = [ "## Options:", "| | |", @@ -403,11 +425,11 @@ def get_command_help( if name[0] == "-": options.append((name, title)) else: - parameters.append(name) + parameters.append((name, title)) usage = f"{self.appname} {command.name} [options]" if parameters: - usage += " " + " ".join(f"<{parameter}>" for parameter in parameters) + usage += " " + " ".join(f"<{parameter[0]}>" for parameter in parameters) for command_group in self.command_groups: if any(isinstance(command, command_class) for command_class in command_group.commands): @@ -422,7 +444,7 @@ def get_command_help( builder = self._build_markdown_command_help else: builder = self._build_plain_command_help - textblocks = builder(usage, command.overview, options, other_command_names) + textblocks = builder(usage, command.overview, parameters, options, other_command_names) # join all stripped blocks, leaving ONE empty blank line between return "\n\n".join(block.strip() for block in textblocks) + "\n" diff --git a/craft_cli/messages.py b/craft_cli/messages.py index c873b96..bff596e 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -710,7 +710,7 @@ def ended_ok(self) -> None: """Finish the messaging system gracefully.""" self._stop() - def _report_error(self, error: errors.CraftError) -> None: + def _report_error(self, error: errors.CraftError) -> None: # noqa: PLR0912 (too many branches) """Report the different message lines from a CraftError.""" if self._mode in (EmitterMode.QUIET, EmitterMode.BRIEF, EmitterMode.VERBOSE): use_timestamp = False @@ -719,8 +719,10 @@ def _report_error(self, error: errors.CraftError) -> None: use_timestamp = True full_stream = sys.stderr - # the initial message - self._printer.show(sys.stderr, str(error), use_timestamp=use_timestamp, end_line=True) + # The initial message. Print every line individually to correctly clear + # previous lines, if necessary. + for line in str(error).splitlines(): + self._printer.show(sys.stderr, line, use_timestamp=use_timestamp, end_line=True) if isinstance(error, errors.CraftCommandError): stderr = error.stderr diff --git a/docs/changelog.rst b/docs/changelog.rst index 9398d4e..dbdb8fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,8 +5,13 @@ Changelog See the `Releases page`_ on GitHub for a complete list of commits that are included in each version. -2.6.0 (2024-02-07) ------------------- +2.7.0 (2024-Sep-05) +------------------- +- Add a new ``CraftCommandError`` class for errors that wrap command output +- Fix the reporting of error messages containing multiple lines + +2.6.0 (2024-Jul-02) +------------------- - Disable exception chaining for help/usage exceptions - Support a doc slug in CraftError in addition to full urls diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 5b4c9d4..fc53703 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -143,6 +143,9 @@ Ask help for specifically for the command:: It will return successfully if the file was properly removed. + Positional arguments: + filepath: The file to be removed + Options: -h, --help: Show this help message and exit -v, --verbose: Show debug information and be more verbose diff --git a/examples.py b/examples.py index c842a1a..c432fb8 100755 --- a/examples.py +++ b/examples.py @@ -513,6 +513,15 @@ def example_30(): time.sleep(0.001) +def example_31(): + """Multiline error message.""" + emit.progress("Setting up computer for build...") + time.sleep(1) + emit.progress("A long progress message") + time.sleep(6) + raise CraftError("Error 1\nError 2") + + # -- end of test cases if len(sys.argv) < 2: diff --git a/tests/integration/test_messages_integration.py b/tests/integration/test_messages_integration.py index 592e1fd..9ff19c3 100644 --- a/tests/integration/test_messages_integration.py +++ b/tests/integration/test_messages_integration.py @@ -1013,6 +1013,23 @@ def test_error_unexpected_debugish(capsys, mode): assert_outputs(capsys, emit, expected_err=expected, expected_log=expected) +@pytest.mark.parametrize("output_is_terminal", [True]) +def test_error_multiline_brief(capsys): + emit = Emitter() + emit.init(EmitterMode.BRIEF, "testapp", GREETING) + emit.progress("A very long message detailing the current task.") + error = CraftError("Error line 1.\nError line 2.", logpath_report=False) + emit.error(error) + + expected = [ + Line("A very long message detailing the current task.", permanent=False), + # The error message is split on two separate lines. + Line("Error line 1.", permanent=True), + Line("Error line 2.", permanent=True), + ] + assert_outputs(capsys, emit, expected_err=expected, expected_log=expected) + + @pytest.mark.parametrize( "mode", [ diff --git a/tests/unit/test_help.py b/tests/unit/test_help.py index cc89a36..42b2edb 100644 --- a/tests/unit/test_help.py +++ b/tests/unit/test_help.py @@ -417,6 +417,10 @@ def test_command_help_text_with_parameters(output_format): Summary: Quite some long text. + Positional arguments: + name: The name of the charm. + extraparam: Another parameter.. + Options: -h, --help: Show this help message and exit. --revision: The revision to release (defaults to latest). @@ -439,6 +443,12 @@ def test_command_help_text_with_parameters(output_format): Quite some long text. + ## Positional arguments: + | | | + |-|-| + | `name` | The name of the charm. | + | `extraparam` | Another parameter.. | + ## Options: | | | |-|-|