A reasonable configuration language rcl-lang.org
configuration-language json
at master 183 lines 5.5 kB view raw
1// RCL -- A reasonable configuration language. 2// Copyright 2023 Ruud van Asseldonk 3 4// Licensed under the Apache License, Version 2.0 (the "License"); 5// you may not use this file except in compliance with the License. 6// A copy of the License has been included in the root of the repository. 7 8// This CLI parser is adapted from the one in Squiller [1], which is in turn 9// adapted from the one in Tako [2] which is copyrighted by Arian van Putten, 10// Ruud van Asseldonk, and Tako Marks, and licensed Apache2. 11// [1] https://github.com/ruuda/squiller 12// [2] https://github.com/ruuda/tako 13 14//! Utilities to aid parsing the command line. 15 16use std::fmt; 17use std::vec; 18 19pub enum Arg<T> { 20 Plain(T), 21 Short(T), 22 Long(T), 23 /// An argument `-` that occurred before a bare `--`. 24 StdInOut, 25} 26 27impl Arg<String> { 28 pub fn as_ref(&self) -> Arg<&str> { 29 match *self { 30 Arg::Plain(ref x) => Arg::Plain(&x[..]), 31 Arg::Short(ref x) => Arg::Short(&x[..]), 32 Arg::Long(ref x) => Arg::Long(&x[..]), 33 Arg::StdInOut => Arg::StdInOut, 34 } 35 } 36} 37 38impl fmt::Display for Arg<String> { 39 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 match *self { 41 Arg::Plain(ref x) => write!(f, "{}", x), 42 Arg::Short(ref x) => write!(f, "-{}", x), 43 Arg::Long(ref x) => write!(f, "--{}", x), 44 Arg::StdInOut => write!(f, "-"), 45 } 46 } 47} 48 49pub struct ArgIter { 50 /// Underlying args iterator. 51 args: vec::IntoIter<String>, 52 53 /// Whether we have observed a `--` argument. 54 is_raw: bool, 55 56 /// Leftover to return after an `--foo=bar` or `-fbar`-style argument. 57 /// 58 /// `--foo=bar` is returned as `Long(foo)` followed by `Plain(bar)`. 59 /// `-fbar` is returned as `Short(f)` followed by `Plain(bar)`. 60 leftover: Option<String>, 61} 62 63impl ArgIter { 64 pub fn new(args: Vec<String>) -> ArgIter { 65 ArgIter { 66 args: args.into_iter(), 67 is_raw: false, 68 leftover: None, 69 } 70 } 71} 72 73impl Iterator for ArgIter { 74 type Item = Arg<String>; 75 76 fn next(&mut self) -> Option<Arg<String>> { 77 if self.leftover.is_some() { 78 return self.leftover.take().map(Arg::Plain); 79 } 80 81 let arg = self.args.next()?; 82 83 if self.is_raw { 84 return Some(Arg::Plain(arg)); 85 } 86 87 if &arg == "--" { 88 self.is_raw = true; 89 return self.next(); 90 } 91 92 if let Some(flag_slice) = arg.strip_prefix("--") { 93 let mut flag = String::from(flag_slice); 94 if let Some(i) = flag.find('=') { 95 self.leftover = Some(flag.split_off(i + 1)); 96 flag.truncate(i); 97 } 98 return Some(Arg::Long(flag)); 99 } 100 101 if arg == "-" { 102 return Some(Arg::StdInOut); 103 } 104 105 if let Some(flag_slice) = arg.strip_prefix('-') { 106 let mut flag = String::from(flag_slice); 107 match flag.char_indices().nth(1) { 108 Some((n, _)) if n < flag.len() => { 109 self.leftover = Some(flag.split_off(n)); 110 flag.truncate(n); 111 } 112 _ => {} 113 } 114 return Some(Arg::Short(flag)); 115 } 116 117 Some(Arg::Plain(arg)) 118 } 119} 120 121/// Helper macro to match option values and print help when there is no match. 122macro_rules! match_option { 123 { 124 $args_iter:ident: $option:expr, 125 $( $pat:literal => $val:expr , )+ 126 } => {{ 127 let mut err = vec![ 128 "Expected ".into(), 129 Doc::from($option.to_string()).with_markup(Markup::Highlight), 130 " to be followed by one of ".into(), 131 ]; 132 $( 133 err.push(Doc::from($pat).with_markup(Markup::Highlight)); 134 err.push(", ".into()); 135 )+ 136 err.pop(); 137 err.push(". See --help for usage.".into()); 138 let err = Doc::Concat(err); 139 140 match $args_iter.next() { 141 Some(arg) => match arg.as_ref() { 142 $( Arg::Plain($pat) => $val, )+ 143 _ => return Error::new(err).err(), 144 } 145 None => return Error::new(err).err(), 146 } 147 }}; 148} 149pub(crate) use match_option; 150 151macro_rules! parse_option { 152 { 153 $args_iter:ident: $option:expr, $parser:expr 154 } => { 155 match $args_iter.next() { 156 // Clippy is wrong here; probably it doesn't know that due to the 157 // macro, the construction and call site of the closure are far away. 158 #[allow(clippy::redundant_closure_call)] 159 Some(Arg::Plain(value)) => match $parser(&value[..]) { 160 Ok(r) => r, 161 Err(..) => { 162 let err = concat! { 163 "'" 164 Doc::highlight(&value).into_owned() 165 "' is not valid for " 166 Doc::from($option.to_string()).with_markup(Markup::Highlight) 167 ". See --help for usage." 168 }; 169 return Error::new(err).err(); 170 } 171 } 172 _ => { 173 let err = concat! { 174 "Expected a value after " 175 Doc::from($option.to_string()).with_markup(Markup::Highlight) 176 ". See --help for usage." 177 }; 178 return Error::new(err).err(); 179 } 180 } 181 }; 182} 183pub(crate) use parse_option;