clingwrap 0.7.0

types and functions to implement command line programs
Documentation
//! Create a [`Command`](https://doc.rust-lang.org/std/process/struct.Command.html)
//! to run things on a remote host over SSH using local `ssh` command.
//!
//! # Example
//!
//! ```rust
//! # use clingwrap::ssh::{Args, Ssh};
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut ssh = Ssh::new("localhost", Args::from(vec!["echo", "hello,", "world"])).command();
//! let output = ssh.output()?;
//! assert!(output.status.success());
//! assert_eq!(output.stdout, b"hello, world\n");
//! # Ok(())
//! # }
//! ```

use std::{
    ffi::{OsStr, OsString},
    process::Command,
};

use crate::runner::shell_quote;

/// A list of arguments to a command run with [`Ssh`].
///
/// ```rust
/// # use clingwrap::ssh::Args;
/// let argv1 = Args::from(vec!["echo", "hello, world"]);
/// let argv2 = Args::default().arg("echo").arg("hello, world");
/// assert_eq!(argv1, argv2);
/// ```
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Args {
    quoted_args: Vec<OsString>,
}

impl Args {
    /// Push a new argument to the end of the list.
    pub fn push<S: Into<OsString>>(&mut self, arg: S) {
        self.quoted_args.push(shell_quote(&arg.into()));
    }

    /// Push a new argument to the end of the list, builder style.
    pub fn arg<S: Into<OsString>>(mut self, arg: S) -> Self {
        self.push(arg);
        self
    }

    fn quoted_args(&self) -> impl Iterator<Item = &OsStr> {
        self.quoted_args.iter().map(|s| s.as_os_str())
    }
}

impl<S> FromIterator<S> for Args
where
    S: Into<OsString>,
{
    fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
        let mut args = Args::default();
        for arg in iter.into_iter() {
            args.push(arg.into());
        }
        args
    }
}

impl From<Vec<&str>> for Args {
    fn from(args: Vec<&str>) -> Self {
        let mut new = Self::default();
        for arg in args.iter() {
            new.push(*arg);
        }
        new
    }
}

/// Represent a command to be run on a remote host over SSH.
pub struct Ssh {
    ssh_target: String,
    argv: Args,
}

impl Ssh {
    /// Create a new [`Ssh`].
    pub fn new<S: Into<String>>(ssh_target: S, argv: Args) -> Self {
        Self {
            ssh_target: ssh_target.into(),
            argv,
        }
    }

    /// Create a [`Command`](https://doc.rust-lang.org/std/process/struct.Command.html)
    /// that executes the command on a remote host.
    pub fn command(&self) -> Command {
        let mut cmd = Command::new("ssh");
        cmd.arg("--");
        cmd.arg(&self.ssh_target);
        for arg in self.argv.quoted_args() {
            cmd.arg(arg);
        }
        cmd
    }
}

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

    #[test]
    fn from_iter() {
        let args = Args::from_iter(["foo", "bar"]);
        let quoted: Vec<&OsStr> = args.quoted_args().collect();
        assert_eq!(quoted, ["foo", "bar"]);
    }
}