1use regex::Regex;
4use std::sync::LazyLock;
5
6#[derive(Debug, Clone)]
13pub struct CmdLine {
14 #[allow(dead_code)]
18 name: String,
19 contents: Vec<String>,
21}
22
23impl Default for CmdLine {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl CmdLine {
30 pub fn new() -> Self {
32 CmdLine {
33 name: "command line".to_string(),
34 contents: Vec::new(),
35 }
36 }
37 pub fn push_toml_line(&mut self, line: String) {
39 self.contents.push(line);
40 }
41
42 fn convert_toml_error(
45 &self,
46 toml_str: &str,
47 error_message: &str,
48 span: &Option<std::ops::Range<usize>>,
49 ) -> String {
50 let linepos = |idx| toml_str.bytes().take(idx).filter(|b| *b == b'\n').count();
52
53 let source_line = span
56 .as_ref()
57 .and_then(|range| {
58 let startline = linepos(range.start);
59 let endline = linepos(range.end);
60 (startline == endline).then_some(startline)
61 })
62 .and_then(|pos| self.contents.get(pos));
63
64 let within = span.as_ref().and_then(|r| toml_str.get(r.clone()));
65
66 match (source_line, within) {
67 (Some(source), _) => {
68 format!("Couldn't parse command line: {error_message} in {source:?}")
69 }
70 (None, Some(within)) => {
71 format!("Couldn't parse command line: {error_message} within {within:?}",)
72 }
73 _ => format!("Couldn't parse command line: {error_message}"),
74 }
75 }
76
77 fn build_toml(&self) -> String {
79 let mut toml_s = String::new();
80 for line in &self.contents {
81 toml_s.push_str(tweak_toml_bareword(line).as_ref().unwrap_or(line));
82 toml_s.push('\n');
83 }
84 toml_s
85 }
86}
87
88impl figment::Provider for CmdLine {
89 fn metadata(&self) -> figment::Metadata {
90 figment::Metadata::named("command line")
91 }
92
93 fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
94 let toml_str = self.build_toml();
95 let toml: toml::Value = toml::from_str(&toml_str).map_err(|toml_err| {
96 self.convert_toml_error(&toml_str, toml_err.message(), &toml_err.span())
97 })?;
98
99 figment::providers::Serialized::defaults(toml).data()
100 }
101}
102
103fn tweak_toml_bareword(s: &str) -> Option<String> {
111 static RE: LazyLock<Regex> = LazyLock::new(|| {
113 Regex::new(
114 r#"(?x:
115 ^
116 [ \t]*
117 # first capture group: dotted barewords
118 ((?:[a-zA-Z0-9_\-]+\.)*
119 [a-zA-Z0-9_\-]+)
120 [ \t]*=[ \t]*
121 # second group: a string without hyphens (e.g. bareword, a disk path, etc.)
122 ([a-zA-Z0-9_:\./\\]+)
123 [ \t]*
124 $)"#,
125 )
126 .expect("Built-in regex compilation failed")
127 });
128
129 RE.captures(s).map(|c| format!("{}=\"{}\"", &c[1], &c[2]))
130}
131
132#[cfg(test)]
133mod test {
134 #![allow(clippy::bool_assert_comparison)]
136 #![allow(clippy::clone_on_copy)]
137 #![allow(clippy::dbg_macro)]
138 #![allow(clippy::mixed_attributes_style)]
139 #![allow(clippy::print_stderr)]
140 #![allow(clippy::print_stdout)]
141 #![allow(clippy::single_char_pattern)]
142 #![allow(clippy::unwrap_used)]
143 #![allow(clippy::unchecked_time_subtraction)]
144 #![allow(clippy::useless_vec)]
145 #![allow(clippy::needless_pass_by_value)]
146 #![allow(clippy::string_slice)] use super::*;
149 use figment::Provider as _;
150
151 #[test]
152 fn bareword_expansion() {
153 assert_eq!(tweak_toml_bareword("dsfklj"), None);
154 assert_eq!(tweak_toml_bareword("=99"), None);
155 assert_eq!(tweak_toml_bareword("=[1,2,3]"), None);
156 assert_eq!(tweak_toml_bareword("a=b-c"), None);
157
158 assert_eq!(tweak_toml_bareword("a=bc"), Some("a=\"bc\"".into()));
159 assert_eq!(tweak_toml_bareword("a=b_c"), Some("a=\"b_c\"".into()));
160 assert_eq!(
161 tweak_toml_bareword("hello.there.now=a_greeting"),
162 Some("hello.there.now=\"a_greeting\"".into())
163 );
164 }
165
166 #[test]
167 fn conv_toml_error() {
168 let mut cl = CmdLine::new();
169 cl.push_toml_line("Hello=world".to_string());
170 cl.push_toml_line("Hola=mundo".to_string());
171 cl.push_toml_line("Bonjour=monde".to_string());
172 let toml_s = cl.build_toml();
173
174 assert_eq!(
175 &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(0..13)),
176 "Couldn't parse command line: Nice greeting in \"Hello=world\""
177 );
178
179 assert_eq!(
180 &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(99..333)),
181 "Couldn't parse command line: Nice greeting"
182 );
183
184 assert_eq!(
185 &cl.convert_toml_error(&toml_s, "Nice greeting with a thing", &Some(0..13)),
186 "Couldn't parse command line: Nice greeting with a thing in \"Hello=world\""
187 );
188 }
189
190 #[test]
191 fn parse_good() {
192 let mut cl = CmdLine::default();
193 cl.push_toml_line("a=3".to_string());
194 cl.push_toml_line("bcd=hello".to_string());
195 cl.push_toml_line("ef=\"gh i\"".to_string());
196 cl.push_toml_line("w=[1,2,3]".to_string());
197 cl.push_toml_line("dir1=.".to_string());
198 cl.push_toml_line("dir2=../".to_string());
199 cl.push_toml_line("dir3=../my_directory".to_string());
200 cl.push_toml_line("dir4=C:\\\\temp\\\\arti".to_string());
201
202 let v = cl
203 .data()
204 .unwrap()
205 .remove(&figment::Profile::Default)
206 .unwrap();
207
208 assert_eq!(v["a"], "3".into());
209 assert_eq!(v["bcd"], "hello".into());
210 assert_eq!(v["ef"], "gh i".into());
211 assert_eq!(v["w"], vec![1, 2, 3].into());
212 assert_eq!(v["dir1"], ".".into());
213 assert_eq!(v["dir2"], "../".into());
214 assert_eq!(v["dir3"], "../my_directory".into());
215 assert_eq!(v["dir4"], "C:\\temp\\arti".into());
216 }
217
218 #[test]
219 fn parse_bad() {
220 let mut cl = CmdLine::default();
221 cl.push_toml_line("x=1 1 1 1 1".to_owned());
222 let v = cl.data();
223 assert!(v.is_err());
224 }
225}