rrun-ssh 0.3.0

Remote run utility; runs a command via SSH if the current directory was mounted via SSHFS.
Documentation
use std::env::{args_os, current_dir};
use std::ffi::OsString;
use std::fmt;
use super::{ProgramArgs, RemoteLocation};

/// Helper method, used for quoting strings.
fn extend_quoted(onto: &mut String, arg: &str) {
    onto.push_str(" \'");
    for ch in arg.chars() {
        match ch {
            '\'' => onto.push_str(r"'\''"),
            '\\' => onto.push_str(r"'\\'"),
            _ => onto.push(ch),
        }
    }
    onto.push('\'');
}

/// Encapsulates a directory in which a program can be executed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Location {
    /// Directory on a remote server
    Remote(RemoteLocation),

    /// Current working directory
    Local,
}
impl Location {
    /// Using the given command-line arguments, generate a set of program arguments if they exist.
    pub fn into_args<I>(self, mut args: I) -> Result<ProgramArgs, ()>
        where I: Iterator<Item = OsString> + ExactSizeIterator
    {
        args.next();
        match self {
            Location::Local => {
                let mut prog_args = ProgramArgs::new(try!(args.next().ok_or(())));
                prog_args.extend(args);
                Ok(prog_args)
            }

            Location::Remote(remote) => {
                if args.len() == 0 {
                    return Err(());
                }

                let mut prog_args = ProgramArgs::new("ssh");
                prog_args.push("-qt");
                prog_args.push(remote.host);

                let mut cmd = String::from("cd");
                extend_quoted(&mut cmd, try!(remote.path.to_str().ok_or(())));
                cmd.push(';');
                for arg in args {
                    extend_quoted(&mut cmd, try!(arg.to_str().ok_or(())));
                }
                prog_args.push(cmd);

                Ok(prog_args)
            }
        }
    }

    /// Using the program environment, generate a set of program arguments if they exist.
    #[inline]
    pub fn into_env_args(self) -> Result<ProgramArgs, ()> {
        self.into_args(args_os())
    }
}
impl fmt::Display for Location {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Location::Local => fmt::Display::fmt(&current_dir().unwrap().display(), f),
            Location::Remote(ref remote) => fmt::Display::fmt(&remote, f),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{Location, extend_quoted};
    use super::super::{ProgramArgs, RemoteLocation};

    #[test]
    fn quote_noescape() {
        let mut v = String::from("never");
        extend_quoted(&mut v, "gonna");
        extend_quoted(&mut v, "give");
        extend_quoted(&mut v, "you");
        extend_quoted(&mut v, "up");
        assert_eq!(v, r"never 'gonna' 'give' 'you' 'up'")
    }

    #[test]
    fn quote_escape_quote() {
        let mut v = String::from("echo");
        extend_quoted(&mut v, "so");
        extend_quoted(&mut v, "here's");
        extend_quoted(&mut v, "a string");
        extend_quoted(&mut v, "I'd");
        extend_quoted(&mut v, "print");
        assert_eq!(v, r"echo 'so' 'here'\''s' 'a string' 'I'\''d' 'print'")
    }

    #[test]
    fn quote_escape_slash() {
        let mut v = String::from("umm");
        extend_quoted(&mut v, r"/test/");
        extend_quoted(&mut v, r"\testing\");
        extend_quoted(&mut v, r"test\\string");
        assert_eq!(v, r"umm '/test/' ''\\'testing'\\'' 'test'\\''\\'string'")
    }

    #[test]
    fn local_empty() {
        let args = vec!["rrun"].into_iter().map(Into::into);
        assert!(Location::Local.into_args(args).is_err());
    }

    #[test]
    fn local_hello_world() {
        let args = vec!["rrun", "echo", "Hello,", "world!"].into_iter().map(Into::into);

        let mut test_args = ProgramArgs::new("echo");
        test_args.push("Hello,");
        test_args.push("world!");

        assert_eq!(Location::Local.into_args(args), Ok(test_args));
    }

    #[test]
    fn remote_empty() {
        let remote = RemoteLocation::new("someone@example.com:/home/user").unwrap();

        let args = vec!["rrun"].into_iter().map(Into::into);

        assert!(Location::Remote(remote).into_args(args).is_err());
    }

    #[test]
    fn remote_hello_world() {
        let remote = RemoteLocation::new("someone@example.com:/home/user").unwrap();

        let args = vec!["rrun", "echo", "Hello,", "world!"].into_iter().map(Into::into);

        let mut test_args = ProgramArgs::new("ssh");
        test_args.push("-qt");
        test_args.push("someone@example.com");
        test_args.push("cd '/home/user'; 'echo' 'Hello,' 'world!'");

        assert_eq!(Location::Remote(remote).into_args(args), Ok(test_args));
    }
}