ucg/src/convert/exec.rs

290 lines
11 KiB
Rust
Raw Normal View History

// Copyright 2017 Jeremy Wall <jeremy@marzhillstudios.com>
//
// 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;
2018-12-06 12:23:52 -06:00
use crate::build::Val;
use crate::build::Val::Tuple;
use crate::convert;
use crate::convert::traits::{ConvertResult, Converter};
2018-12-06 12:23:52 -06:00
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<Val>)>> = None;
let mut command: Option<&str> = None;
let mut args: Option<&Vec<Rc<Val>>> = 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.
2018-12-06 12:46:47 -06:00
write!(script, "#!/usr/bin/env bash\n")?;
// 2. then some initial setup. for bash hygiene.
2018-12-06 12:46:47 -06:00
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());
}
}
2018-12-06 12:46:47 -06:00
write!(script, "\n")?;
let flag_converter = convert::flags::FlagConverter::new();
// 4. Then construct our command line. (be sure to use exec)
2018-12-06 12:46:47 -06:00
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) => {
2018-12-06 12:46:47 -06:00
write!(script, "{} ", s)?;
}
2018-12-06 12:46:47 -06:00
&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);
2018-12-06 12:46:47 -06:00
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<Val>, 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::*;
2018-12-31 10:10:19 -06:00
use crate::build::FileBuilder;
2018-12-06 12:23:52 -06:00
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<u8> = Vec::new();
let err: Vec<u8> = 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\",
};",
2018-12-10 21:27:44 -06:00
)
.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<u8> = Vec::new();
let err: Vec<u8> = 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\",
},
};",
2018-12-10 21:27:44 -06:00
)
.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<u8> = Vec::new();
let err: Vec<u8> = 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},
],
};",
2018-12-10 21:27:44 -06:00
)
.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);
}
}