UI: Command line subcommands and arguments are more usable.

The build and validate commands now take a list of files to process.

The outputs for files that specify the are given the same name as
the containing file with the extension changed to the extension that
the converter specifies.
This commit is contained in:
Jeremy Wall 2018-08-15 18:22:05 -05:00
parent 1c464083fb
commit 6418533562
11 changed files with 88 additions and 20 deletions

View File

@ -39,8 +39,9 @@ FLAGS:
-V, --version Prints version information
SUBCOMMANDS:
build Compile a specific ucg file.
build Build a specific ucg file.
help Prints this message or the help of the given subcommand(s)
inspect Inspect a specific symbol in a ucg file.
validate Check a specific ucg file for errors.
```

View File

@ -40,3 +40,5 @@ let server_config = {
},
l = ["foo", "bar"]
};
out json server_config;

View File

@ -319,8 +319,7 @@ pub struct Builder {
build_output: ValueMap,
/// last is the result of the last statement.
pub last: Option<Rc<Val>>,
// FIXME(jwall): This should be a per file mapping.
out_lock: Option<(String, Rc<Val>)>,
pub out_lock: Option<(String, Rc<Val>)>,
}
macro_rules! eval_binary_expr {
@ -541,8 +540,8 @@ impl Builder {
&Statement::Let(ref def) => self.build_let(def),
&Statement::Import(ref def) => self.build_import(def),
&Statement::Expression(ref expr) => self.eval_expr(expr),
// FIXME(jwall): Stash this into an output slot.
// Only one output can be used per file.
// Only one output can be used per file. Right now we enforce this by
// having a single builder per file.
&Statement::Output(ref typ, ref expr) => {
if let None = self.out_lock {
let val = try!(self.eval_expr(expr));

View File

@ -87,4 +87,8 @@ impl Converter for EnvConverter {
fn convert(&self, v: Rc<Val>, mut w: &mut Write) -> Result {
self.write(&v, &mut w)
}
fn file_ext(&self) -> String {
String::from(".env")
}
}

View File

@ -185,6 +185,10 @@ 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")
}
}
#[cfg(test)]

View File

@ -107,6 +107,10 @@ impl Converter for FlagConverter {
fn convert(&self, v: Rc<Val>, mut w: &mut Write) -> Result {
self.write("", &v, &mut w)
}
fn file_ext(&self) -> String {
String::from("txt")
}
}
// We need some unit tests for this now :D

View File

@ -88,4 +88,8 @@ impl Converter for JsonConverter {
fn convert(&self, v: Rc<Val>, mut w: &mut Write) -> Result {
self.write(&v, &mut w)
}
fn file_ext(&self) -> String {
String::from("json")
}
}

View File

@ -64,4 +64,9 @@ impl ConverterRunner {
pub fn convert(&self, v: Rc<Val>, mut w: Box<Write>) -> traits::Result {
self.converter.convert(v, &mut w)
}
/// ext returns the expected file extension for this conversion.
pub fn ext(&self) -> String {
self.converter.file_ext()
}
}

View File

@ -26,4 +26,5 @@ pub type Result = result::Result<(), Box<Error>>;
/// final conversion stage of the ucg compiler.
pub trait Converter {
fn convert(&self, vs: Rc<Val>, w: &mut Write) -> Result;
fn file_ext(&self) -> String;
}

View File

@ -35,23 +35,32 @@ fn do_flags<'a>() -> clap::ArgMatches<'a> {
(version: crate_version!())
(author: crate_authors!())
(about: "Universal Configuration Grammar compiler.")
(@subcommand inspect =>
(about: "Inspect a specific symbol in a ucg file.")
(@arg sym: --sym +takes_value +required "Specify a specific binding in the ucg file to output.")
(@arg target: --format +required +takes_value "Inspect output type. (flags, json, env, exec)")
(@arg INPUT: +required "Input ucg file to inspect symbol from.")
)
(@subcommand build =>
(about: "Compile a specific ucg file.")
(@arg sym: --sym +takes_value +required "Specify a specific let binding in the ucg file to output.")
(@arg target: --target -t +required +takes_value "Target output type. (flags, json, env, exec)")
(about: "Build a specific ucg file.")
(@arg out: --out -o +takes_value "Output file to write to.")
(@arg INPUT: +required "Input ucg file to build.")
(@arg INPUT: ... +required "Input ucg files to build.")
)
(@subcommand validate =>
(about: "Check a specific ucg file for errors.")
(@arg INPUT: +required "Input ucg file to validate.")
(@arg INPUT: ... +required "Input ucg files to validate.")
)
).get_matches()
}
fn run_converter(c: ConverterRunner, v: Rc<Val>, f: Option<&str>) -> traits::Result {
let file: Box<std::io::Write> = match f {
Some(f) => Box::new(try!(File::create(f))),
Some(f) => {
let mut path_buf = PathBuf::from(f);
path_buf.set_extension(c.ext());
let new_path = path_buf.to_str().unwrap();
Box::new(try!(File::create(&new_path)))
}
None => Box::new(io::stdout()),
};
c.convert(v, file)
@ -60,16 +69,15 @@ fn run_converter(c: ConverterRunner, v: Rc<Val>, f: Option<&str>) -> traits::Res
fn main() {
let app = do_flags();
let cache = Rc::new(RefCell::new(MemoryCache::new()));
if let Some(matches) = app.subcommand_matches("build") {
if let Some(matches) = app.subcommand_matches("inspect") {
let file = matches.value_of("INPUT").unwrap();
let out = matches.value_of("out");
let sym = matches.value_of("sym");
let target = matches.value_of("target").unwrap();
let root = PathBuf::from(file);
let mut builder = build::Builder::new(root.parent().unwrap(), cache);
match ConverterRunner::new(target) {
Ok(converter) => {
// FIXME(jwall): What should happen if they requested we build a _test.ucg file.
// TODO(jwall): What should happen if they requested we build a _test.ucg file.
// 1. We could automatically go into validate mode.
// 2. We could warn that this is a test file.
let result = builder.build_file(file);
@ -83,7 +91,8 @@ fn main() {
};
match val {
Some(value) => {
run_converter(converter, value, out).unwrap();
// We use None here because we always output to stdout for an inspect.
run_converter(converter, value, None).unwrap();
eprintln!("Build successful");
process::exit(0);
}
@ -98,12 +107,46 @@ fn main() {
process::exit(1);
}
}
} else if let Some(matches) = app.subcommand_matches("build") {
// TODO(jwall): This should take multiple files I think.
let files = matches.values_of("INPUT").unwrap();
// TODO(jwall): We should default to the file name with appropriate
// extension if this is not set.
for file in files {
let root = PathBuf::from(file);
let mut builder = build::Builder::new(root.parent().unwrap(), cache);
let result = builder.build_file(file);
if !result.is_ok() {
eprintln!("{:?}", result.err().unwrap());
process::exit(1);
}
let (typ, val) = match builder.out_lock {
Some((ref typ, ref val)) => (typ, val.clone()),
None => {
eprintln!("Build results in no value.");
process::exit(1);
}
};
match ConverterRunner::new(typ) {
Ok(converter) => {
run_converter(converter, val, Some(file)).unwrap();
eprintln!("Build successful");
process::exit(0);
}
Err(msg) => {
eprintln!("{}", msg);
process::exit(1);
}
}
}
} else if let Some(matches) = app.subcommand_matches("validate") {
let file = matches.value_of("INPUT").unwrap();
let files = matches.values_of("INPUT").unwrap();
let mut builder = build::Builder::new(std::env::current_dir().unwrap(), cache);
builder.enable_validate_mode();
for file in files {
builder.build_file(file).unwrap();
println!("File Validates");
}
process::exit(0);
}
}

View File

@ -500,7 +500,8 @@ named!(copy_expression<TokenIter, Expression, error::Error>,
fn tuple_to_macro(mut t: (Position, Vec<Value>, Value)) -> ParseResult<Expression> {
match t.2 {
Value::Tuple(v) => Ok(Expression::Macro(MacroDef {
argdefs: t.1
argdefs: t
.1
.drain(0..)
.map(|s| Positioned {
pos: s.pos().clone(),