rusht 1.3.0

Shell commands written in Rust
Documentation
use ::std::collections::HashMap;
use ::std::env;
use ::std::fmt::Write;
use ::std::path::PathBuf;

use ::itertools::Itertools;
use ::lazy_static::lazy_static;
use ::regex::Regex;
use ::serde::Deserialize;
use ::serde::Serialize;
use dirs::home_dir;

use crate::common::resolve_executable;

lazy_static! {
    static ref SAFE_ARG_RE: Regex = Regex::new(r"^[\p{L}0-9_\-\.,@/:=\+]+$").unwrap();
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Task {
    pub cmd: String,
    pub args: Vec<String>,
    pub working_dir: PathBuf,
    #[serde(default)]
    pub stdin: Option<String>,
    #[serde(default)]
    pub extra_envs: HashMap<String, String>,
}

impl Task {
    pub fn new(
        cmd: String,
        args: Vec<String>,
        working_dir: PathBuf,
        stdin: Option<String>,
    ) -> Self {
        Task::new_with_env(cmd, args, working_dir, stdin, HashMap::new())
    }

    pub fn new_in_cwd(cmd: String, stdin: Option<String>, args: Vec<String>) -> Self {
        Task::new(cmd, args, env::current_dir().unwrap(), stdin)
    }

    pub fn new_with_env(
        cmd: String,
        args: Vec<String>,
        working_dir: PathBuf,
        stdin: Option<String>,
        extra_envs: HashMap<String, String>,
    ) -> Self {
        let full_cmd = resolve_executable(cmd);
        Task {
            cmd: full_cmd,
            args,
            working_dir,
            stdin,
            extra_envs,
        }
    }

    pub fn new_split_in_cwd(parts: Vec<String>) -> Self {
        let (cmd, args) = parts.split_first().unwrap();
        Task::new(
            cmd.to_owned(),
            args.to_vec(),
            env::current_dir().unwrap(),
            None,
        )
    }

    pub fn new_split(parts: Vec<String>, working_dir: PathBuf, stdin: Option<String>) -> Self {
        let (cmd, args) = parts.split_first().unwrap();
        Task::new(cmd.to_owned(), args.to_vec(), working_dir, stdin)
    }

    #[cfg(test)]
    pub fn noop() -> Self {
        Task::new("true".to_owned(), vec![], env::current_dir().unwrap(), None)
    }

    pub fn push_arg(&mut self, extra_arg: &str) {
        self.args.push(extra_arg.to_owned());
    }

    pub fn with_extra_env(self, key_values: &[(String, String)]) -> Task {
        debug_assert!(!key_values.is_empty());
        let Task { cmd, args, working_dir, stdin, mut extra_envs } = self.clone();
        for (key, value) in key_values {
            extra_envs.insert(key.into(), value.into());
        }
        Task {
            cmd,
            args,
            working_dir,
            stdin,
            extra_envs,
        }
    }

    pub fn as_cmd_str(&self) -> String {
        let mut txt = String::new();
        if let Some(home) = home_dir() {
            let cmd = PathBuf::from(&self.cmd);
            if let Ok(rel_pth) = cmd.strip_prefix(&home) {
                txt.push_str("~/");
                txt.push_str(rel_pth.to_str().expect("path should be utf8"))
            }
        }
        if txt.is_empty() {
            txt.push_str(&self.cmd)
        }
        self.as_cmd_str_maxlen(txt, usize::MAX)
    }

    pub fn as_short_cmd_str(&self) -> String {
        let txt = PathBuf::from(&self.cmd).file_name()
            .map(|n| n.to_str().unwrap().to_owned())
            .unwrap_or_else(|| self.cmd.to_owned());
        self.as_cmd_str_maxlen(txt, 5 * 72)
    }

    /// Command but with at most max_len chars (except if the executable is longer)
    fn as_cmd_str_maxlen(&self, exe_name: String, max_len: usize) -> String {
        debug_assert!(max_len > 3);
        let mut txt = exe_name;
        for arg in &self.args {
            if txt.len() + arg.len() > max_len - 4 {
                write!(txt, " ...").unwrap();
                break;
            }
            if SAFE_ARG_RE.is_match(arg) {
                write!(txt, " {}", arg).unwrap()
            } else {
                write!(txt, " '{}'", arg).unwrap()
            }
        }
        txt
    }

    pub fn as_str(&self) -> String {
        let mut cmd_str = if let Some(sin) = &self.stdin {
            format!(" <{}b", sin.len())
        } else {
            "".to_owned()
        };
        if self.working_dir != env::current_dir().unwrap() {
            write!(cmd_str, " @ {}", self.working_dir.to_string_lossy()).unwrap()
        };
        let env_str = if self.extra_envs.is_empty() {
            "".to_owned()
        } else {
            format!(
                "{} ",
                self.extra_envs
                    .iter()
                    .map(|(k, v)| format!("{}='{}'", k, v))
                    .join(" ")
            )
        };
        format!("{}{}{}", env_str, self.as_cmd_str(), cmd_str,)
    }
}