gdenv-lib 1.1.0

The best command-line tool to install and switch between multiple versions of Godot.
Documentation
//! Utilities for executing external programs in structures that are easy to test.

use anyhow::Result;
use anyhow::{Context, bail};
use std::fmt::{Display, Formatter};
use std::path::PathBuf;

#[derive(Clone, Debug)]
pub struct Command {
    pub executable: PathBuf,
    pub working_dir: PathBuf,
    pub args: Vec<String>,
    pub failure_message: Option<String>,
}

#[derive(Default, Clone, Debug)]
pub struct CommandChain {
    commands: Vec<Command>,
}

impl Command {
    pub fn execute(&self) -> Result<()> {
        let mut command = std::process::Command::new(&self.executable);
        command.current_dir(&self.working_dir).args(&self.args);

        if !self.working_dir.exists() {
            bail!(
                "Can't execute command: {:?}\n    Reason: Working directory does not exist: {:?}",
                command,
                self.working_dir
            );
        }

        let status = command
            .spawn()
            .with_context(|| format!("Failed to spawn process: {:?}", command))?
            .wait()
            .with_context(|| format!("Failed to wait for process: {:?}", command))?;

        if !status.success() {
            let message = self
                .failure_message
                .as_ref()
                .map(|m| format!("\n{}", m))
                .unwrap_or_default();
            bail!(
                "Process exited with code {}\nCommand: {}{}",
                status,
                self,
                message
            )
        } else {
            Ok(())
        }
    }
}

impl CommandChain {
    pub fn new() -> Self {
        Self::default()
    }

    /// Append a new command to the chain.
    pub fn append(&mut self, command: impl Into<Command>) -> &mut Self {
        self.commands.push(command.into());
        self
    }

    /// Prepend a new command to the chain.
    pub fn prepend(&mut self, command: impl Into<Command>) -> &mut Self {
        self.commands.insert(0, command.into());
        self
    }

    /// Execute commands in sequence. If any command fails, the entire chain fails immediately.
    pub fn execute(&self) -> Result<()> {
        for command in self.commands.iter() {
            command.execute()?;
        }
        Ok(())
    }

    pub fn commands(&self) -> &[Command] {
        &self.commands
    }
}

impl Display for Command {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            r#"cd "{}" && {}"#,
            self.working_dir.display(),
            self.executable.display()
        )?;
        for arg in &self.args {
            write!(f, " {}", arg)?;
        }
        Ok(())
    }
}

impl Display for CommandChain {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut first = true;
        for command in &self.commands {
            if !first {
                write!(f, " && ")?;
            }
            write!(f, "{}", command)?;
            first = false;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_command_chain_display() {
        let chain = CommandChain {
            commands: vec![
                Command {
                    executable: PathBuf::from("echo"),
                    working_dir: PathBuf::from("/home/user"),
                    args: vec![String::from("hello")],
                    failure_message: None,
                },
                Command {
                    executable: PathBuf::from("cat"),
                    working_dir: PathBuf::from("/home/user"),
                    args: vec![String::from("world")],
                    failure_message: None,
                },
            ],
        };
        assert_eq!(
            format!("{}", chain),
            r#"cd "/home/user" && echo hello && cd "/home/user" && cat world"#
        );
    }
}