1#![warn(missing_docs)]
2#![cfg_attr(not(test), warn(clippy::unwrap_used))]
3
4pub 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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
36pub enum ExitStatus {
37 Success,
39 Failure,
41 Error,
43 Code(i32),
45}
46
47impl From<ExitStatus> for ExitCode {
48 fn from(status: ExitStatus) -> Self {
49 match status {
50 ExitStatus::Success => ExitCode::from(0),
51 ExitStatus::Failure => ExitCode::from(1),
52 ExitStatus::Error => ExitCode::from(2),
53 ExitStatus::Code(code) => ExitCode::from(u8::try_from(code).unwrap_or(1)),
54 }
55 }
56}
57
58pub fn run(args: Args) -> Result<ExitStatus> {
60 let Args {
61 cache_dir,
62 config,
63 color,
64 command,
65 } = args;
66
67 if let Some(color_override) = colored_override(color, std::env::var_os("FORCE_COLOR")) {
68 colored::control::set_override(color_override);
69 }
70
71 match command {
72 Command::Check(command) => commands::check::check(*command, &config, cache_dir.as_deref()),
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}