Skip to main content

shuck/
lib.rs

1#![warn(missing_docs)]
2#![cfg_attr(not(test), warn(clippy::unwrap_used))]
3
4//! Library entrypoints for the `shuck` CLI.
5//!
6//! This crate primarily exists so the command-line binary, tests, and benchmarks can share the
7//! same argument parsing and command execution code. Most users should invoke the `shuck` binary
8//! directly rather than depend on this library API.
9
10/// Command-line argument types and parsing helpers for the `shuck` executable.
11pub mod args;
12
13mod cache;
14mod commands;
15#[doc(hidden)]
16pub mod config_docs;
17mod discover;
18mod format_settings;
19#[doc(hidden)]
20pub mod shellcheck_compat;
21#[doc(hidden)]
22pub mod shellcheck_runtime;
23mod stdin;
24
25use std::path::{Path, PathBuf};
26use std::process::ExitCode;
27
28use anyhow::Result;
29use shuck_config::ConfigArguments;
30
31use crate::args::{Args, Command, FormatCommand, TerminalColor};
32
33/// Exit status returned by [`run`].
34#[derive(Copy, Clone, Debug, Eq, PartialEq)]
35pub enum ExitStatus {
36    /// The command completed successfully without reporting failures.
37    Success,
38    /// The command completed, but reported lint or formatting failures.
39    Failure,
40    /// The command failed due to invalid input, I/O, or another runtime error.
41    Error,
42    /// The command exited with an explicit process status code.
43    Code(i32),
44}
45
46impl From<ExitStatus> for ExitCode {
47    fn from(status: ExitStatus) -> Self {
48        match status {
49            ExitStatus::Success => ExitCode::from(0),
50            ExitStatus::Failure => ExitCode::from(1),
51            ExitStatus::Error => ExitCode::from(2),
52            ExitStatus::Code(code) => ExitCode::from(u8::try_from(code).unwrap_or(1)),
53        }
54    }
55}
56
57/// Run a parsed `shuck` command and return the resulting process status.
58pub fn run(args: Args) -> Result<ExitStatus> {
59    let Args {
60        cache_dir,
61        config,
62        color,
63        command,
64    } = args;
65
66    if let Some(color_override) = colored_override(color, std::env::var_os("FORCE_COLOR")) {
67        colored::control::set_override(color_override);
68    }
69
70    match command {
71        Command::Check(command) => commands::check::check(*command, &config, cache_dir.as_deref()),
72        Command::Server(_command) => commands::server::server(),
73        Command::Run(command) => commands::runtime::run(command, &config),
74        Command::Install(command) => commands::runtime::install(command),
75        Command::Shell(command) => commands::runtime::shell(command, &config),
76        Command::Format(command) => format(command, &config, cache_dir.as_deref()),
77        Command::Clean(command) => commands::clean::clean(command, &config, cache_dir.as_deref()),
78    }
79}
80
81#[doc(hidden)]
82pub fn benchmark_check_paths(
83    cwd: &Path,
84    paths: &[PathBuf],
85    output_format: args::CheckOutputFormatArg,
86) -> Result<usize> {
87    commands::check::benchmark_check_paths(cwd, paths, output_format)
88}
89
90fn format(
91    mut args: FormatCommand,
92    config_arguments: &ConfigArguments,
93    cache_dir: Option<&Path>,
94) -> Result<ExitStatus> {
95    let stdin = is_stdin(&args.files, args.stdin_filename.as_deref());
96    args.files = resolve_default_files(args.files, stdin);
97
98    if stdin {
99        commands::format_stdin::format_stdin(args, config_arguments)
100    } else {
101        commands::format::format(args, config_arguments, cache_dir)
102    }
103}
104
105fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
106    if stdin_filename.is_some() {
107        return true;
108    }
109
110    matches!(files, [file] if file == Path::new("-"))
111}
112
113fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
114    if files.is_empty() {
115        if is_stdin {
116            vec![PathBuf::from("-")]
117        } else {
118            vec![PathBuf::from(".")]
119        }
120    } else {
121        files
122    }
123}
124
125fn colored_override(
126    color: Option<TerminalColor>,
127    env_force_color: Option<std::ffi::OsString>,
128) -> Option<bool> {
129    match color {
130        Some(TerminalColor::Always) => Some(true),
131        Some(TerminalColor::Never) => Some(false),
132        Some(TerminalColor::Auto) | None => {
133            env_force_color.map(|force_color| !force_color.is_empty())
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn force_color_env_is_respected() {
144        assert_eq!(colored_override(None, Some("1".into())), Some(true));
145    }
146
147    #[test]
148    fn cli_color_overrides_force_color_env() {
149        assert_eq!(
150            colored_override(Some(TerminalColor::Never), Some("1".into())),
151            Some(false)
152        );
153        assert_eq!(
154            colored_override(Some(TerminalColor::Always), None),
155            Some(true)
156        );
157    }
158}