mirror of
https://github.com/zaphar/ucg.git
synced 2025-07-22 18:19:54 -04:00
284 lines
11 KiB
Rust
284 lines
11 KiB
Rust
// 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;
|
|
|
|
use ast::{Position, Positioned};
|
|
use build::Val;
|
|
use build::Val::Tuple;
|
|
use convert;
|
|
use convert::traits::{Converter, Result};
|
|
use error::Error;
|
|
use 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 Write) -> Result {
|
|
// 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(Box::new(Error::new(
|
|
"Exec tuples must have no more than 3 fields",
|
|
ErrorType::TypeFail,
|
|
Position::new(0, 0, 0),
|
|
)));
|
|
}
|
|
let mut env: Option<&Vec<(Positioned<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.val == "command" {
|
|
if command.is_some() {
|
|
return Err(Box::new(Error::new(
|
|
"There can only be one command field in an exec tuple",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
if let &Val::Str(ref s) = val.as_ref() {
|
|
command = Some(s);
|
|
continue;
|
|
}
|
|
return Err(Box::new(Error::new(
|
|
"The command field of an exec tuple must be a string",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
// We optionally allow an env field in our exec tuple.
|
|
if name.val == "env" {
|
|
if let &Val::Tuple(ref l) = val.as_ref() {
|
|
if env.is_some() {
|
|
return Err(Box::new(Error::new(
|
|
"There can only be one env field in an exec tuple",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
env = Some(l);
|
|
continue;
|
|
}
|
|
return Err(Box::new(Error::new(
|
|
"The env field of an exec tuple must be a list",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
// We optionally allow an args field in our exec tuple.
|
|
if name.val == "args" {
|
|
if let &Val::List(ref l) = val.as_ref() {
|
|
if args.is_some() {
|
|
return Err(Box::new(Error::new(
|
|
"There can only be one args field of an exec tuple",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
args = Some(l);
|
|
continue;
|
|
}
|
|
return Err(Box::new(Error::new(
|
|
"The args field of an exec tuple must be a list",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
}
|
|
if command.is_none() {
|
|
return Err(Box::new(Error::new(
|
|
"An exec tuple must have a command field",
|
|
ErrorType::TypeFail,
|
|
Position::new(0, 0, 0),
|
|
)));
|
|
}
|
|
// 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.
|
|
try!(write!(script, "#!/usr/bin/env bash\n"));
|
|
// 2. then some initial setup. for bash hygiene.
|
|
try!(write!(script, "# Turn on unofficial Bash-Strict-Mode\n"));
|
|
try!(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() {
|
|
try!(write!(script, "{}=\"{}\"\n", name.val, s));
|
|
continue;
|
|
}
|
|
return Err(Box::new(Error::new(
|
|
"The env fields of an exec tuple must contain only string values",
|
|
ErrorType::TypeFail,
|
|
name.pos.clone(),
|
|
)));
|
|
}
|
|
}
|
|
try!(write!(script, "\n"));
|
|
// TODO(jwall): Should Flag converter have a strict mode?
|
|
let flag_converter = convert::flags::FlagConverter::new();
|
|
// 4. Then construct our command line. (be sure to use exec)
|
|
try!(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) => {
|
|
try!(write!(script, "{} ", s));
|
|
}
|
|
&Val::Tuple(_) => try!(flag_converter.convert(v.clone(), &mut script)),
|
|
_ => {
|
|
return Err(Box::new(Error::new(
|
|
"Exec args must be a list of strings or tuples of strings.",
|
|
ErrorType::TypeFail,
|
|
Position::new(0, 0, 0),
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Put cursor to the beginning of our script so when we copy
|
|
// we copy the whole thing.
|
|
script.set_position(0);
|
|
try!(std::io::copy(&mut script, w));
|
|
return Ok(());
|
|
}
|
|
|
|
Err(Box::new(Error::new(
|
|
"Exec outputs must be of type Tuple",
|
|
ErrorType::TypeFail,
|
|
Position::new(0, 0, 0),
|
|
)))
|
|
}
|
|
}
|
|
|
|
impl Converter for ExecConverter {
|
|
fn convert(&self, v: Rc<Val>, mut w: &mut Write) -> Result {
|
|
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()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod exec_test {
|
|
use super::*;
|
|
use build::assets::MemoryCache;
|
|
use build::Builder;
|
|
use convert::traits::Converter;
|
|
|
|
use std;
|
|
use std::cell::RefCell;
|
|
use std::io::Cursor;
|
|
|
|
#[test]
|
|
fn convert_just_command_test() {
|
|
let cache = Rc::new(RefCell::new(MemoryCache::new()));
|
|
let mut b = Builder::new(std::env::current_dir().unwrap(), cache);
|
|
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 cache = Rc::new(RefCell::new(MemoryCache::new()));
|
|
let mut b = Builder::new(std::env::current_dir().unwrap(), cache);
|
|
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 cache = Rc::new(RefCell::new(MemoryCache::new()));
|
|
let mut b = Builder::new(std::env::current_dir().unwrap(), cache);
|
|
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);
|
|
}
|
|
}
|