Skip to main content

loonfs_cli/
lib.rs

1//! LoonFS command-line entrypoint.
2//!
3//! The CLI supports embedded profiles that talk directly to object storage and
4//! remote profiles that talk to a LoonFS server. It keeps command output stable
5//! for humans and scripts.
6
7mod args;
8mod backend;
9mod backend_error;
10mod commands;
11mod config;
12mod error;
13mod payload;
14mod profiles;
15mod progress;
16mod prompt;
17mod render;
18mod resolve;
19mod uploads;
20
21use clap::Parser;
22use std::process::ExitCode;
23
24/// Exit status for a command line the parser rejected, which is clap's own.
25///
26/// It stays distinct from the failure status a command that actually ran
27/// reports, so a script can tell "this command never started" from "this
28/// command started and failed" without reading the message.
29const USAGE_EXIT_CODE: u8 = 2;
30
31pub async fn main() -> ExitCode {
32    let cli = match args::Cli::try_parse() {
33        Ok(cli) => cli,
34        Err(error) => return render_parse_failure(&error),
35    };
36    let runtime = args::RuntimeBehavior::detect(&cli);
37
38    match commands::run(cli, runtime).await {
39        Ok(output) => match render::render_success(&output, runtime.json) {
40            // A recursive transfer renders its per-item outcomes as success
41            // data but still exits nonzero when any item failed.
42            Ok(()) if output.data.reports_failures() => ExitCode::FAILURE,
43            Ok(()) => ExitCode::SUCCESS,
44            Err(err) => {
45                let failure = commands::CommandFailure {
46                    kind: output.kind,
47                    profile: output.profile.clone(),
48                    mode: output.mode,
49                    error: Box::new(error::CliError::io(err)),
50                };
51                let _ = render::render_error(&failure, runtime.json);
52                ExitCode::FAILURE
53            }
54        },
55        Err(failure) => {
56            let _ = render::render_error(&failure, runtime.json);
57            ExitCode::FAILURE
58        }
59    }
60}
61
62/// Renders a command line clap rejected, in whichever form the caller asked
63/// for.
64///
65/// `--json` is one of the things clap failed to parse, so whether it was
66/// asked for has to be read off the raw arguments. A caller who asked for
67/// JSON gets the same envelope a runtime failure produces, and every caller
68/// keeps clap's exit status: a parse failure is not a command that ran.
69///
70/// `--help` and `--version` arrive here as errors too, and are not
71/// failures: clap renders them on stdout and exits zero.
72fn render_parse_failure(error: &clap::Error) -> ExitCode {
73    if !error.use_stderr() {
74        error.print().ok();
75        return ExitCode::SUCCESS;
76    }
77    if !json_requested(std::env::args_os()) {
78        error.print().ok();
79        return ExitCode::from(USAGE_EXIT_CODE);
80    }
81    let failure = error::CliError::invalid_usage(error.render().to_string());
82    let _ = render::render_parse_error(&failure);
83    ExitCode::from(USAGE_EXIT_CODE)
84}
85
86/// Whether the raw arguments asked for `--json`, scanned the way clap would
87/// have: a bare `--` ends option parsing, so a `--json` after it is a value,
88/// not this flag.
89fn json_requested(arguments: impl IntoIterator<Item = std::ffi::OsString>) -> bool {
90    for argument in arguments {
91        if argument == "--" {
92            return false;
93        }
94        if argument == "--json" {
95            return true;
96        }
97    }
98    false
99}