ucglib/convert/
exec.rs

1// Copyright 2017 Jeremy Wall <jeremy@marzhillstudios.com>
2//
3//  Licensed under the Apache License, Version 2.0 (the "License");
4//  you may not use this file except in compliance with the License.
5//  You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14
15//! Contains code for converting a UCG Val into an executable script output target.
16use std;
17use std::io::{Cursor, Write};
18use std::rc::Rc;
19
20use crate::build::Val;
21use crate::build::Val::Tuple;
22use crate::convert;
23use crate::convert::traits::{ConvertResult, Converter};
24use crate::error::BuildError;
25use crate::error::ErrorType;
26
27pub struct ExecConverter {}
28
29// let exec = {
30// env = [],
31// command = "",
32// args = [],
33// };
34impl ExecConverter {
35    pub fn new() -> Self {
36        ExecConverter {}
37    }
38
39    #[allow(unused_assignments)]
40    fn write(&self, v: &Val, w: &mut dyn Write) -> ConvertResult {
41        // We always expect the Val to be a Tuple.
42        if let &Tuple(ref fields) = v {
43            // We expect no more than three fields in our exec tuple.
44            if fields.len() > 3 {
45                return Err(BuildError::new(
46                    "Exec tuples must have no more than 3 fields",
47                    ErrorType::TypeFail,
48                )
49                .to_boxed());
50            }
51            let mut env: Option<&Vec<(String, Rc<Val>)>> = None;
52            let mut command: Option<&str> = None;
53            let mut args: Option<&Vec<Rc<Val>>> = None;
54            for &(ref name, ref val) in fields.iter() {
55                // We require a command field in our exec tuple.
56                if name == "command" {
57                    if command.is_some() {
58                        return Err(BuildError::new(
59                            "There can only be one command field in an exec tuple",
60                            ErrorType::TypeFail,
61                        )
62                        .to_boxed());
63                    }
64                    if let &Val::Str(ref s) = val.as_ref() {
65                        command = Some(s);
66                        continue;
67                    }
68                    return Err(BuildError::new(
69                        "The command field of an exec tuple must be a string",
70                        ErrorType::TypeFail,
71                    )
72                    .to_boxed());
73                }
74                // We optionally allow an env field in our exec tuple.
75                if name == "env" {
76                    if let &Val::Tuple(ref l) = val.as_ref() {
77                        if env.is_some() {
78                            return Err(BuildError::new(
79                                "There can only be one env field in an exec tuple",
80                                ErrorType::TypeFail,
81                            )
82                            .to_boxed());
83                        }
84                        env = Some(l);
85                        continue;
86                    }
87                    return Err(BuildError::new(
88                        "The env field of an exec tuple must be a list",
89                        ErrorType::TypeFail,
90                    )
91                    .to_boxed());
92                }
93                // We optionally allow an args field in our exec tuple.
94                if name == "args" {
95                    if let &Val::List(ref l) = val.as_ref() {
96                        if args.is_some() {
97                            return Err(BuildError::new(
98                                "There can only be one args field of an exec tuple",
99                                ErrorType::TypeFail,
100                            )
101                            .to_boxed());
102                        }
103                        args = Some(l);
104                        continue;
105                    }
106                    return Err(BuildError::new(
107                        "The args field of an exec tuple must be a list",
108                        ErrorType::TypeFail,
109                    )
110                    .to_boxed());
111                }
112            }
113            if command.is_none() {
114                return Err(BuildError::new(
115                    "An exec tuple must have a command field",
116                    ErrorType::TypeFail,
117                )
118                .to_boxed());
119            }
120            // Okay if we have made it this far then we are ready to start creating our script.
121            let mut script = Cursor::new(vec![]);
122            // 1. First the script prefix line.
123            write!(script, "#!/usr/bin/env bash\n")?;
124            // 2. then some initial setup. for bash hygiene.
125            write!(script, "# Turn on unofficial Bash-Strict-Mode\n")?;
126            write!(script, "set -euo pipefail\n")?;
127            // 3. Then assign our environment variables
128            if let Some(env_list) = env {
129                for &(ref name, ref v) in env_list.iter() {
130                    // We only allow string fields in our env tuple.
131                    if let &Val::Str(ref s) = v.as_ref() {
132                        write!(script, "{}=\"{}\"\n", name, s)?;
133                        continue;
134                    }
135                    return Err(BuildError::new(
136                        "The env fields of an exec tuple must contain only string values",
137                        ErrorType::TypeFail,
138                    )
139                    .to_boxed());
140                }
141            }
142            write!(script, "\n")?;
143            let flag_converter = convert::flags::FlagConverter::new();
144            // 4. Then construct our command line. (be sure to use exec)
145            write!(script, "exec {} ", command.unwrap())?;
146            if let Some(arg_list) = args {
147                for v in arg_list.iter() {
148                    // We only allow tuples or strings in our args list.
149                    match v.as_ref() {
150                        &Val::Str(ref s) => {
151                            write!(script, "{} ", s)?;
152                        }
153                        &Val::Tuple(_) => flag_converter.convert(v.clone(), &mut script)?,
154                        _ => {
155                            return Err(BuildError::new(
156                                "Exec args must be a list of strings or tuples of strings.",
157                                ErrorType::TypeFail,
158                            )
159                            .to_boxed());
160                        }
161                    }
162                }
163            }
164            // Put cursor to the beginning of our script so when we copy
165            // we copy the whole thing.
166            script.set_position(0);
167            std::io::copy(&mut script, w)?;
168            return Ok(());
169        }
170
171        Err(BuildError::new("Exec outputs must be of type Tuple", ErrorType::TypeFail).to_boxed())
172    }
173}
174
175impl Converter for ExecConverter {
176    fn convert(&self, v: Rc<Val>, mut w: &mut dyn Write) -> ConvertResult {
177        self.write(&v, &mut w)
178    }
179
180    fn file_ext(&self) -> String {
181        String::from("sh")
182    }
183
184    fn description(&self) -> String {
185        "Convert ucg Vals into an bash script with \nenvironment variables set and command line arguments sent..".to_string()
186    }
187
188    #[allow(unused_must_use)]
189    fn help(&self) -> String {
190        include_str!("exec_help.txt").to_string()
191    }
192}
193
194#[cfg(test)]
195mod exec_test {
196    use std::cell::RefCell;
197
198    use super::*;
199    use crate::build::opcode::Environment;
200    use crate::build::FileBuilder;
201    use crate::convert::traits::Converter;
202
203    use std;
204    use std::io::Cursor;
205
206    #[test]
207    fn convert_just_command_test() {
208        let i_paths = Vec::new();
209        let out: Vec<u8> = Vec::new();
210        let err: Vec<u8> = Vec::new();
211        let env = RefCell::new(Environment::new(out, err));
212        let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, &env);
213        let conv = ExecConverter::new();
214        b.eval_string(
215            "let script = {
216            command = \"/bin/echo\",
217        };",
218        )
219        .unwrap();
220        let result = b.get_out_by_name("script").unwrap();
221        let mut expected = "#!/usr/bin/env bash\n".to_string();
222        expected.push_str("# Turn on unofficial Bash-Strict-Mode\n");
223        expected.push_str("set -euo pipefail\n\n");
224        expected.push_str("exec /bin/echo ");
225        let mut buf = Cursor::new(vec![]);
226        conv.convert(result, &mut buf).unwrap();
227        assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected);
228    }
229
230    #[test]
231    fn convert_command_with_env_test() {
232        let i_paths = Vec::new();
233        let out: Vec<u8> = Vec::new();
234        let err: Vec<u8> = Vec::new();
235        let env = RefCell::new(Environment::new(out, err));
236        let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, &env);
237        let conv = ExecConverter::new();
238        b.eval_string(
239            "let script = {
240            command = \"/bin/echo\",
241            env = {
242                foo = \"bar\",
243                quux = \"baz\",
244            },
245        };",
246        )
247        .unwrap();
248        let result = b.get_out_by_name("script").unwrap();
249        let mut expected = "#!/usr/bin/env bash\n".to_string();
250        expected.push_str("# Turn on unofficial Bash-Strict-Mode\n");
251        expected.push_str("set -euo pipefail\n");
252        expected.push_str("foo=\"bar\"\n");
253        expected.push_str("quux=\"baz\"\n");
254        expected.push_str("\n");
255        expected.push_str("exec /bin/echo ");
256        let mut buf = Cursor::new(vec![]);
257        conv.convert(result, &mut buf).unwrap();
258        assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected);
259    }
260
261    #[test]
262    fn convert_command_with_arg_test() {
263        let i_paths = Vec::new();
264        let out: Vec<u8> = Vec::new();
265        let err: Vec<u8> = Vec::new();
266        let env = RefCell::new(Environment::new(out, err));
267        let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, &env);
268        let conv = ExecConverter::new();
269        b.eval_string(
270            "let script = {
271            command = \"/bin/echo\",
272            env = {
273                foo = \"bar\",
274                quux = \"baz\",
275            },
276            args = [
277                \"subcommand\",
278                {flag1 = 1},
279            ],
280        };",
281        )
282        .unwrap();
283        let result = b.get_out_by_name("script").unwrap();
284        let mut expected = "#!/usr/bin/env bash\n".to_string();
285        expected.push_str("# Turn on unofficial Bash-Strict-Mode\n");
286        expected.push_str("set -euo pipefail\n");
287        expected.push_str("foo=\"bar\"\n");
288        expected.push_str("quux=\"baz\"\n");
289        expected.push_str("\n");
290        expected.push_str("exec /bin/echo subcommand --flag1 1 ");
291        let mut buf = Cursor::new(vec![]);
292        conv.convert(result, &mut buf).unwrap();
293        assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected);
294    }
295}