scriptplan_bash/
lib.rs

1use yaml_rust::yaml::Hash;
2use yaml_rust::Yaml;
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::collections::VecDeque;
7
8use std::convert::TryFrom;
9use std::ops::Deref;
10use std::process::Stdio;
11use std::rc::Rc;
12use std::sync::Arc;
13
14use shellwords::split;
15
16use async_trait::async_trait;
17
18use std::process::ExitStatus;
19
20use scriptplan_core::Command;
21use scriptplan_core::ScriptGroup;
22use scriptplan_core::ScriptParser;
23use scriptplan_core::VarArgs;
24use scriptplan_core::{Alias, CommandGroup, Script};
25
26use tokio;
27use tokio::io::AsyncWriteExt;
28
29pub extern crate scriptplan_core;
30pub extern crate yaml_rust;
31
32#[derive(Debug)]
33pub struct BashCommand {
34    pub command_str: String,
35}
36
37impl From<&str> for BashCommand {
38    fn from(command_str: &str) -> Self {
39        BashCommand::from(command_str.to_string())
40    }
41}
42
43impl From<String> for BashCommand {
44    fn from(command_str: String) -> Self {
45        BashCommand { command_str }
46    }
47}
48
49#[async_trait]
50impl Command for BashCommand {
51    async fn run(&self, vars: VarArgs) -> Result<ExitStatus, ()> {
52        let has_params = self.command_str.contains("$");
53
54        let args: VecDeque<&str> = vars.iter().map(|x| (*x).as_str()).collect();
55        let mut process = tokio::process::Command::new("bash")
56            .stdin(Stdio::piped())
57            .stdout(Stdio::inherit())
58            .env("PS0", "")
59            .env("PS1", "")
60            .env("PS2", "")
61            .arg("-s")
62            .arg("--")
63            .args(args)
64            .spawn()
65            .unwrap();
66
67        let spread_args = if has_params { "" } else { " $@" };
68
69        process
70            .stdin
71            .take()
72            .unwrap()
73            .write_all((self.command_str.to_string() + spread_args).as_bytes())
74            .await
75            .unwrap();
76
77        let output = process.wait_with_output().await.unwrap();
78
79        let status = output.status;
80
81        return Ok(status);
82    }
83}
84
85fn parse_command(command_str: &str) -> Script<BashCommand> {
86    Script::Command(command_str.into())
87}
88
89fn parse_alias(alias_str: &str) -> Script<BashCommand> {
90    let mut words: Vec<_> = split(alias_str).unwrap();
91    Script::Alias(Alias {
92        task: words.remove(0),
93        args: words.into_iter().map(|string| Arc::new(string)).collect(),
94    })
95}
96
97fn yaml_to_group(yaml: &Yaml) -> Result<ScriptGroup<BashCommand>, ()> {
98    let yaml_list = yaml.as_vec().ok_or(())?;
99
100    let mut scripts_iter = yaml_list.iter().map(yaml_to_script);
101
102    let first = (scripts_iter.next().unwrap())?;
103
104    let scripts_result: Result<VecDeque<_>, _> = scripts_iter.collect();
105    let scripts = scripts_result?;
106
107    Ok(ScriptGroup {
108        bail: false,
109        first,
110        rest: scripts,
111    })
112}
113
114fn yaml_to_script(yaml: &Yaml) -> Result<Script<BashCommand>, ()> {
115    if let Some(command_str) = yaml.as_str() {
116        return Ok(parse_command(command_str));
117    } else if let Some(hash) = yaml.as_hash() {
118        if let Some(task) = hash.get(&Yaml::from_str("task")) {
119            // TODO: Need a splitn
120            return Ok(parse_alias(task.as_str().unwrap()));
121        } else if let Some(command_str) = hash.get(&Yaml::from_str("script")) {
122            return Ok(parse_command(command_str.as_str().unwrap()));
123        } else if let Some(serial_yaml) = hash.get(&Yaml::from_str("series")) {
124            Ok(Script::Group(Box::new(CommandGroup::Series(
125                yaml_to_group(serial_yaml)?,
126            ))))
127        } else if let Some(parallel_yaml) = hash.get(&Yaml::from_str("parallel")) {
128            Ok(Script::Group(Box::new(CommandGroup::Parallel(
129                yaml_to_group(parallel_yaml)?,
130            ))))
131        } else {
132            panic!("should never happen");
133        }
134    } else {
135        panic!("should never happen");
136    }
137}
138
139enum YamlOrTask<'a> {
140    NotLoaded(&'a Yaml),
141    Loaded(Rc<Script<BashCommand>>),
142}
143
144pub struct LazyTask<'a> {
145    yaml_or_task: RefCell<YamlOrTask<'a>>,
146}
147
148impl<'a> From<&'a Yaml> for LazyTask<'a> {
149    fn from(yaml: &'a Yaml) -> Self {
150        LazyTask {
151            yaml_or_task: RefCell::new(YamlOrTask::NotLoaded(yaml)),
152        }
153    }
154}
155
156impl LazyTask<'_> {
157    fn parse(&self) -> Result<Rc<Script<BashCommand>>, ()> {
158        let mut yaml_or_task = self.yaml_or_task.borrow_mut();
159        match yaml_or_task.deref() {
160            &YamlOrTask::Loaded(ref script) => {
161                return Ok(script.clone());
162            }
163            &YamlOrTask::NotLoaded(ref yaml) => {
164                let script: Rc<Script<BashCommand>> = Rc::new(yaml_to_script(*yaml)?);
165                let script_cell = script.clone();
166
167                *yaml_or_task = YamlOrTask::Loaded(script);
168
169                return Ok(script_cell);
170            }
171        }
172    }
173}
174
175pub struct YamlScriptParser<'a> {
176    pub tasks: HashMap<&'a str, LazyTask<'a>>,
177}
178
179impl<'a> TryFrom<&'a Hash> for YamlScriptParser<'a> {
180    type Error = ();
181
182    fn try_from(yaml_object: &'a Hash) -> Result<Self, Self::Error> {
183        let tasks_result: Result<HashMap<&'a str, LazyTask<'a>, _>, _> = yaml_object
184            .iter()
185            .map(|(yaml_name, yaml_value)| -> Result<_, Self::Error> {
186                return Ok((yaml_name.as_str().ok_or(())?, yaml_value.into()));
187            })
188            .collect();
189        Ok(YamlScriptParser {
190            tasks: tasks_result?,
191        })
192    }
193}
194
195impl ScriptParser<BashCommand> for YamlScriptParser<'_> {
196    fn parse(&self, task_name: &str) -> Result<Rc<Script<BashCommand>>, ()> {
197        self.tasks
198            .get(task_name)
199            .expect(format!("The task {} does not exist", task_name).as_str())
200            .parse()
201    }
202}