1use std::ops::Range;
2
3use crate::{
4 args::{Arg, State},
5 buffer::{Block, Color, Doc, Style, Token},
6 item::{Item, ShortLong},
7 meta_help::Metavar,
8 meta_youmean::{Suggestion, Variant},
9 Meta,
10};
11
12#[derive(Debug)]
14pub struct Error(pub(crate) Message);
15
16impl Error {
17 pub(crate) fn combine_with(self, other: Self) -> Self {
18 Error(self.0.combine_with(other.0))
19 }
20}
21
22#[derive(Debug)]
23pub(crate) enum Message {
24 NoEnv(&'static str),
27
28 ParseSome(&'static str),
30
31 ParseFail(&'static str),
33
34 PureFailed(String),
36
37 Missing(Vec<MissingItem>),
41
42 ParseFailure(ParseFailure),
45
46 StrictPos(usize, Metavar),
49
50 NonStrictPos(usize, Metavar),
52
53 ParseFailed(Option<usize>, String),
55
56 GuardFailed(Option<usize>, &'static str),
58
59 NoArgument(usize, Metavar),
65
66 Unconsumed(usize),
69
70 Ambiguity(usize, String),
72
73 Suggestion(usize, Suggestion),
75
76 Conflict(usize, usize),
79
80 Expected(Vec<Item>, Option<usize>),
82
83 OnlyOnce(usize, usize),
85}
86
87impl Message {
88 pub(crate) fn can_catch(&self) -> bool {
89 match self {
90 Message::NoEnv(_)
91 | Message::ParseSome(_)
92 | Message::ParseFail(_)
93 | Message::Missing(_)
94 | Message::PureFailed(_)
95 | Message::NonStrictPos(_, _) => true,
96 Message::StrictPos(_, _)
97 | Message::ParseFailed(_, _)
98 | Message::GuardFailed(_, _)
99 | Message::Unconsumed(_)
100 | Message::Ambiguity(_, _)
101 | Message::Suggestion(_, _)
102 | Message::Conflict(_, _)
103 | Message::ParseFailure(_)
104 | Message::Expected(_, _)
105 | Message::OnlyOnce(_, _)
106 | Message::NoArgument(_, _) => false,
107 }
108 }
109
110 pub(crate) fn wrong_input(&self) -> bool {
112 matches!(
113 self,
114 Message::StrictPos(_, _)
115 | Message::NonStrictPos(_, _)
116 | Message::NoArgument(_, _)
117 | Message::Unconsumed(_)
118 | Message::Ambiguity(_, _)
119 )
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct MissingItem {
126 pub(crate) item: Item,
128 pub(crate) position: usize,
130 pub(crate) scope: Range<usize>,
133}
134
135impl Message {
136 #[must_use]
137 pub(crate) fn combine_with(self, other: Self) -> Self {
138 #[allow(clippy::match_same_arms)]
139 match (self, other) {
140 (a @ Message::ParseFailure(_), _) => a,
142 (_, b @ Message::ParseFailure(_)) => b,
143
144 (Message::Missing(mut a), Message::Missing(mut b)) => {
146 a.append(&mut b);
147 Message::Missing(a)
148 }
149
150 (a, b) => {
152 if a.can_catch() {
153 b
154 } else {
155 a
156 }
157 }
158 }
159 }
160}
161
162#[derive(Clone, Debug)]
181pub enum ParseFailure {
182 Stdout(Doc, bool),
184 Completion(String),
187 Stderr(Doc),
189}
190
191impl ParseFailure {
192 #[allow(clippy::must_use_candidate)]
198 #[track_caller]
199 pub fn unwrap_stderr(self) -> String {
200 match self {
201 Self::Stderr(err) => err.monochrome(true),
202 Self::Completion(..) | Self::Stdout(..) => panic!("not an stderr: {:?}", self),
203 }
204 }
205
206 #[allow(clippy::must_use_candidate)]
212 #[track_caller]
213 pub fn unwrap_stdout(self) -> String {
214 match self {
215 Self::Stdout(err, full) => err.monochrome(full),
216 Self::Completion(s) => s,
217 Self::Stderr(..) => panic!("not an stdout: {:?}", self),
218 }
219 }
220
221 #[allow(clippy::must_use_candidate)]
223 pub fn exit_code(self) -> i32 {
224 match self {
225 Self::Stdout(..) | Self::Completion(..) => 0,
226 Self::Stderr(..) => 1,
227 }
228 }
229
230 #[doc(hidden)]
231 #[deprecated = "Please use ParseFailure::print_message, with two s"]
232 pub fn print_mesage(&self, max_width: usize) {
233 self.print_message(max_width)
234 }
235
236 pub fn print_message(&self, max_width: usize) {
238 let color = Color::default();
239 match self {
240 ParseFailure::Stdout(msg, full) => {
241 println!("{}", msg.render_console(*full, color, max_width));
242 }
243 ParseFailure::Completion(s) => {
244 print!("{}", s);
245 }
246 ParseFailure::Stderr(msg) => {
247 #[allow(unused_mut)]
248 let mut error;
249 #[cfg(not(feature = "color"))]
250 {
251 error = "Error: ";
252 }
253
254 #[cfg(feature = "color")]
255 {
256 error = String::new();
257 color.push_str(Style::Invalid, &mut error, "Error: ");
258 }
259
260 eprintln!("{}{}", error, msg.render_console(true, color, max_width));
261 }
262 }
263 }
264}
265
266fn check_conflicts(args: &State) -> Option<Message> {
267 let (loser, winner) = args.conflict()?;
268 Some(Message::Conflict(winner, loser))
269}
270
271fn textual_part(args: &State, ix: Option<usize>) -> Option<std::borrow::Cow<'_, str>> {
272 match args.items.get(ix?)? {
273 Arg::Short(_, _, _) | Arg::Long(_, _, _) => None,
274 Arg::ArgWord(s) | Arg::Word(s) | Arg::PosWord(s) => Some(s.to_string_lossy()),
275 }
276}
277
278fn only_once(args: &State, cur: usize) -> Option<usize> {
279 if cur == 0 {
280 return None;
281 }
282 let mut iter = args.items[..cur].iter().rev();
283 let offset = match args.items.get(cur)? {
284 Arg::Short(s, _, _) => iter.position(|a| a.match_short(*s)),
285 Arg::Long(l, _, _) => iter.position(|a| a.match_long(l)),
286 Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_) => None,
287 };
288 Some(cur - offset? - 1)
289}
290
291impl Message {
292 #[allow(clippy::too_many_lines)] pub(crate) fn render(mut self, args: &State, meta: &Meta) -> ParseFailure {
294 match self {
296 Message::Unconsumed(ix) => {
297 if let Some(conflict) = check_conflicts(args) {
298 self = conflict;
299 } else if let Some(prev_ix) = only_once(args, ix) {
300 self = Message::OnlyOnce(prev_ix, ix);
301 } else if let Some((ix, suggestion)) = crate::meta_youmean::suggest(args, meta) {
302 self = Message::Suggestion(ix, suggestion);
303 }
304 }
305 Message::Missing(xs) => {
306 self = summarize_missing(&xs, meta, args);
307 }
308 _ => {}
309 }
310
311 let mut doc = Doc::default();
312 match self {
313 Message::ParseFailure(f) => return f,
315
316 Message::Missing(_) => {
318 }
320
321 Message::Unconsumed(ix) => {
323 let item = &args.items[ix];
324 doc.token(Token::BlockStart(Block::TermRef));
325 doc.write(item, Style::Invalid);
326 doc.token(Token::BlockEnd(Block::TermRef));
327 doc.text(" is not expected in this context");
328 }
329
330 Message::NoEnv(name) => {
332 doc.text("environment variable ");
333 doc.token(Token::BlockStart(Block::TermRef));
334 doc.invalid(name);
335 doc.token(Token::BlockEnd(Block::TermRef));
336 doc.text(" is not set");
337 }
338
339 Message::StrictPos(_ix, metavar) => {
341 doc.text("expected ");
342 doc.token(Token::BlockStart(Block::TermRef));
343 doc.metavar(metavar);
344 doc.token(Token::BlockEnd(Block::TermRef));
345 doc.text(" to be on the right side of ");
346 doc.token(Token::BlockStart(Block::TermRef));
347 doc.literal("--");
348 doc.token(Token::BlockEnd(Block::TermRef));
349 }
350
351 Message::NonStrictPos(_ix, metavar) => {
353 doc.text("expected ");
354 doc.token(Token::BlockStart(Block::TermRef));
355 doc.metavar(metavar);
356 doc.token(Token::BlockEnd(Block::TermRef));
357 doc.text(" to be on the left side of ");
358 doc.token(Token::BlockStart(Block::TermRef));
359 doc.literal("--");
360 doc.token(Token::BlockEnd(Block::TermRef));
361 }
362
363 Message::ParseSome(s) | Message::ParseFail(s) => {
365 doc.text(s);
366 }
367
368 Message::ParseFailed(mix, s) => {
370 doc.text("couldn't parse");
371 if let Some(field) = textual_part(args, mix) {
372 doc.text(" ");
373 doc.token(Token::BlockStart(Block::TermRef));
374 if field.is_empty() {
375 let mut s = String::new();
376 if let Some(ix) = mix {
377 if ix > 0 {
378 if let Some(arg) = args.items.get(ix - 1) {
379 use std::fmt::Write as _;
380 match arg {
381 Arg::Short(n, true, _) => _ = write!(&mut s, "-{n}="),
382 Arg::Short(n, false, _) => _ = write!(&mut s, "-{n} "),
383 Arg::Long(l, false, _) => _ = write!(&mut s, "--{l} "),
384 Arg::Long(l, true, _) => _ = write!(&mut s, "--{l}="),
385 _ => {}
386 }
387 }
388 }
389 }
390 s.push_str(r#""""#);
391 doc.invalid(&s);
392 } else {
393 doc.invalid(&field);
394 }
395 doc.token(Token::BlockEnd(Block::TermRef));
396 }
397 doc.text(": ");
398 doc.text(&s);
399 }
400
401 Message::GuardFailed(mix, s) => {
403 if let Some(field) = textual_part(args, mix) {
404 doc.token(Token::BlockStart(Block::TermRef));
405 doc.invalid(&field);
406 doc.token(Token::BlockEnd(Block::TermRef));
407 doc.text(": ");
408 } else {
409 doc.text("check failed: ");
410 }
411 doc.text(s);
412 }
413
414 Message::NoArgument(x, mv) => match args.get(x + 1) {
417 Some(Arg::Short(_, _, os) | Arg::Long(_, _, os)) => {
418 let arg = &args.items[x];
419 let os = &os.to_string_lossy();
420
421 doc.token(Token::BlockStart(Block::TermRef));
422 doc.write(arg, Style::Literal);
423 doc.token(Token::BlockEnd(Block::TermRef));
424 doc.text(" requires an argument ");
425 doc.token(Token::BlockStart(Block::TermRef));
426 doc.metavar(mv);
427 doc.token(Token::BlockEnd(Block::TermRef));
428 doc.text(", got a flag ");
429 doc.token(Token::BlockStart(Block::TermRef));
430 doc.write(os, Style::Invalid);
431 doc.token(Token::BlockEnd(Block::TermRef));
432 doc.text(", try ");
433 doc.token(Token::BlockStart(Block::TermRef));
434 doc.write(arg, Style::Literal);
435 doc.literal("=");
436 doc.write(os, Style::Literal);
437 doc.token(Token::BlockEnd(Block::TermRef));
438 doc.text(" to use it as an argument");
439 }
440 Some(Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_)) | None => {
442 let arg = &args.items[x];
443 doc.token(Token::BlockStart(Block::TermRef));
444 doc.write(arg, Style::Literal);
445 doc.token(Token::BlockEnd(Block::TermRef));
446 doc.text(" requires an argument ");
447 doc.token(Token::BlockStart(Block::TermRef));
448 doc.metavar(mv);
449 doc.token(Token::BlockEnd(Block::TermRef));
450 }
451 },
452 Message::PureFailed(s) => {
454 doc.text(&s);
455 }
456 Message::Ambiguity(ix, name) => {
459 let mut chars = name.chars();
460 let first = chars.next().unwrap();
461 let rest = chars.as_str();
462 let second = chars.next().unwrap();
463 let s = args.items[ix].os_str().to_str().unwrap();
464
465 if let Some(name) = args.path.first() {
466 doc.literal(name);
467 doc.text(" supports ");
468 } else {
469 doc.text("app supports ");
470 }
471
472 doc.token(Token::BlockStart(Block::TermRef));
473 doc.literal("-");
474 doc.write_char(first, Style::Literal);
475 doc.token(Token::BlockEnd(Block::TermRef));
476 doc.text(" as both an option and an option-argument, try to split ");
477 doc.token(Token::BlockStart(Block::TermRef));
478 doc.write(s, Style::Literal);
479 doc.token(Token::BlockEnd(Block::TermRef));
480 doc.text(" into individual options (");
481 doc.literal("-");
482 doc.write_char(first, Style::Literal);
483 doc.literal(" -");
484 doc.write_char(second, Style::Literal);
485 doc.literal(" ..");
486 doc.text(") or use ");
487 doc.token(Token::BlockStart(Block::TermRef));
488 doc.literal("-");
489 doc.write_char(first, Style::Literal);
490 doc.literal("=");
491 doc.literal(rest);
492 doc.token(Token::BlockEnd(Block::TermRef));
493 doc.text(" syntax to disambiguate");
494 }
495 Message::Suggestion(ix, suggestion) => {
497 let actual = &args.items[ix].to_string();
498 match suggestion {
499 Suggestion::Variant(v) => {
500 let ty = match &args.items[ix] {
501 _ if actual.starts_with('-') => "flag",
502 Arg::Short(_, _, _) | Arg::Long(_, _, _) => "flag",
503 Arg::ArgWord(_) => "argument value",
504 Arg::Word(_) | Arg::PosWord(_) => "command or positional",
505 };
506
507 doc.text("no such ");
508 doc.text(ty);
509 doc.text(": ");
510 doc.token(Token::BlockStart(Block::TermRef));
511 doc.invalid(actual);
512 doc.token(Token::BlockEnd(Block::TermRef));
513 doc.text(", did you mean ");
514 doc.token(Token::BlockStart(Block::TermRef));
515
516 match v {
517 Variant::CommandLong(name) => doc.literal(name),
518 Variant::Flag(ShortLong::Long(l) | ShortLong::Both(_, l)) => {
519 doc.literal("--");
520 doc.literal(l);
521 }
522 Variant::Flag(ShortLong::Short(s)) => {
523 doc.literal("-");
524 doc.write_char(s, Style::Literal);
525 }
526 };
527
528 doc.token(Token::BlockEnd(Block::TermRef));
529 doc.text("?");
530 }
531 Suggestion::MissingDash(name) => {
532 doc.text("no such flag: ");
533 doc.token(Token::BlockStart(Block::TermRef));
534 doc.literal("-");
535 doc.literal(name);
536 doc.token(Token::BlockEnd(Block::TermRef));
537 doc.text(" (with one dash), did you mean ");
538 doc.token(Token::BlockStart(Block::TermRef));
539 doc.literal("--");
540 doc.literal(name);
541 doc.token(Token::BlockEnd(Block::TermRef));
542 doc.text("?");
543 }
544 Suggestion::ExtraDash(name) => {
545 doc.text("no such flag: ");
546 doc.token(Token::BlockStart(Block::TermRef));
547 doc.literal("--");
548 doc.write_char(name, Style::Literal);
549 doc.token(Token::BlockEnd(Block::TermRef));
550 doc.text(" (with two dashes), did you mean ");
551 doc.token(Token::BlockStart(Block::TermRef));
552 doc.literal("-");
553 doc.write_char(name, Style::Literal);
554 doc.token(Token::BlockEnd(Block::TermRef));
555 doc.text("?");
556 }
557 Suggestion::Nested(x, v) => {
558 let ty = match v {
559 Variant::CommandLong(_) => "subcommand",
560 Variant::Flag(_) => "flag",
561 };
562 doc.text(ty);
563 doc.text(" ");
564 doc.token(Token::BlockStart(Block::TermRef));
565 doc.literal(actual);
566 doc.token(Token::BlockEnd(Block::TermRef));
567 doc.text(
568 " is not valid in this context, did you mean to pass it to command ",
569 );
570 doc.token(Token::BlockStart(Block::TermRef));
571 doc.literal(&x);
572 doc.token(Token::BlockEnd(Block::TermRef));
573 doc.text("?");
574 }
575 }
576 }
577 Message::Expected(exp, actual) => {
579 doc.text("expected ");
580 match exp.len() {
581 0 => {
582 doc.text("no arguments");
583 }
584 1 => {
585 doc.token(Token::BlockStart(Block::TermRef));
586 doc.write_item(&exp[0]);
587 doc.token(Token::BlockEnd(Block::TermRef));
588 }
589 2 => {
590 doc.token(Token::BlockStart(Block::TermRef));
591 doc.write_item(&exp[0]);
592 doc.token(Token::BlockEnd(Block::TermRef));
593 doc.text(" or ");
594 doc.token(Token::BlockStart(Block::TermRef));
595 doc.write_item(&exp[1]);
596 doc.token(Token::BlockEnd(Block::TermRef));
597 }
598 _ => {
599 doc.token(Token::BlockStart(Block::TermRef));
600 doc.write_item(&exp[0]);
601 doc.token(Token::BlockEnd(Block::TermRef));
602 doc.text(", ");
603 doc.token(Token::BlockStart(Block::TermRef));
604 doc.write_item(&exp[1]);
605 doc.token(Token::BlockEnd(Block::TermRef));
606 doc.text(", or more");
607 }
608 }
609 match actual {
610 Some(actual) => {
611 doc.text(", got ");
612 doc.token(Token::BlockStart(Block::TermRef));
613 doc.write(&args.items[actual], Style::Invalid);
614 doc.token(Token::BlockEnd(Block::TermRef));
615 doc.text(". Pass ");
616 }
617 None => {
618 doc.text(", pass ");
619 }
620 }
621 doc.token(Token::BlockStart(Block::TermRef));
622 doc.literal("--help");
623 doc.token(Token::BlockEnd(Block::TermRef));
624 doc.text(" for usage information");
625 }
626
627 Message::Conflict(winner, loser) => {
629 doc.token(Token::BlockStart(Block::TermRef));
630 doc.write(&args.items[loser], Style::Literal);
631 doc.token(Token::BlockEnd(Block::TermRef));
632 doc.text(" cannot be used at the same time as ");
633 doc.token(Token::BlockStart(Block::TermRef));
634 doc.write(&args.items[winner], Style::Literal);
635 doc.token(Token::BlockEnd(Block::TermRef));
636 }
637
638 Message::OnlyOnce(_winner, loser) => {
640 doc.text("argument ");
641 doc.token(Token::BlockStart(Block::TermRef));
642 doc.write(&args.items[loser], Style::Literal);
643 doc.token(Token::BlockEnd(Block::TermRef));
644 doc.text(" cannot be used multiple times in this context");
645 }
646 };
647
648 ParseFailure::Stderr(doc)
649 }
650}
651
652pub(crate) fn summarize_missing(items: &[MissingItem], inner: &Meta, args: &State) -> Message {
654 let best_item = match items
656 .iter()
657 .max_by_key(|item| (item.position, item.scope.start))
658 {
659 Some(x) => x,
660 None => return Message::ParseSome("parser requires an extra flag, argument or parameter, but its name is hidden by the author"),
661 };
662
663 let mut best_scope = best_item.scope.clone();
664
665 let mut saw_command = false;
666 let expected = items
667 .iter()
668 .filter_map(|i| {
669 let cmd = matches!(i.item, Item::Command { .. });
670 if i.scope == best_scope && !(saw_command && cmd) {
671 saw_command |= cmd;
672 Some(i.item.clone())
673 } else {
674 None
675 }
676 })
677 .collect::<Vec<_>>();
678
679 best_scope.start = best_scope.start.max(best_item.position);
680 let mut args = args.clone();
681 args.set_scope(best_scope);
682 if let Some((ix, _arg)) = args.items_iter().next() {
683 if let Some((ix, sugg)) = crate::meta_youmean::suggest(&args, inner) {
684 Message::Suggestion(ix, sugg)
685 } else {
686 Message::Expected(expected, Some(ix))
687 }
688 } else {
689 Message::Expected(expected, None)
690 }
691}
692
693