hyper-scripter 0.7.1

The script managing tool for script lovers
Documentation
use hyper_scripter::{
    config::{Config, PromptLevel},
    error::EXIT_KNOWN_ERR,
    my_env_logger,
    path::normalize_path,
    set_once,
    state::State,
};
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::{Mutex, MutexGuard, Once};

pub const HOME_RELATIVE: &str = "./.hyper_scripter";

fn get_exe() -> String {
    #[cfg(not(debug_assertions))]
    let mode = "release";
    #[cfg(debug_assertions)]
    let mode = "debug";

    format!("{}/../target/{}/hs", env!("CARGO_MANIFEST_DIR"), mode)
}

fn get_editor_script() -> String {
    format!("{}/tests/editor.sh", env!("CARGO_MANIFEST_DIR"))
}

#[derive(Debug, Default)]
pub struct RunEnv {
    pub home: Option<PathBuf>,
    pub dir: Option<PathBuf>,
    pub no_touch: Option<bool>,
    pub silent: Option<bool>,
    pub allow_other_error: Option<bool>,
    pub custom_env: Option<Vec<(String, String)>>,
}

macro_rules! run {
    ($($key:ident: $val:expr,)* $lit:literal) => ({
        run!($($key: $val,)* $lit,)
    });
    ($($key:ident: $val:expr,)* $lit:literal, $($arg:tt)*) => ({
        let env = RunEnv{
            $($key: Some($val.into()),)*
            ..Default::default()
        };
        run_with_env(env, format!($lit, $($arg)*))
    });
}
pub(crate) use run;

#[derive(Debug)]
enum ErrorInner {
    Other,
    ExitStatus(ExitStatus),
}
#[derive(Debug)]
pub struct Error {
    msg: Vec<String>,
    inner: ErrorInner,
}
impl Error {
    pub fn exit_status(e: ExitStatus) -> Error {
        Error {
            msg: vec![],
            inner: ErrorInner::ExitStatus(e),
        }
    }
    pub fn other<T: ToString>(s: T) -> Error {
        Error {
            msg: vec![s.to_string()],
            inner: ErrorInner::Other,
        }
    }
    pub fn context<T: ToString>(mut self, s: T) -> Error {
        self.msg.push(s.to_string());
        self
    }
}
type Result<T = ()> = std::result::Result<T, Error>;

pub fn get_home() -> PathBuf {
    normalize_path(HOME_RELATIVE).unwrap()
}
pub fn load_conf() -> Config {
    Config::load(hyper_scripter::path::get_home()).unwrap()
}
pub fn setup<'a>() -> MutexGuard<'a, ()> {
    let g = setup_with_utils();
    run!("rm --purge * -s all").unwrap();
    g
}
pub fn setup_with_utils<'a>() -> MutexGuard<'a, ()> {
    static LOCK: State<Mutex<()>> = State::new();
    set_once!(LOCK, || Mutex::new(()));
    let guard = LOCK.get().lock().unwrap_or_else(|err| err.into_inner());
    let _ = my_env_logger::try_init();
    let home: PathBuf = get_home();
    match std::fs::remove_dir_all(&home) {
        Ok(_) => (),
        Err(e) => {
            if e.kind() != std::io::ErrorKind::NotFound {
                panic!("重整測試用資料夾 {:?} 失敗了……", home);
            }
        }
    }

    run!(silent: true, "ls").unwrap(); // create the home

    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        hyper_scripter::path::set_home(Some(&home), true).unwrap();
        Config::init().unwrap();
        Config::set_runtime_conf(Some(PromptLevel::Never), true);
    });

    // 避免編輯器堵住整個程式
    let mut conf = load_conf();
    conf.editor = vec!["bash".to_owned(), get_editor_script()];
    conf.store().unwrap();

    guard
}
fn join_path(p: &[&str]) -> PathBuf {
    let mut file = get_home();
    for p in p.iter() {
        file = file.join(p);
    }
    file
}

pub fn read(p: &[&str]) -> String {
    let file = join_path(p);
    let s = std::fs::read(file).unwrap();
    let s: &str = std::str::from_utf8(&s).unwrap();
    s.to_owned()
}
pub fn check_exist(p: &[&str]) -> bool {
    let file = join_path(p);
    file.exists()
}

fn fmt_result(res: &Result<String>) -> String {
    if let Ok(s) = res.as_ref() {
        format!("\n{}", s)
    } else {
        format!("{:?}", res)
    }
}

pub fn run_with_env<T: ToString>(env: RunEnv, args: T) -> Result<String> {
    let home = match env.home {
        Some(h) => {
            log::info!("使用腳本之家 {:?}", h);
            h
        }
        None => get_home(),
    };
    let home = home.to_string_lossy();
    let mut full_args = vec!["-H", home.as_ref(), "--prompt-level", "never"];
    let args = args.to_string();
    let args_vec: Vec<&str> = if args.find('|').is_some() {
        let (first, second) = args.split_once("|").unwrap();
        let mut v: Vec<_> = first.split(' ').filter(|s| !s.is_empty()).collect();
        let second = second.trim();
        if second.len() > 0 {
            v.push(second);
        }
        v
    } else {
        args.split_whitespace().collect()
    };
    full_args.extend(&args_vec);

    log::info!("開始執行 {:?}", args_vec);
    let mut cmd = Command::new(normalize_path(get_exe()).unwrap());
    let no_touch = env.no_touch;
    if let Some(dir) = env.dir {
        log::info!("使用路徑 {}", dir.to_string_lossy());
        cmd.current_dir(&dir);
        // cmd.env("PWD", dir); NOTE: 不應使用 PWD 環境變數
    }
    if no_touch == Some(true) {
        cmd.env("NO_TOUCH", "1");
    }
    if let Some(custom_env) = env.custom_env {
        for (k, v) in custom_env.iter() {
            cmd.env(k, v);
        }
    }
    let mut child = cmd
        .args(&full_args)
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit())
        .spawn()
        .unwrap();
    let stdout = child.stdout.as_mut().unwrap();
    let mut out_str = vec![];
    let reader = BufReader::new(stdout);
    let silent = env.silent;
    reader
        .lines()
        .filter_map(|line| line.ok())
        .for_each(|line| {
            if silent != Some(true) {
                println!("{}", line);
            }
            out_str.push(line);
        });

    let status = child.wait().unwrap();
    let res = if status.success() {
        Ok(out_str.join("\n"))
    } else if env.allow_other_error == Some(true) || status.code() == Some(EXIT_KNOWN_ERR.code()) {
        Err(Error::exit_status(status).context(format!("執行 {:?} 失敗", args_vec)))
    } else {
        panic!("執行 {:?} 遭未知的錯誤!", args_vec);
    };
    log::info!("執行 {:?} 完畢,結果為 {}", args_vec, fmt_result(&res));
    res
}

pub fn get_ls(select: Option<&str>, query: Option<&str>) -> Vec<String> {
    let ls_res = run!(
        "ls {} --grouping none --plain --name {}",
        select.map(|f| format!("-s {}", f)).unwrap_or_default(),
        query.unwrap_or_default()
    )
    .unwrap();
    ls_res
        .split_whitespace()
        .filter_map(|s| {
            if !s.is_empty() {
                Some(s.to_owned())
            } else {
                None
            }
        })
        .collect::<Vec<_>>()
}
pub fn assert_ls_len(expect: usize, select: Option<&str>, query: Option<&str>) {
    let res = get_ls(select, query);
    assert_eq!(expect, res.len(), "ls {:?} 結果為 {:?}", select, res);
}
pub fn assert_ls<T: ToString>(expect: Vec<T>, select: Option<&str>, query: Option<&str>) {
    let mut expect: Vec<_> = expect.into_iter().map(|s| s.to_string()).collect();
    expect.sort();
    let mut res = get_ls(select, query);
    res.sort();
    assert_eq!(expect, res, "ls {:?} 結果為 {:?}", select, res);
}

// TODO: 把整合測試的大部份地方改用這個結構
#[derive(Debug)]
pub struct ScriptTest {
    name: String,
}
impl<'a> ToString for &'a ScriptTest {
    fn to_string(&self) -> String {
        self.name.clone()
    }
}
impl ScriptTest {
    pub fn get_name(&self) -> &str {
        &self.name
    }
    pub fn new_regardless(name: &str, tags: Option<&str>, content: Option<&str>) -> (Self, Result) {
        let tags_str = tags.map(|s| format!("-t {}", s)).unwrap_or_default();
        let content = content.unwrap_or("echo $NAME");
        let res = run!("e {} ={} | {}", tags_str, name, content).map(|_| ());
        (
            ScriptTest {
                name: name.to_owned(),
            },
            res,
        )
    }
    pub fn new(name: &str, tags: Option<&str>, content: Option<&str>) -> Self {
        let (t, res) = Self::new_regardless(name, tags, content);
        res.unwrap();
        t
    }
    pub fn assert_not_exist(&self, args: Option<&str>, msg: Option<&str>) {
        let s = format!("cat {} ={}", args.unwrap_or_default(), self.name);
        let msg = msg.map(|s| format!("\n{}", s)).unwrap_or_default();
        run!("{}", s).expect_err(&format!("{} 找到東西{}", s, msg));
    }
    pub fn archaeology<'a>(&'a self) -> ScriptTestWithSelect<'a> {
        self.select("-A")
    }
    pub fn select<'a>(&'a self, select: &'a str) -> ScriptTestWithSelect<'a> {
        ScriptTestWithSelect {
            script: self,
            allow_other_error: false,
            select,
        }
    }
    pub fn allow_other_error<'a>(&'a self) -> ScriptTestWithSelect<'a> {
        ScriptTestWithSelect {
            script: self,
            allow_other_error: true,
            select: "",
        }
    }
    pub fn run(&self, args: &str) -> Result<String> {
        self.select("").run(args)
    }
    pub fn can_find(&self, command: &str) -> Result {
        self.select("").can_find(command)
    }
    pub fn can_find_by_name(&self) -> Result {
        self.select("").can_find_by_name()
    }
}
pub struct ScriptTestWithSelect<'a> {
    script: &'a ScriptTest,
    select: &'a str,
    allow_other_error: bool,
}
impl<'a> ScriptTestWithSelect<'a> {
    pub fn run(&self, args: &str) -> Result<String> {
        run!(
            allow_other_error: self.allow_other_error,
            "{} ={} {}",
            self.select,
            self.script.name,
            args
        )
    }
    pub fn can_find(&self, command: &str) -> Result {
        let res = run!(
            allow_other_error: self.allow_other_error,
            "{} ls --plain --grouping=none --name {}",
            self.select,
            command
        )?;
        if res == self.script.name {
            Ok(())
        } else {
            Err(Error::other(format!(
                "想找 {} 卻找到 {}",
                self.script.name, res
            )))
        }
    }
    pub fn can_find_by_name(&self) -> Result {
        self.can_find(&format!("={}", self.script.name))
    }
}