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}
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
55pub 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}