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}