Skip to main content

torrust_linting/linters/
shellcheck.rs

1use std::process::Command;
2use std::time::Instant;
3
4use anyhow::Result;
5use tracing::{error, info, warn};
6
7use crate::utils::is_command_available;
8
9/// Install shellcheck using system package manager
10///
11/// # Errors
12///
13/// Returns an error if no supported package manager is found or if installation fails.
14fn install_shellcheck() -> Result<()> {
15    info!("Installing ShellCheck...");
16
17    // Try different package managers
18    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
63/// Find shell scripts in the current directory
64///
65/// # Errors
66///
67/// Returns an error if the find command fails.
68fn find_shell_scripts() -> Result<Vec<String>> {
69    let mut files = Vec::new();
70
71    // Find .sh files
72    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    // Find .bash files
94    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
118/// Run the `ShellCheck` linter
119///
120/// # Errors
121///
122/// Returns an error if shellcheck is not available, cannot be installed,
123/// or if the linting fails.
124pub fn run_shellcheck_linter() -> Result<()> {
125    let t = Instant::now();
126    info!(target: "shellcheck", "Running ShellCheck on shell scripts...");
127
128    // Check if shellcheck is installed
129    if !is_command_available("shellcheck") {
130        warn!(target: "shellcheck", "shellcheck not found. Attempting to install...");
131        install_shellcheck()?;
132    }
133
134    // Find shell scripts
135    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    // Prepare the shellcheck command
145    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        // Print the output from shellcheck
159        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}