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;
15mod config;
16mod discover;
17mod format_settings;
18#[doc(hidden)]
19pub mod shellcheck_compat;
20#[doc(hidden)]
21pub mod shellcheck_runtime;
22mod stdin;
23
24use std::path::{Path, PathBuf};
25use std::process::ExitCode;
26
27use anyhow::Result;
28
29use crate::args::{Args, Command, FormatCommand, TerminalColor};
30use crate::config::ConfigArguments;
31
32/// Exit status returned by [`run`].
33#[derive(Copy, Clone, Debug, Eq, PartialEq)]
34pub enum ExitStatus {
35    /// The command completed successfully without reporting failures.
36    Success,
37    /// The command completed, but reported lint or formatting failures.
38    Failure,
39    /// The command failed due to invalid input, I/O, or another runtime error.
40    Error,
41}
42
43impl From<ExitStatus> for ExitCode {
44    fn from(status: ExitStatus) -> Self {
45        match status {
46            ExitStatus::Success => ExitCode::from(0),
47            ExitStatus::Failure => ExitCode::from(1),
48            ExitStatus::Error => ExitCode::from(2),
49        }
50    }
51}
52
53/// Run a parsed `shuck` command and return the resulting process status.
54pub fn run(args: Args) -> Result<ExitStatus> {
55    let Args {
56        cache_dir,
57        config,
58        color,
59        command,
60    } = args;
61
62    if let Some(color_override) = colored_override(color, std::env::var_os("FORCE_COLOR")) {
63        colored::control::set_override(color_override);
64    }
65
66    match command {
67        Command::Check(command) => commands::check::check(command, &config, cache_dir.as_deref()),
68        Command::Format(command) => format(command, &config, cache_dir.as_deref()),
69        Command::Clean(command) => commands::clean::clean(command, &config, cache_dir.as_deref()),
70    }
71}
72
73#[doc(hidden)]
74pub fn benchmark_check_paths(
75    cwd: &Path,
76    paths: &[PathBuf],
77    output_format: args::CheckOutputFormatArg,
78) -> Result<usize> {
79    commands::check::benchmark_check_paths(cwd, paths, output_format)
80}
81
82fn format(
83    mut args: FormatCommand,
84    config_arguments: &ConfigArguments,
85    cache_dir: Option<&Path>,
86) -> Result<ExitStatus> {
87    let stdin = is_stdin(&args.files, args.stdin_filename.as_deref());
88    args.files = resolve_default_files(args.files, stdin);
89
90    if stdin {
91        commands::format_stdin::format_stdin(args, config_arguments)
92    } else {
93        commands::format::format(args, config_arguments, cache_dir)
94    }
95}
96
97fn is_stdin(files: &[PathBuf], stdin_filename: Option<&Path>) -> bool {
98    if stdin_filename.is_some() {
99        return true;
100    }
101
102    matches!(files, [file] if file == Path::new("-"))
103}
104
105fn resolve_default_files(files: Vec<PathBuf>, is_stdin: bool) -> Vec<PathBuf> {
106    if files.is_empty() {
107        if is_stdin {
108            vec![PathBuf::from("-")]
109        } else {
110            vec![PathBuf::from(".")]
111        }
112    } else {
113        files
114    }
115}
116
117fn colored_override(
118    color: Option<TerminalColor>,
119    env_force_color: Option<std::ffi::OsString>,
120) -> Option<bool> {
121    match color {
122        Some(TerminalColor::Always) => Some(true),
123        Some(TerminalColor::Never) => Some(false),
124        Some(TerminalColor::Auto) | None => {
125            env_force_color.map(|force_color| !force_color.is_empty())
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn force_color_env_is_respected() {
136        assert_eq!(colored_override(None, Some("1".into())), Some(true));
137    }
138
139    #[test]
140    fn cli_color_overrides_force_color_env() {
141        assert_eq!(
142            colored_override(Some(TerminalColor::Never), Some("1".into())),
143            Some(false)
144        );
145        assert_eq!(
146            colored_override(Some(TerminalColor::Always), None),
147            Some(true)
148        );
149    }
150}