xianyun_cli/exitcode.rs
1//! A small carrier for "this command finished, but report a specific
2//! non-zero exit code" -- the equivalent of Go's `cli.Exit("", code)`
3//! (`urfave/cli/v3`'s `cli.ExitCoder`).
4//!
5//! Go's `cli.Exit` constructs a special error value that the `cli`
6//! framework inspects at the top level to pick a process exit code,
7//! without printing anything extra (the command has already reported
8//! whatever went wrong). This crate's commands return `anyhow::Result`,
9//! so [`CliExit`] plays the same role: a command's business logic
10//! returns `Err(CliExit::new(code).into())` instead of calling
11//! `std::process::exit` directly (which would make it untestable --
12//! every in-process test calling that code path would kill the whole
13//! test binary), and [`exit_for`] at the real process boundary
14//! (`main`/`commands::dispatch`) inspects the final error to translate
15//! it into the matching process exit code.
16
17use std::fmt;
18
19/// Port of `cli.Exit`'s `ExitCoder`: an error carrying a specific process
20/// exit code rather than a message to print (the command already
21/// reported whatever went wrong on its own streams).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct CliExit(pub i32);
24
25impl CliExit {
26 #[must_use]
27 pub fn new(code: i32) -> Self {
28 Self(code)
29 }
30}
31
32impl fmt::Display for CliExit {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 write!(f, "exit code {}", self.0)
35 }
36}
37
38impl std::error::Error for CliExit {}
39
40/// Inspects `result` for a [`CliExit`] anywhere in its error chain and
41/// returns the process exit code to use: `0` for `Ok`, the carried code
42/// for a `CliExit`, or `1` for any other error (matching the default
43/// `anyhow`/`clap` behavior for a plain `Err`).
44#[must_use]
45pub fn exit_for(result: &anyhow::Result<()>) -> i32 {
46 match result {
47 Ok(()) => 0,
48 Err(err) => err
49 .chain()
50 .find_map(|cause| cause.downcast_ref::<CliExit>())
51 .map_or(1, |exit| exit.0),
52 }
53}