Skip to main content

skif_cli/
lib.rs

1//! The `skif` command line as a library: a thin consumer of the `skif` library.
2//!
3//! It is the only code that reads the environment, prints, prompts, writes files and spawns a process; [`run`]
4//! returns an exit code and no function below calls `std::process::exit`, except the panic hook that
5//! [`install_panic_hook`] puts in place for the `skif` binary. The binary and the npm addon both call [`run`].
6
7use std::ffi::OsString;
8
9use clap::Parser;
10
11mod args;
12mod commands;
13mod config;
14mod config_toml;
15mod env;
16mod error;
17mod explain;
18mod fix;
19mod install;
20mod lint;
21mod migrate;
22mod output;
23mod roots;
24mod write;
25
26use args::{Cli, Command, Format};
27use env::Env;
28pub use error::{CliError, Kind};
29pub use lint::lint_to_json;
30use output::emit_error;
31
32/// Runs the CLI with `args` (the first item is the program name, as in `std::env::args_os`) and returns the exit
33/// code. Both the `skif` binary and the npm addon call this, so their behaviour cannot drift.
34pub fn run(args: impl IntoIterator<Item = OsString>) -> u8 {
35    let raw: Vec<OsString> = args.into_iter().collect();
36    env::enable_timings();
37    let cli = match Cli::try_parse_from(&raw) {
38        Ok(cli) => cli,
39        Err(error) => return usage(&raw, &error),
40    };
41    match cli.command {
42        None => lint::run(&cli.lint, Env::detect()),
43        Some(Command::Rules(args)) => commands::rules(&args),
44        Some(Command::Schema) => commands::schema(),
45        Some(Command::Init(args)) => commands::init(&args),
46        Some(Command::Config(args)) => config::run(&args),
47        Some(Command::ExplainUsage(args)) => explain::run(&args),
48        Some(Command::Migrate(args)) => migrate::run(&args),
49    }
50}
51
52/// Keeps the JSON contract when skif itself panics: in `--format json` mode stdout still receives exactly one
53/// `error` document (kind `internal`) and the process exits 2, instead of aborting with a bare message on stderr.
54///
55/// For the `skif` binary only; an embedding host owns its own panic handling. Without `--format json` the default
56/// hook is left alone.
57pub fn install_panic_hook(raw: &[OsString]) {
58    if !wants_json(raw) {
59        return;
60    }
61    let default = std::panic::take_hook();
62    std::panic::set_hook(Box::new(move |info| {
63        default(info);
64        emit_error(
65            &CliError::new(Kind::Internal, format!("internal error: {info}")),
66            Format::Json,
67        );
68        std::process::exit(2);
69    }));
70}
71
72/// Help and version go to stdout with exit 0; usage errors exit 2, as a JSON `error` document when `--format json`
73/// is on the command line.
74fn usage(raw: &[OsString], error: &clap::Error) -> u8 {
75    if !error.use_stderr() {
76        let _ = error.print();
77        return 0;
78    }
79    if wants_json(raw) {
80        let message = error.to_string();
81        emit_error(&CliError::new(Kind::Usage, message.trim_end()), Format::Json);
82    } else {
83        let _ = error.print();
84    }
85    2
86}
87
88fn wants_json(raw: &[OsString]) -> bool {
89    let args: Vec<&str> = raw.iter().filter_map(|arg| arg.to_str()).collect();
90    args.contains(&"--format=json") || args.windows(2).any(|pair| pair[0] == "--format" && pair[1] == "json")
91}