torrust_linting/linters/
shellcheck.rs1use std::process::Command;
2use std::time::Instant;
3
4use anyhow::Result;
5use tracing::{error, info, warn};
6
7use crate::utils::is_command_available;
8
9fn install_shellcheck() -> Result<()> {
15 info!("Installing ShellCheck...");
16
17 if is_command_available("apt-get") {
19 let output = Command::new("sudo").args(["apt-get", "update"]).output()?;
20
21 if !output.status.success() {
22 warn!("Failed to update package list");
23 }
24
25 let output = Command::new("sudo")
26 .args(["apt-get", "install", "-y", "shellcheck"])
27 .output()?;
28
29 if output.status.success() {
30 info!("shellcheck installed successfully");
31 return Ok(());
32 }
33 } else if is_command_available("dnf") {
34 let output = Command::new("sudo").args(["dnf", "install", "-y", "ShellCheck"]).output()?;
35
36 if output.status.success() {
37 info!("shellcheck installed successfully");
38 return Ok(());
39 }
40 } else if is_command_available("pacman") {
41 let output = Command::new("sudo")
42 .args(["pacman", "-S", "--noconfirm", "shellcheck"])
43 .output()?;
44
45 if output.status.success() {
46 info!("shellcheck installed successfully");
47 return Ok(());
48 }
49 } else if is_command_available("brew") {
50 let output = Command::new("brew").args(["install", "shellcheck"]).output()?;
51
52 if output.status.success() {
53 info!("shellcheck installed successfully");
54 return Ok(());
55 }
56 }
57
58 error!("Could not install shellcheck: unsupported package manager");
59 info!("Please install shellcheck manually: https://github.com/koalaman/shellcheck#installing");
60 Err(anyhow::anyhow!("Could not install shellcheck"))
61}
62
63fn find_shell_scripts() -> Result<Vec<String>> {
69 let mut files = Vec::new();
70
71 let sh_output = Command::new("find")
73 .args([
74 ".",
75 "-name",
76 "*.sh",
77 "-type",
78 "f",
79 "-not",
80 "-path",
81 "*/.git/*",
82 "-not",
83 "-path",
84 "*/.terraform/*",
85 ])
86 .output()?;
87
88 if sh_output.status.success() {
89 let stdout = String::from_utf8_lossy(&sh_output.stdout);
90 files.extend(stdout.lines().filter(|s| !s.is_empty()).map(String::from));
91 }
92
93 let bash_output = Command::new("find")
95 .args([
96 ".",
97 "-name",
98 "*.bash",
99 "-type",
100 "f",
101 "-not",
102 "-path",
103 "*/.git/*",
104 "-not",
105 "-path",
106 "*/.terraform/*",
107 ])
108 .output()?;
109
110 if bash_output.status.success() {
111 let stdout = String::from_utf8_lossy(&bash_output.stdout);
112 files.extend(stdout.lines().filter(|s| !s.is_empty()).map(String::from));
113 }
114
115 Ok(files)
116}
117
118pub fn run_shellcheck_linter() -> Result<()> {
125 let t = Instant::now();
126 info!(target: "shellcheck", "Running ShellCheck on shell scripts...");
127
128 if !is_command_available("shellcheck") {
130 warn!(target: "shellcheck", "shellcheck not found. Attempting to install...");
131 install_shellcheck()?;
132 }
133
134 let shell_files = find_shell_scripts()?;
136
137 if shell_files.is_empty() {
138 warn!(target: "shellcheck", "No shell scripts found ({:.3}s)", t.elapsed().as_secs_f64());
139 return Ok(());
140 }
141
142 info!(target: "shellcheck", "Found {} shell script(s) to check", shell_files.len());
143
144 let mut cmd = Command::new("shellcheck");
146 cmd.args(["--source-path=SCRIPTDIR", "--exclude=SC1091"]);
147 cmd.args(&shell_files);
148
149 let output = cmd.output()?;
150
151 if output.status.success() {
152 info!(target: "shellcheck", "shellcheck passed ({:.3}s)", t.elapsed().as_secs_f64());
153 Ok(())
154 } else {
155 let stderr = String::from_utf8_lossy(&output.stderr);
156 let stdout = String::from_utf8_lossy(&output.stdout);
157
158 if !stdout.is_empty() {
160 println!("{stdout}");
161 }
162 if !stderr.is_empty() {
163 eprintln!("{stderr}");
164 }
165
166 println!();
167 error!(target: "shellcheck", "shellcheck failed ({:.3}s)", t.elapsed().as_secs_f64());
168 Err(anyhow::anyhow!("shellcheck failed"))
169 }
170}