A reasonable configuration language
rcl-lang.org
configuration-language
json
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;