Skip to main content

gdenv_lib/
command_runner.rs

1//! Utilities for executing external programs in structures that are easy to test.
2
3use anyhow::Result;
4use anyhow::{Context, bail};
5use std::fmt::{Display, Formatter};
6use std::path::PathBuf;
7
8#[derive(Clone, Debug)]
9pub struct Command {
10    pub executable: PathBuf,
11    pub working_dir: PathBuf,
12    pub args: Vec<String>,
13    pub failure_message: Option<String>,
14}
15
16#[derive(Default, Clone, Debug)]
17pub struct CommandChain {
18    commands: Vec<Command>,
19}
20
21impl Command {
22    pub fn execute(&self) -> Result<()> {
23        let mut command = std::process::Command::new(&self.executable);
24        command.current_dir(&self.working_dir).args(&self.args);
25
26        if !self.working_dir.exists() {
27            bail!(
28                "Can't execute command: {:?}\n    Reason: Working directory does not exist: {:?}",
29                command,
30                self.working_dir
31            );
32        }
33
34        let status = command
35            .spawn()
36            .with_context(|| format!("Failed to spawn process: {:?}", command))?
37            .wait()
38            .with_context(|| format!("Failed to wait for process: {:?}", command))?;
39
40        if !status.success() {
41            let message = self
42                .failure_message
43                .as_ref()
44                .map(|m| format!("\n{}", m))
45                .unwrap_or_default();
46            bail!(
47                "Process exited with code {}\nCommand: {}{}",
48                status,
49                self,
50                message
51            )
52        } else {
53            Ok(())
54        }
55    }
56}
57
58impl CommandChain {
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Append a new command to the chain.
64    pub fn append(&mut self, command: impl Into<Command>) -> &mut Self {
65        self.commands.push(command.into());
66        self
67    }
68
69    /// Prepend a new command to the chain.
70    pub fn prepend(&mut self, command: impl Into<Command>) -> &mut Self {
71        self.commands.insert(0, command.into());
72        self
73    }
74
75    /// Execute commands in sequence. If any command fails, the entire chain fails immediately.
76    pub fn execute(&self) -> Result<()> {
77        for command in self.commands.iter() {
78            command.execute()?;
79        }
80        Ok(())
81    }
82
83    pub fn commands(&self) -> &[Command] {
84        &self.commands
85    }
86}
87
88impl Display for Command {
89    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
90        write!(
91            f,
92            r#"cd "{}" && {}"#,
93            self.working_dir.display(),
94            self.executable.display()
95        )?;
96        for arg in &self.args {
97            write!(f, " {}", arg)?;
98        }
99        Ok(())
100    }
101}
102
103impl Display for CommandChain {
104    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105        let mut first = true;
106        for command in &self.commands {
107            if !first {
108                write!(f, " && ")?;
109            }
110            write!(f, "{}", command)?;
111            first = false;
112        }
113        Ok(())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_command_chain_display() {
123        let chain = CommandChain {
124            commands: vec![
125                Command {
126                    executable: PathBuf::from("echo"),
127                    working_dir: PathBuf::from("/home/user"),
128                    args: vec![String::from("hello")],
129                    failure_message: None,
130                },
131                Command {
132                    executable: PathBuf::from("cat"),
133                    working_dir: PathBuf::from("/home/user"),
134                    args: vec![String::from("world")],
135                    failure_message: None,
136                },
137            ],
138        };
139        assert_eq!(
140            format!("{}", chain),
141            r#"cd "/home/user" && echo hello && cd "/home/user" && cat world"#
142        );
143    }
144}