Skip to main content

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}