1#![warn(missing_docs)]
2#![cfg_attr(not(test), warn(clippy::unwrap_used))]
3
4pub 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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
35pub enum ExitStatus {
36 Success,
38 Failure,
40 Error,
42 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
57pub 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}