Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_syntax/tests/argv.rs

25.7 KiB, 1 run

created by r1870400018:62026, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1mod fixture;
2
3use fixture::{
4 ore,
5 words,
6 BACK_EXCESS,
7 BACK_MISSING,
8 FORGET_MISSING,
9 REST_MISSING,
10};
11
12use oxedyne_fe2o3_syntax::{
13 argv::{
14 self,
15 Parsed,
16 },
17 arg::{
18 Arg,
19 ArgConfig,
20 },
21 cmd::{
22 Cmd,
23 CmdConfig,
24 },
25 core::{
26 Syntax,
27 SyntaxConfig,
28 SyntaxRef,
29 },
30 help::Page,
31 key::Key,
32 msg::{
33 Msg,
34 MsgCmd,
35 },
36 val::Val,
37};
38
39use oxedyne_fe2o3_core::{
40 prelude::*,
41 byte::{
42 Encoding,
43 ToBytes,
44 },
45};
46use oxedyne_fe2o3_jdat::prelude::*;
47
48use std::ffi::OsString;
49
50
51fn run(line: &str) -> Outcome<Msg> {
52 let syntax = res!(ore());
53 let msg = Msg::new(syntax);
54 msg.rx_argv(words(line), Some(argv::SIMILARITY_THRESHOLD))
55}
56
57fn cmd<'a>(msg: &'a Msg, name: &str) -> Outcome<&'a MsgCmd> {
58 msg.get_cmd(name).ok_or_else(|| err!("No command '{}' in {}.", name, msg; Missing))
59}
60
61fn refusal(line: &str) -> Outcome<String> {
62 match run(line) {
63 Ok(msg) => Err(err!("'{}' was accepted as {}, and should not have been.", line, msg;
64 Unexpected)),
65 Err(e) => Ok(e.plain()),
66 }
67}
68
69fn strs(v: &[&str]) -> Vec<String> { v.iter().map(|s| s.to_string()).collect() }
70
71// G5: a word the shell has unquoted is the value, not daticle text to decode.
72#[test]
73fn argv_takes_string_values_as_they_stand() -> Outcome<()> {
74 let msg = res!(run_words(vec!["mark", "42", "(x)", "don't", "[a]", "{b}"]));
75 let mark = res!(cmd(&msg, "mark"));
76 req!(res!(mark.str_vals()), vec!["42", "(x)", "don't", "[a]", "{b}"]);
77 // The same words read as a REPL line are decoded, which is what argv mode is for.
78 let line = Msg::new(res!(ore()));
79 if let Ok(msg) = line.rx_text_iter(strs(&["mark", "42"]), None) {
80 return Err(err!("A line reading of 'mark 42' took 42 as {:?}, a string value.",
81 msg; Unexpected));
82 }
83 Ok(())
84}
85
86fn run_words(v: Vec<&str>) -> Outcome<Msg> {
87 let msg = Msg::new(res!(ore()));
88 msg.rx_argv(strs(&v), Some(argv::SIMILARITY_THRESHOLD))
89}
90
91// G1: an option need not have a short form, and one without it registers no bare "-".
92#[test]
93fn long_only_options() -> Outcome<()> {
94 let syntax = res!(ore());
95 let sync = res!(syntax.get_cmd("sync").ok_or_else(|| err!("no sync"; Missing)));
96 req!(sync.args.contains_key(&Key::from("-")), false);
97 req!(sync.args.contains_key(&Key::from("--dry-run")), true);
98 let msg = res!(run("sync --dry-run ../x"));
99 let sync = res!(cmd(&msg, "sync"));
100 req!(sync.has_arg("--dry-run"), true);
101 req!(res!(sync.str_vals()), vec!["../x"]);
102 // The internal name of an option is a value on a command line, not the option.
103 let msg = res!(run("sync dry-run"));
104 let sync = res!(cmd(&msg, "sync"));
105 req!(sync.has_arg("--dry-run"), false);
106 req!(res!(sync.str_vals()), vec!["dry-run"]);
107 Ok(())
108}
109
110// G3, G7: an optional value may be absent, and a value may follow options.
111#[test]
112fn optional_values_and_values_after_options() -> Outcome<()> {
113 for (line, dir, unsigned, mirror) in [
114 ("init", None, false, None),
115 ("init d", Some("d"), false, None),
116 ("init --unsigned d", Some("d"), true, None),
117 ("init d --mirror m", Some("d"), false, Some("m")),
118 ("init --mirror m d", Some("d"), false, Some("m")),
119 ("init --unsigned", None, true, None),
120 ] {
121 let msg = res!(run(line));
122 let init = res!(cmd(&msg, "init"));
123 let vals = res!(init.str_vals());
124 req!(vals.first().copied(), dir, "'{}'", line);
125 let few = vals.len() <= 1;
126 req!(few, true, "'{}'", line);
127 req!(init.has_arg("--unsigned"), unsigned, "'{}'", line);
128 let got = res!(init.str_arg_vals("--mirror"));
129 req!(got.first().copied(), mirror, "'{}'", line);
130 }
131 let e = res!(refusal("init a b"));
132 let ok = e.contains("'b'") && e.contains("position 3") && e.contains("'init'");
133 req!(ok, true, "{}", e);
134 Ok(())
135}
136
137// G4: a repeated value takes every remaining word, and '--' starts the rest.
138#[test]
139fn repeated_values_and_the_rest() -> Outcome<()> {
140 let msg = res!(run("mark n"));
141 req!(res!(res!(cmd(&msg, "mark")).str_vals()), vec!["n"]);
142 let msg = res!(run("mark n fixed the log"));
143 // 'log' is a command's name, and with one command per line it is only a word here.
144 req!(res!(res!(cmd(&msg, "mark")).str_vals()), vec!["n", "fixed", "the", "log"]);
145 req!(msg.cmds.len(), 1);
146
147 let msg = res!(run("forget a --reason r b"));
148 let forget = res!(cmd(&msg, "forget"));
149 req!(res!(forget.str_vals()), vec!["a", "b"]);
150 req!(res!(forget.str_arg_vals("--reason")), vec!["r"]);
151
152 let msg = res!(run("back m -- p q"));
153 let back = res!(cmd(&msg, "back"));
154 req!(res!(back.str_vals()), vec!["m"]);
155 let rest = res!(back.get_rest().ok_or_else(|| err!("no rest"; Missing)));
156 req!(rest.clone(), vec![dat!("p"), dat!("q")]);
157 // After '--' nothing is an option or a command.
158 let msg = res!(run("back m -- --dry-run -h sync"));
159 let back = res!(cmd(&msg, "back"));
160 let rest = res!(back.get_rest().ok_or_else(|| err!("no rest"; Missing)));
161 req!(rest.clone(), vec![dat!("--dry-run"), dat!("-h"), dat!("sync")]);
162 // No '--', no rest, which is not the same as an empty one.
163 let msg = res!(run("back m"));
164 req!(res!(cmd(&msg, "back")).get_rest().is_none(), true);
165 Ok(())
166}
167
168// G12: a command's own sentences replace the generic ones.
169#[test]
170fn commands_own_sentences_for_absence_and_excess() -> Outcome<()> {
171 let e = res!(refusal("back"));
172 req!(e.contains(BACK_MISSING), true, "{}", e);
173 let e = res!(refusal("back a b"));
174 let ok = e.contains(BACK_EXCESS) && e.contains("'b'") && e.contains("position 3");
175 req!(ok, true, "{}", e);
176 let e = res!(refusal("back m --"));
177 req!(e.contains(REST_MISSING), true, "{}", e);
178 let e = res!(refusal("forget"));
179 req!(e.contains(FORGET_MISSING), true, "{}", e);
180 let e = res!(refusal("forget --dry-run"));
181 req!(e.contains(FORGET_MISSING), true, "{}", e);
182 // Without a sentence of its own, the refusal names the command and the value.
183 let e = res!(refusal("sync"));
184 req!(e.contains("The command 'sync' needs <repo|url>."), true, "{}", e);
185 let e = res!(refusal("forget a --reason"));
186 req!(e.contains("The option '--reason' needs <text>."), true, "{}", e);
187 Ok(())
188}
189
190// G3 for an option's value.
191#[test]
192fn optional_option_values() -> Outcome<()> {
193 let msg = res!(run("key --veil --veil-key"));
194 let key = res!(cmd(&msg, "key"));
195 req!(key.has_arg("--veil"), true);
196 req!(key.get_arg_vals("--veil").is_none(), true);
197 req!(key.has_arg("--veil-key"), true);
198 let msg = res!(run("key --veil k"));
199 req!(res!(res!(cmd(&msg, "key")).str_arg_vals("--veil")), vec!["k"]);
200 // An owed option value may itself begin with a hyphen.
201 let msg = res!(run("forget a --reason -x"));
202 req!(res!(res!(cmd(&msg, "forget")).str_arg_vals("--reason")), vec!["-x"]);
203 Ok(())
204}
205
206// G6: exactly one command per line.
207#[test]
208fn one_command_per_line() -> Outcome<()> {
209 let e = res!(refusal("sync x log"));
210 let ok = e.contains("'log'") && e.contains("position 3") && e.contains("'sync'");
211 req!(ok, true, "{}", e);
212 // A syntax without one_cmd still reads several commands on a line, as a REPL does.
213 let mut s = Syntax::from(SyntaxConfig { name: fmt!("repl"), ..Default::default() });
214 s = res!(s.add_cmd(Cmd::from(CmdConfig {
215 name: fmt!("a"), vals: vec![Val::text("x")], ..Default::default() })));
216 s = res!(s.add_cmd(Cmd::from(CmdConfig { name: fmt!("b"), ..Default::default() })));
217 let msg = Msg::new(SyntaxRef::new(s));
218 let msg = res!(msg.rx_argv(strs(&["a", "1", "b"]), None));
219 req!(msg.cmds.len(), 2);
220 Ok(())
221}
222
223// Every command refuses an option it does not know, naming the nearest one.
224#[test]
225fn unknown_options_and_commands_are_refused_with_a_suggestion() -> Outcome<()> {
226 let e = res!(refusal("sync --dry-rn x"));
227 req!(e.contains("Did you mean '--dry-run'?"), true, "{}", e);
228 req!(e.contains("'--dry-rn' at position 2"), true, "{}", e);
229 let e = res!(refusal("mark n --force"));
230 let ok = e.contains("'--force'") && e.contains("'mark'");
231 req!(ok, true, "{}", e);
232 // After a '--' that ends the options there is still only one repository to name.
233 let e = res!(refusal("sync x -- y"));
234 let ok = e.contains("'y' at position 4") && e.contains("more than the command 'sync'");
235 req!(ok, true, "{}", e);
236 let e = res!(refusal("-- sync"));
237 req!(e.contains("no command has been named"), true, "{}", e);
238 // G11: the suggestion is a question, not a question with a full stop after it.
239 let e = res!(refusal("mrak n"));
240 req!(e.contains("Did you mean 'mark'? 'mrak' at position 1 is not a command."), true, "{}", e);
241 req!(e.contains("?."), false, "{}", e);
242 Ok(())
243}
244
245// An unknown verb points at help, generically, not with the old "is not an argument,
246// and neither is it a command of" jargon.
247#[test]
248fn unknown_verb_names_the_help_command() -> Outcome<()> {
249 let e = res!(refusal("frobnicate"));
250 req!(e.contains("'frobnicate' at position 1 is not a command."), true, "{}", e);
251 req!(e.contains("Type 'ore help' for the list."), true, "{}", e);
252 // The suggestion form points at help too.
253 let e = res!(refusal("mrak n"));
254 req!(e.contains("Type 'ore help' for the list."), true, "{}", e);
255 Ok(())
256}
257
258#[test]
259fn numeric_values_are_still_read_as_numbers() -> Outcome<()> {
260 let msg = res!(run("sync x --oresyn-max 3"));
261 let sync = res!(cmd(&msg, "sync"));
262 let n = res!(sync.get_arg_vals("--oresyn-max").ok_or_else(|| err!("none"; Missing)));
263 req!(n.clone(), vec![Dat::U8(3)]);
264 let e = res!(refusal("sync x --oresyn-max 300"));
265 let ok = e.contains("'300' at position 4") && e.contains("must be a u8");
266 req!(ok, true, "{}", e);
267 let e = res!(refusal("sync x --oresyn-max many"));
268 req!(e.contains("'many'"), true, "{}", e);
269 Ok(())
270}
271
272// G10: a word that is not UTF-8 is refused by position, and nothing panics.
273#[cfg(unix)]
274#[test]
275fn non_utf8_words_are_refused_by_position() -> Outcome<()> {
276 use std::os::unix::ffi::OsStringExt;
277 let syntax = res!(ore());
278 let args = vec![
279 OsString::from("ore"),
280 OsString::from("mark"),
281 OsString::from_vec(vec![b'n', 0xff, b'x']),
282 ];
283 match argv::parse(&syntax, args) {
284 Ok(p) => Err(err!("A non-UTF-8 word was accepted: {:?}", p; Unexpected)),
285 Err(e) => {
286 let e = e.plain();
287 let ok = e.contains("position 2") && e.contains("not UTF-8");
288 req!(ok, true, "{}", e);
289 Ok(())
290 },
291 }
292}
293
294// G9: help and version never reach the caller's dispatch.
295#[test]
296fn the_driver_answers_help_and_version_itself() -> Outcome<()> {
297 let syntax = res!(ore());
298 let parse = |v: &[&str]| {
299 let mut args = vec![OsString::from("ore")];
300 for w in v { args.push(OsString::from(w)); }
301 argv::parse(&syntax, args)
302 };
303 for (v, want) in [
304 (vec![], Page::Summary),
305 (vec!["help"], Page::Summary),
306 (vec!["--help"], Page::Summary),
307 (vec!["-h"], Page::Summary),
308 (vec!["help", "--all"], Page::All),
309 (vec!["help", "sync"], Page::Command(fmt!("sync"))),
310 (vec!["help", "capture"], Page::Topic(fmt!("capture"))),
311 (vec!["sync", "--help"], Page::Command(fmt!("sync"))),
312 (vec!["sync", "x", "-h"], Page::Command(fmt!("sync"))),
313 (vec!["forget", "a", "--help"], Page::Command(fmt!("forget"))),
314 ] {
315 match res!(parse(&v)) {
316 Parsed::Help(page) => req!(page, want, "{:?}", v),
317 other => return Err(err!("{:?} gave {:?}, not help.", v, other; Unexpected)),
318 }
319 }
320 match res!(parse(&["--version"])) {
321 Parsed::Version => (),
322 other => return Err(err!("--version gave {:?}.", other; Unexpected)),
323 }
324 // After '--', '-h' is a path, so the command runs.
325 match res!(parse(&["back", "m", "--", "-h"])) {
326 Parsed::Run(msg) => {
327 let back = res!(cmd(&msg, "back"));
328 req!(back.get_rest().cloned(), Some(vec![dat!("-h")]));
329 },
330 other => return Err(err!("'back m -- -h' gave {:?}.", other; Unexpected)),
331 }
332 match parse(&["help", "snyc"]) {
333 Ok(p) => return Err(err!("'help snyc' gave {:?}.", p; Unexpected)),
334 Err(e) => {
335 let e = e.plain();
336 req!(e.contains("Did you mean 'sync'?"), true, "{}", e);
337 },
338 }
339 match parse(&["snyc", "--help"]) {
340 Ok(p) => return Err(err!("'snyc --help' gave {:?}.", p; Unexpected)),
341 Err(e) => {
342 let e = e.plain();
343 req!(e.contains("Did you mean 'sync'?"), true, "{}", e);
344 },
345 }
346 match res!(parse(&["sync", "x", "--dry-run"])) {
347 Parsed::Run(msg) => req!(res!(cmd(&msg, "sync")).has_arg("--dry-run"), true),
348 other => return Err(err!("'sync x --dry-run' gave {:?}.", other; Unexpected)),
349 }
350 Ok(())
351}
352
353#[test]
354fn syntax_shapes_a_parser_cannot_read_are_refused() -> Outcome<()> {
355 let s = Syntax::from(SyntaxConfig { name: fmt!("bad"), ..Default::default() });
356 let c = Cmd::from(CmdConfig {
357 name: fmt!("c"),
358 vals: vec![Val::text("a").many(), Val::text("b")],
359 ..Default::default()
360 });
361 let refused = s.clone().add_cmd(c).is_err();
362 req!(refused, true);
363 let c = Cmd::from(CmdConfig {
364 name: fmt!("c"),
365 vals: vec![Val::text("a").optional(), Val::text("b")],
366 ..Default::default()
367 });
368 let refused = s.clone().add_cmd(c).is_err();
369 req!(refused, true);
370 // A verbatim value that repeats takes every word left, so a rest after it is unreachable.
371 let c = Cmd::from(CmdConfig {
372 name: fmt!("c"),
373 vals: vec![Val::text("a").many().verbatim()],
374 rest: Some(Val::text("b").many()),
375 ..Default::default()
376 });
377 let refused = s.clone().add_cmd(c).is_err();
378 req!(refused, true);
379 // Two options answering to one name used to shadow each other silently.
380 let c = Cmd::from(CmdConfig { name: fmt!("c"), ..Default::default() });
381 let c = res!(c.add_arg(Arg::from(ArgConfig {
382 name: fmt!("x"), hyph1: Some(fmt!("v")), ..Default::default() })));
383 let refused = c.add_arg(Arg::from(ArgConfig {
384 name: fmt!("y"), hyph1: Some(fmt!("v")), ..Default::default() })).is_err();
385 req!(refused, true);
386 Ok(())
387}
388
389/// The wire format of a fixed-arity syntax, pinned to the bytes the encoder produced before
390/// values could be optional or repeated (fe2o3 47bfc8d).
391fn fixed_syntax() -> Outcome<SyntaxRef> {
392 let mut p = Syntax::from(SyntaxConfig {
393 name: fmt!("TestSyntax"),
394 vals: vec![(Kind::Str, fmt!("s")).into(), (Kind::I128, fmt!("n")).into()],
395 ..Default::default()
396 });
397 p = res!(p.add_arg(Arg::from(ArgConfig {
398 name: fmt!("Arg_a"), hyph1: Some(fmt!("a")), hyph2: Some(fmt!("a0")),
399 vals: vec![(Kind::Str, fmt!("s")).into(), (Kind::I32, fmt!("n")).into()],
400 ..Default::default()
401 })));
402 let mut c = Cmd::from(CmdConfig {
403 name: fmt!("cmd"),
404 vals: vec![(Kind::Str, fmt!("s")).into(), (Kind::I16, fmt!("n")).into()],
405 ..Default::default()
406 });
407 c = res!(c.add_arg(Arg::from(ArgConfig {
408 name: fmt!("Arg_b"), hyph1: Some(fmt!("b")), hyph2: Some(fmt!("b0")),
409 vals: vec![(Kind::Str, fmt!("s")).into(), (Kind::U8, fmt!("n")).into()],
410 ..Default::default()
411 })));
412 p = res!(p.add_cmd(c));
413 Ok(SyntaxRef::new(p))
414}
415
416const WIRE_BINARY: [u8; 90] = [
417 1, 51, 33, 25, 41, 33, 5, 104, 101, 108, 108, 111, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
418 0, 0, 0, 42, 33, 1, 11, 0, 0, 51, 33, 15, 41, 33, 7, 103, 111, 111, 100, 98, 121, 101, 18,
419 0, 0, 0, 1, 33, 1, 11, 0, 0, 51, 33, 11, 41, 33, 5, 97, 103, 97, 105, 110, 17, 255, 253, 33,
420 1, 11, 0, 0, 51, 33, 11, 41, 33, 6, 100, 101, 106, 97, 118, 117, 10, 42,
421];
422const WIRE_TEXT: &str =
423 "(str|\"hello\") (i128|42) -a (str|\"goodbye\") (i32|1) cmd (str|\"again\") (i16|-3) \
424 -b (str|\"dejavu\") (u8|42)";
425
426#[test]
427fn the_wire_format_of_a_fixed_syntax_is_unchanged() -> Outcome<()> {
428 let syntax = res!(fixed_syntax());
429 let msg = Msg::new(syntax.clone());
430 let mut m = res!(msg.from_str("hello 42 -a goodbye 1 cmd again -3 -b dejavu 42", None));
431 m.set_encoding(Encoding::Binary);
432 let bin = res!(m.to_bytes(Vec::new()));
433 req!(bin, WIRE_BINARY.to_vec());
434 m.set_encoding(Encoding::UTF8);
435 let txt = res!(m.to_bytes(Vec::new()));
436 req!(txt[0], 2u8);
437 req!(String::from_utf8_lossy(&txt[1..]).to_string(), WIRE_TEXT.to_string());
438 // And the pinned bytes still decode to the message.
439 let back = res!(Msg::new(syntax).from_bytes(&WIRE_BINARY, None));
440 req!(back.vals, vec![dat!("hello"), dat!(42i128)]);
441 let c = res!(cmd(&back, "cmd"));
442 req!(c.vals.clone(), vec![dat!("again"), dat!(-3i16)]);
443 Ok(())
444}
445
446#[test]
447fn a_rest_and_repeated_values_cross_the_wire() -> Outcome<()> {
448 let syntax = res!(ore());
449 for line in ["back m -- p q", "back m", "mark n a b c", "forget a b --reason r"] {
450 let mut m = res!(Msg::new(syntax.clone()).rx_argv(words(line), None));
451 m.set_encoding(Encoding::Binary);
452 let bin = res!(m.to_bytes(Vec::new()));
453 let back = res!(Msg::new(syntax.clone()).from_bytes(&bin, None));
454 req!(back.cmds.len(), 1, "{}", line);
455 for (name, sent) in &m.cmds {
456 let got = res!(cmd(&back, name));
457 req!(got.vals.clone(), sent.vals.clone(), "{}", line);
458 req!(got.rest.clone(), sent.rest.clone(), "{}", line);
459 req!(got.args.clone(), sent.args.clone(), "{}", line);
460 }
461 }
462 Ok(())
463}
464
465/// A chat protocol shaped like Daimond's: commands of fixed values, several to a message.
466fn chat_syntax() -> Outcome<SyntaxRef> {
467 let mut s = res!(Syntax::new("chat").with_default_help_cmd());
468 for (name, n) in [("chat", 1), ("session_switch", 1), ("session_list", 0), ("fs_write", 2)] {
469 s = res!(s.add_cmd(Cmd::from(CmdConfig {
470 name: fmt!("{}", name),
471 vals: (0..n).map(|_| (Kind::Str, fmt!("text")).into()).collect(),
472 ..Default::default()
473 })));
474 }
475 Ok(SyntaxRef::new(s))
476}
477
478#[test]
479fn a_command_name_on_the_wire_fills_a_value_still_owed() -> Outcome<()> {
480 let syntax = res!(chat_syntax());
481 // Owed a value, a command's name is that value, quoted or not, as before fe2o3 11cc494.
482 for (line, name, vals) in [
483 ("chat help", "chat", vec![dat!("help")]),
484 ("chat \"help\"", "chat", vec![dat!("help")]),
485 ("chat chat", "chat", vec![dat!("chat")]),
486 ("fs_write a.txt help", "fs_write", vec![dat!("a.txt"), dat!("help")]),
487 ("fs_write session_list x", "fs_write", vec![dat!("session_list"), dat!("x")]),
488 ] {
489 let m = res!(Msg::new(syntax.clone()).from_str(line, None));
490 req!(m.cmds.len(), 1, "{}", line);
491 req!(res!(cmd(&m, name)).vals.clone(), vals, "{}", line);
492 }
493 // Once the values are given, a command's name still starts the next command.
494 let m = res!(Msg::new(syntax.clone()).from_str("session_switch abc session_list", None));
495 req!(m.cmds.len(), 2);
496 req!(res!(cmd(&m, "session_switch")).vals.clone(), vec![dat!("abc")]);
497 req!(res!(cmd(&m, "session_list")).vals.is_empty(), true);
498 Ok(())
499}
500
501/// A command shaped like `ore mark`: a name and a message, both verbatim, and one option.
502fn say() -> Outcome<SyntaxRef> {
503 let mut s = Syntax::from(SyntaxConfig {
504 name: fmt!("tool"),
505 one_cmd: true,
506 ..Default::default()
507 });
508 let mut c = Cmd::from(CmdConfig {
509 name: fmt!("say"),
510 vals: vec![
511 Val::text("name").missing("say needs a name.").verbatim(),
512 Val::text("words").many().verbatim(),
513 ],
514 ..Default::default()
515 });
516 c = res!(c.add_arg(Arg::from(ArgConfig {
517 name: fmt!("loud"), hyph2: Some(fmt!("loud")), ..Default::default() })));
518 s = res!(s.add_cmd(c));
519 let mut c = Cmd::from(CmdConfig {
520 name: fmt!("get"),
521 vals: vec![Val::text("what")],
522 ..Default::default()
523 });
524 c = res!(c.add_arg(Arg::from(ArgConfig {
525 name: fmt!("why"), hyph2: Some(fmt!("why")), vals: vec![Val::text("text")],
526 ..Default::default() })));
527 s = res!(s.add_cmd(c));
528 Ok(SyntaxRef::new(s))
529}
530
531fn parse_say(v: &[&str]) -> Outcome<Parsed> {
532 let syntax = res!(say());
533 argv::read(&syntax, strs(v))
534}
535
536fn said(v: &[&str]) -> Outcome<(Vec<String>, bool)> {
537 match res!(parse_say(v)) {
538 Parsed::Run(msg) => {
539 let c = res!(msg.cmds.values().next().ok_or_else(|| err!("no command"; Missing)));
540 let vals = res!(c.str_vals()).iter().map(|v| fmt!("{}", v)).collect();
541 Ok((vals, c.has_arg("--loud")))
542 },
543 other => Err(err!("{:?} gave {:?}, not a message.", v, other; Unexpected)),
544 }
545}
546
547fn is_page(v: &[&str], name: &str) -> Outcome<()> {
548 match res!(parse_say(v)) {
549 Parsed::Help(page) => { req!(page, Page::Command(fmt!("{}", name)), "{:?}", v); Ok(()) },
550 other => Err(err!("{:?} gave {:?}, not the page of '{}'.", v, other, name; Unexpected)),
551 }
552}
553
554// A message is what was typed, so a word of it is never read as an option or as help.
555#[test]
556fn a_verbatim_value_takes_words_as_they_were_typed() -> Outcome<()> {
557 // Once the words have begun, every word is one of them.
558 req!(res!(said(&["say", "n", "-h", "--loud", "--", "--help", "x"])),
559 (strs(&["n", "-h", "--loud", "--", "--help", "x"]), false));
560 // Options come first.
561 req!(res!(said(&["say", "--loud", "n", "w"])), (strs(&["n", "w"]), true));
562 // A name may begin with a dash, as a commit's subject can.
563 req!(res!(said(&["say", "-WIP", "w"])), (strs(&["-WIP", "w"]), false));
564 req!(res!(said(&["say", "- fix typo", "w"])), (strs(&["- fix typo", "w"]), false));
565 // Which is the price: a misspelt option there is a name, since nothing can tell them apart.
566 req!(res!(said(&["say", "--lod", "w"])), (strs(&["--lod", "w"]), false));
567 // Before the values begin, a help flag still asks for the page, and '--' still ends the
568 // options, after which even a help flag is a name.
569 res!(is_page(&["say", "-h"], "say"));
570 res!(is_page(&["say", "--help", "w"], "say"));
571 res!(is_page(&["say", "--loud", "-h"], "say"));
572 req!(res!(said(&["say", "--", "-h", "w"])), (strs(&["-h", "w"]), false));
573 req!(res!(said(&["say", "--", "--loud"])), (strs(&["--loud"]), false));
574 match parse_say(&["say", "--"]) {
575 Ok(p) => return Err(err!("'say --' gave {:?}.", p; Unexpected)),
576 Err(e) => req!(e.plain().contains("say needs a name."), true, "{}", e.plain()),
577 }
578 // A command's own option is never a name.
579 match parse_say(&["say", "--loud"]) {
580 Ok(p) => return Err(err!("'say --loud' gave {:?}.", p; Unexpected)),
581 Err(e) => req!(e.plain().contains("say needs a name."), true, "{}", e.plain()),
582 }
583 Ok(())
584}
585
586// A help flag is help only where an option could stand.
587#[test]
588fn a_help_flag_is_help_only_where_an_option_could_stand() -> Outcome<()> {
589 res!(is_page(&["get", "--help"], "get"));
590 res!(is_page(&["get", "x", "-h"], "get"));
591 res!(is_page(&["get", "--why", "w", "--help"], "get"));
592 // An option owed a value takes it.
593 match res!(parse_say(&["get", "x", "--why", "-h"])) {
594 Parsed::Run(msg) => req!(res!(res!(cmd(&msg, "get")).str_arg_vals("--why")), vec!["-h"]),
595 other => return Err(err!("'get x --why -h' gave {:?}.", other; Unexpected)),
596 }
597 // After '--' it is a value.
598 match res!(parse_say(&["get", "--", "--help"])) {
599 Parsed::Run(msg) => req!(res!(res!(cmd(&msg, "get")).str_vals()), vec!["--help"]),
600 other => return Err(err!("'get -- --help' gave {:?}.", other; Unexpected)),
601 }
602 // Words before it are still read, and a refusal among them is the answer.
603 match parse_say(&["get", "--bogus", "-h"]) {
604 Ok(p) => return Err(err!("'get --bogus -h' gave {:?}.", p; Unexpected)),
605 Err(e) => req!(e.plain().contains("'--bogus'"), true, "{}", e.plain()),
606 }
607 Ok(())
608}
609
610// Only a word with the shape of an option is read as one; any other word is a value.
611#[test]
612fn only_option_shaped_words_are_options() -> Outcome<()> {
613 for w in ["-v", "-abc", "-h", "--dry-run", "--x_y", "--dry-run=1", "--oresyn-max=x y"] {
614 req!(Msg::looks_like_option(w), true, "{}", w);
615 }
616 for w in ["-", "--", "---", "-5", "-.5", "- fix typo", "-x.txt", "--no verify", "--=x", "-é"] {
617 req!(Msg::looks_like_option(w), false, "{}", w);
618 }
619 let msg = res!(run("forget -x.txt"));
620 req!(res!(res!(cmd(&msg, "forget")).str_vals()), vec!["-x.txt"]);
621 let syntax = res!(ore());
622 let msg = res!(Msg::new(syntax).rx_argv(strs(&["sync", "- a"]), None));
623 req!(res!(res!(cmd(&msg, "sync")).str_vals()), vec!["- a"]);
624 Ok(())
625}
626
627// '--' ends the options: every word after it is one of the command's values, until a
628// command with a rest of its own gives the words after it to that instead.
629#[test]
630fn a_double_dash_ends_the_options() -> Outcome<()> {
631 let msg = res!(run("forget --dry-run -- --reason b"));
632 let forget = res!(cmd(&msg, "forget"));
633 req!(res!(forget.str_vals()), vec!["--reason", "b"]);
634 req!(forget.has_arg("--dry-run"), true);
635 req!(forget.has_arg("--reason"), false);
636 let msg = res!(run("sync -- --pull-only"));
637 let sync = res!(cmd(&msg, "sync"));
638 req!(res!(sync.str_vals()), vec!["--pull-only"]);
639 req!(sync.has_arg("--pull-only"), false);
640 let msg = res!(run("back m -- --dry-run"));
641 let back = res!(cmd(&msg, "back"));
642 req!(res!(back.str_vals()), vec!["m"]);
643 req!(back.get_rest().cloned(), Some(vec![dat!("--dry-run")]));
644 Ok(())
645}