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