diff --git a/README.md b/README.md index c65e33e..b4b9fac 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,16 @@ The `Invocation` object contains: - `path`: A list of strings that contains the path to the command that was called. - `command`: The command that was called. +### Aborting a command + +Use `invocation.cli.ui.abort` to report an error and stop a command. Aborting +unwinds the stack, so associated `finally` blocks are executed. `Command.run` +then terminates the process with exit code 1. + +Applications that need to perform work after an abort can use +`Command.run-for-exit-code`. It returns 1 after an abort instead of terminating +the process. Other exceptions continue unwinding. + ### Cache The cache is a simple key-value store that persists between runs. Cached data may diff --git a/src/cli.toit b/src/cli.toit index ce3196b..a0fd585 100644 --- a/src/cli.toit +++ b/src/cli.toit @@ -365,6 +365,42 @@ class Command: --add-ui-help/bool=(not cli) --add-completion/bool=true --completion-as-flag/bool?=null: + exit-code := run-for-exit-code arguments + --invoked-command=invoked-command + --cli=cli + --add-ui-help=add-ui-help + --add-completion=add-completion + --completion-as-flag=completion-as-flag + if exit-code != 0: exit exit-code + + /** + Runs this command and returns the resulting process exit code. + + Unlike $run, this method does not terminate the process when $Ui.abort is + called. It unwinds the stack, runs associated `finally` blocks, and returns + exit code 1. Other exceptions continue unwinding. + */ + run-for-exit-code arguments/List -> int + --invoked-command=system.program-name + --cli/Cli?=null + --add-ui-help/bool=(not cli) + --add-completion/bool=true + --completion-as-flag/bool?=null: + exception := catch --unwind=(: it != ABORT-EXCEPTION_): + run_ arguments + --invoked-command=invoked-command + --cli=cli + --add-ui-help=add-ui-help + --add-completion=add-completion + --completion-as-flag=completion-as-flag + return exception ? 1 : 0 + + run_ arguments/List -> none + --invoked-command/string + --cli/Cli? + --add-ui-help/bool + --add-completion/bool + --completion-as-flag/bool?: added-completion-flag := false if add-completion: added-completion-flag = add-completion-bootstrap_ @@ -383,7 +419,7 @@ class Command: completion-args = completion-args[1..] result := complete-with-timeout_ this completion-args // The completion scripts treat a non-zero exit as "no completions". - if not result: exit 1 + if not result: throw ABORT-EXCEPTION_ result.candidates.do: | candidate/CompletionCandidate_ | print candidate.to-string if result.extensions and not result.extensions.is-empty: diff --git a/src/ui.toit b/src/ui.toit index dc4ecc8..57a42c1 100644 --- a/src/ui.toit +++ b/src/ui.toit @@ -7,6 +7,8 @@ import io import log import log as log-lib +ABORT-EXCEPTION_ ::= Object + create-ui-from-args_ args/List -> Ui: verbose-level/string? := null output-format/string? := null @@ -715,10 +717,10 @@ class Ui: # Inheritance It is safe to override this method with a custom implementation. The - method should always abort. Either with 'exit 1', or with an exception. + method should never return. */ abort -> none: - exit 1 + throw ABORT-EXCEPTION_ /** Returns a new Ui object with the given $level and $printer. diff --git a/tests/abort_test.toit b/tests/abort_test.toit new file mode 100644 index 0000000..364b7e1 --- /dev/null +++ b/tests/abort_test.toit @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Toit contributors. +// Use of this source code is governed by a Zero-Clause BSD license that can +// be found in the tests/TESTS_LICENSE file. + +import cli +import expect show * + +main: + test-success + test-abort + test-parser-abort + test-other-exception + +test-success: + command := cli.Command "app" --run=:: null + expect-equals 0 (command.run-for-exit-code []) + +test-abort: + finalized := false + ui := cli.Ui.human --level=cli.Ui.SILENT-LEVEL + command := cli.Command "app" + --run=:: | invocation/cli.Invocation | + try: + invocation.cli.ui.abort "Stop." + finally: + finalized = true + + exit-code := command.run-for-exit-code [] + --cli=(cli.Cli "app" --ui=ui) + expect-equals 1 exit-code + expect finalized + +test-parser-abort: + command := cli.Command "app" + --rest=[cli.Option "argument" --required] + --run=:: unreachable + expect-equals 1 (command.run-for-exit-code []) + +test-other-exception: + command := cli.Command "app" --run=:: throw "OTHER" + exception := catch: command.run-for-exit-code [] + expect-equals "OTHER" exception