Skip to main content

ocelot_base/
cli.rs

1use crate::error::ErrorKind;
2use crate::error::OcelotError;
3use crate::result::OcelotResult;
4use std::fmt::Write as _;
5use std::process::ExitCode;
6
7/// Runs a fallible CLI entrypoint and converts the result into a process exit code.
8///
9/// It runs a fallible entrypoint, prints a readable error report on failure,
10/// and converts the outcome into a process exit code.
11pub fn try_main(run: impl FnOnce() -> OcelotResult<()>) -> ExitCode {
12    match run() {
13        Ok(()) => ExitCode::SUCCESS,
14        Err(error) => {
15            eprint!("{}", format_cli_error("operation failed", &error));
16            ExitCode::FAILURE
17        }
18    }
19}
20
21/// Runs a fallible CLI entrypoint with a custom top-level error headline.
22///
23/// It behaves like [`try_main`], but lets a binary choose a more specific
24/// headline for its top-level error report.
25pub fn try_main_with_headline(headline: &str, run: impl FnOnce() -> OcelotResult<()>) -> ExitCode {
26    match run() {
27        Ok(()) => ExitCode::SUCCESS,
28        Err(error) => {
29            eprint!("{}", format_cli_error(headline, &error));
30            ExitCode::FAILURE
31        }
32    }
33}
34
35/// Returns a stable, human-readable rendering of an [`OcelotError`] for CLI output.
36///
37/// It returns a stable, human-readable rendering of a [`OcelotError`] that is
38/// suitable for printing from a command-line binary.
39pub fn format_cli_error(headline: &str, error: &OcelotError) -> String {
40    if let Some(rendered_diagnostics) = compilation_diagnostics_output(error) {
41        return rendered_diagnostics.to_string();
42    }
43
44    let mut rendered = String::new();
45    let _ = writeln!(&mut rendered, "\u{1b}[1;31m━━ {}\u{1b}[0m", headline);
46    if error.write_to(&mut rendered).is_err() {
47        let _ = writeln!(
48            &mut rendered,
49            "\u{1b}[1;31m× error\u{1b}[0m failed to render detailed error output"
50        );
51    }
52
53    let mut causes = Vec::new();
54    let mut current = error.source();
55    while let Some(cause) = current {
56        causes.push((cause.kind().to_string(), cause.location()));
57        current = cause.source();
58    }
59
60    if !causes.is_empty() {
61        let simple_causes: Vec<_> = causes
62            .iter()
63            .filter(|(cause, _)| !cause.contains('\n'))
64            .collect();
65        if simple_causes.is_empty() {
66            return rendered;
67        }
68
69        rendered.push('\n');
70        rendered.push_str("\u{1b}[1;33m━━ cause chain\u{1b}[0m\n");
71        for (cause, location) in simple_causes {
72            let _ = writeln!(&mut rendered, "  • {}", cause);
73            let _ = writeln!(
74                &mut rendered,
75                "    at {}:{}:{}",
76                location.file(),
77                location.line(),
78                location.column()
79            );
80        }
81    }
82
83    rendered
84}
85
86fn compilation_diagnostics_output(error: &OcelotError) -> Option<&str> {
87    match error.kind() {
88        ErrorKind::CompilationError(_) => error
89            .source()
90            .and_then(|source| source.kind().as_message())
91            .map(|message| message.as_str()),
92        ErrorKind::Message(_) | ErrorKind::Std(_) => None,
93    }
94}