// Copyright 2017 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Contains code for converting a UCG Val into an executable script output target. use std; use std::io::{Cursor, Write}; use std::rc::Rc; use crate::build::Val; use crate::build::Val::Tuple; use crate::convert; use crate::convert::traits::{ConvertResult, Converter}; use crate::error::BuildError; use crate::error::ErrorType; pub struct ExecConverter {} // let exec = { // env = [], // command = "", // args = [], // }; impl ExecConverter { pub fn new() -> Self { ExecConverter {} } #[allow(unused_assignments)] fn write(&self, v: &Val, w: &mut dyn Write) -> ConvertResult { // We always expect the Val to be a Tuple. if let &Tuple(ref fields) = v { // We expect no more than three fields in our exec tuple. if fields.len() > 3 { return Err(BuildError::new( "Exec tuples must have no more than 3 fields", ErrorType::TypeFail, ) .to_boxed()); } let mut env: Option<&Vec<(String, Rc)>> = None; let mut command: Option<&str> = None; let mut args: Option<&Vec>> = None; for &(ref name, ref val) in fields.iter() { // We require a command field in our exec tuple. if name == "command" { if command.is_some() { return Err(BuildError::new( "There can only be one command field in an exec tuple", ErrorType::TypeFail, ) .to_boxed()); } if let &Val::Str(ref s) = val.as_ref() { command = Some(s); continue; } return Err(BuildError::new( "The command field of an exec tuple must be a string", ErrorType::TypeFail, ) .to_boxed()); } // We optionally allow an env field in our exec tuple. if name == "env" { if let &Val::Tuple(ref l) = val.as_ref() { if env.is_some() { return Err(BuildError::new( "There can only be one env field in an exec tuple", ErrorType::TypeFail, ) .to_boxed()); } env = Some(l); continue; } return Err(BuildError::new( "The env field of an exec tuple must be a list", ErrorType::TypeFail, ) .to_boxed()); } // We optionally allow an args field in our exec tuple. if name == "args" { if let &Val::List(ref l) = val.as_ref() { if args.is_some() { return Err(BuildError::new( "There can only be one args field of an exec tuple", ErrorType::TypeFail, ) .to_boxed()); } args = Some(l); continue; } return Err(BuildError::new( "The args field of an exec tuple must be a list", ErrorType::TypeFail, ) .to_boxed()); } } if command.is_none() { return Err(BuildError::new( "An exec tuple must have a command field", ErrorType::TypeFail, ) .to_boxed()); } // Okay if we have made it this far then we are ready to start creating our script. let mut script = Cursor::new(vec![]); // 1. First the script prefix line. write!(script, "#!/usr/bin/env bash\n")?; // 2. then some initial setup. for bash hygiene. write!(script, "# Turn on unofficial Bash-Strict-Mode\n")?; write!(script, "set -euo pipefail\n")?; // 3. Then assign our environment variables if let Some(env_list) = env { for &(ref name, ref v) in env_list.iter() { // We only allow string fields in our env tuple. if let &Val::Str(ref s) = v.as_ref() { write!(script, "{}=\"{}\"\n", name, s)?; continue; } return Err(BuildError::new( "The env fields of an exec tuple must contain only string values", ErrorType::TypeFail, ) .to_boxed()); } } write!(script, "\n")?; let flag_converter = convert::flags::FlagConverter::new(); // 4. Then construct our command line. (be sure to use exec) write!(script, "exec {} ", command.unwrap())?; if let Some(arg_list) = args { for v in arg_list.iter() { // We only allow tuples or strings in our args list. match v.as_ref() { &Val::Str(ref s) => { write!(script, "{} ", s)?; } &Val::Tuple(_) => flag_converter.convert(v.clone(), &mut script)?, _ => { return Err(BuildError::new( "Exec args must be a list of strings or tuples of strings.", ErrorType::TypeFail, ) .to_boxed()); } } } } // Put cursor to the beginning of our script so when we copy // we copy the whole thing. script.set_position(0); std::io::copy(&mut script, w)?; return Ok(()); } Err(BuildError::new("Exec outputs must be of type Tuple", ErrorType::TypeFail).to_boxed()) } } impl Converter for ExecConverter { fn convert(&self, v: Rc, mut w: &mut dyn Write) -> ConvertResult { self.write(&v, &mut w) } fn file_ext(&self) -> String { String::from("sh") } fn description(&self) -> String { "Convert ucg Vals into an bash script with \nenvironment variables set and command line arguments sent..".to_string() } #[allow(unused_must_use)] fn help(&self) -> String { include_str!("exec_help.txt").to_string() } } #[cfg(test)] mod exec_test { use super::*; use crate::build::FileBuilder; use crate::convert::traits::Converter; use std; use std::io::Cursor; #[test] fn convert_just_command_test() { let i_paths = Vec::new(); let out: Vec = Vec::new(); let err: Vec = Vec::new(); let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, out, err); let conv = ExecConverter::new(); b.eval_string( "let script = { command = \"/bin/echo\", };", ) .unwrap(); let result = b.get_out_by_name("script").unwrap(); let mut expected = "#!/usr/bin/env bash\n".to_string(); expected.push_str("# Turn on unofficial Bash-Strict-Mode\n"); expected.push_str("set -euo pipefail\n\n"); expected.push_str("exec /bin/echo "); let mut buf = Cursor::new(vec![]); conv.convert(result, &mut buf).unwrap(); assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected); } #[test] fn convert_command_with_env_test() { let i_paths = Vec::new(); let out: Vec = Vec::new(); let err: Vec = Vec::new(); let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, out, err); let conv = ExecConverter::new(); b.eval_string( "let script = { command = \"/bin/echo\", env = { foo = \"bar\", quux = \"baz\", }, };", ) .unwrap(); let result = b.get_out_by_name("script").unwrap(); let mut expected = "#!/usr/bin/env bash\n".to_string(); expected.push_str("# Turn on unofficial Bash-Strict-Mode\n"); expected.push_str("set -euo pipefail\n"); expected.push_str("foo=\"bar\"\n"); expected.push_str("quux=\"baz\"\n"); expected.push_str("\n"); expected.push_str("exec /bin/echo "); let mut buf = Cursor::new(vec![]); conv.convert(result, &mut buf).unwrap(); assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected); } #[test] fn convert_command_with_arg_test() { let i_paths = Vec::new(); let out: Vec = Vec::new(); let err: Vec = Vec::new(); let mut b = FileBuilder::new(std::env::current_dir().unwrap(), &i_paths, out, err); let conv = ExecConverter::new(); b.eval_string( "let script = { command = \"/bin/echo\", env = { foo = \"bar\", quux = \"baz\", }, args = [ \"subcommand\", {flag1 = 1}, ], };", ) .unwrap(); let result = b.get_out_by_name("script").unwrap(); let mut expected = "#!/usr/bin/env bash\n".to_string(); expected.push_str("# Turn on unofficial Bash-Strict-Mode\n"); expected.push_str("set -euo pipefail\n"); expected.push_str("foo=\"bar\"\n"); expected.push_str("quux=\"baz\"\n"); expected.push_str("\n"); expected.push_str("exec /bin/echo subcommand --flag1 1 "); let mut buf = Cursor::new(vec![]); conv.convert(result, &mut buf).unwrap(); assert_eq!(String::from_utf8_lossy(&buf.into_inner()), expected); } }