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