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
111#[derive(Debug, Clone)]
113pub struct MissingItem {
114 pub(crate) item: Item,
116 pub(crate) position: usize,
118 pub(crate) scope: Range<usize>,
121}
122
123impl Message {
124 #[must_use]
125 pub(crate) fn combine_with(self, other: Self) -> Self {
126 #[allow(clippy::match_same_arms)]
127 match (self, other) {
128 (a @ Message::ParseFailure(_), _) => a,
130 (_, b @ Message::ParseFailure(_)) => b,
131
132 (Message::Missing(mut a), Message::Missing(mut b)) => {
134 a.append(&mut b);
135 Message::Missing(a)
136 }
137
138 (a, b) => {
140 if a.can_catch() {
141 b
142 } else {
143 a
144 }
145 }
146 }
147 }
148}
149
150#[derive(Clone, Debug)]
169pub enum ParseFailure {
170 Stdout(Doc, bool),
172 Completion(String),
175 Stderr(Doc),
177}
178
179impl ParseFailure {
180 #[allow(clippy::must_use_candidate)]
186 #[track_caller]
187 pub fn unwrap_stderr(self) -> String {
188 match self {
189 Self::Stderr(err) => err.monochrome(true),
190 Self::Completion(..) | Self::Stdout(..) => panic!("not an stderr: {:?}", self),
191 }
192 }
193
194 #[allow(clippy::must_use_candidate)]
200 #[track_caller]
201 pub fn unwrap_stdout(self) -> String {
202 match self {
203 Self::Stdout(err, full) => err.monochrome(full),
204 Self::Completion(s) => s,
205 Self::Stderr(..) => panic!("not an stdout: {:?}", self),
206 }
207 }
208
209 #[allow(clippy::must_use_candidate)]
211 pub fn exit_code(self) -> i32 {
212 match self {
213 Self::Stdout(..) | Self::Completion(..) => 0,
214 Self::Stderr(..) => 1,
215 }
216 }
217
218 #[doc(hidden)]
219 #[deprecated = "Please use ParseFailure::print_message, with two s"]
220 pub fn print_mesage(&self, max_width: usize) {
221 self.print_message(max_width)
222 }
223
224 pub fn print_message(&self, max_width: usize) {
226 let color = Color::default();
227 match self {
228 ParseFailure::Stdout(msg, full) => {
229 println!("{}", msg.render_console(*full, color, max_width));
230 }
231 ParseFailure::Completion(s) => {
232 print!("{}", s);
233 }
234 ParseFailure::Stderr(msg) => {
235 #[allow(unused_mut)]
236 let mut error;
237 #[cfg(not(feature = "color"))]
238 {
239 error = "Error: ";
240 }
241
242 #[cfg(feature = "color")]
243 {
244 error = String::new();
245 color.push_str(Style::Invalid, &mut error, "Error: ");
246 }
247
248 eprintln!("{}{}", error, msg.render_console(true, color, max_width));
249 }
250 }
251 }
252}
253
254fn check_conflicts(args: &State) -> Option<Message> {
255 let (loser, winner) = args.conflict()?;
256 Some(Message::Conflict(winner, loser))
257}
258
259fn textual_part(args: &State, ix: Option<usize>) -> Option<std::borrow::Cow<str>> {
260 match args.items.get(ix?)? {
261 Arg::Short(_, _, _) | Arg::Long(_, _, _) => None,
262 Arg::ArgWord(s) | Arg::Word(s) | Arg::PosWord(s) => Some(s.to_string_lossy()),
263 }
264}
265
266fn only_once(args: &State, cur: usize) -> Option<usize> {
267 if cur == 0 {
268 return None;
269 }
270 let mut iter = args.items[..cur].iter().rev();
271 let offset = match args.items.get(cur)? {
272 Arg::Short(s, _, _) => iter.position(|a| a.match_short(*s)),
273 Arg::Long(l, _, _) => iter.position(|a| a.match_long(l)),
274 Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_) => None,
275 };
276 Some(cur - offset? - 1)
277}
278
279impl Message {
280 #[allow(clippy::too_many_lines)] pub(crate) fn render(mut self, args: &State, meta: &Meta) -> ParseFailure {
282 match self {
284 Message::Unconsumed(ix) => {
285 if let Some(conflict) = check_conflicts(args) {
286 self = conflict;
287 } else if let Some(prev_ix) = only_once(args, ix) {
288 self = Message::OnlyOnce(prev_ix, ix);
289 } else if let Some((ix, suggestion)) = crate::meta_youmean::suggest(args, meta) {
290 self = Message::Suggestion(ix, suggestion);
291 }
292 }
293 Message::Missing(xs) => {
294 self = summarize_missing(&xs, meta, args);
295 }
296 _ => {}
297 }
298
299 let mut doc = Doc::default();
300 match self {
301 Message::ParseFailure(f) => return f,
303
304 Message::Missing(_) => {
306 }
308
309 Message::Unconsumed(ix) => {
311 let item = &args.items[ix];
312 doc.token(Token::BlockStart(Block::TermRef));
313 doc.write(item, Style::Invalid);
314 doc.token(Token::BlockEnd(Block::TermRef));
315 doc.text(" is not expected in this context");
316 }
317
318 Message::NoEnv(name) => {
320 doc.text("environment variable ");
321 doc.token(Token::BlockStart(Block::TermRef));
322 doc.invalid(name);
323 doc.token(Token::BlockEnd(Block::TermRef));
324 doc.text(" is not set");
325 }
326
327 Message::StrictPos(_ix, metavar) => {
329 doc.text("expected ");
330 doc.token(Token::BlockStart(Block::TermRef));
331 doc.metavar(metavar);
332 doc.token(Token::BlockEnd(Block::TermRef));
333 doc.text(" to be on the right side of ");
334 doc.token(Token::BlockStart(Block::TermRef));
335 doc.literal("--");
336 doc.token(Token::BlockEnd(Block::TermRef));
337 }
338
339 Message::NonStrictPos(_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 left side of ");
346 doc.token(Token::BlockStart(Block::TermRef));
347 doc.literal("--");
348 doc.token(Token::BlockEnd(Block::TermRef));
349 }
350
351 Message::ParseSome(s) | Message::ParseFail(s) => {
353 doc.text(s);
354 }
355
356 Message::ParseFailed(mix, s) => {
358 doc.text("couldn't parse");
359 if let Some(field) = textual_part(args, mix) {
360 doc.text(" ");
361 doc.token(Token::BlockStart(Block::TermRef));
362 doc.invalid(&field);
363 doc.token(Token::BlockEnd(Block::TermRef));
364 }
365 doc.text(": ");
366 doc.text(&s);
367 }
368
369 Message::GuardFailed(mix, s) => {
371 if let Some(field) = textual_part(args, mix) {
372 doc.token(Token::BlockStart(Block::TermRef));
373 doc.invalid(&field);
374 doc.token(Token::BlockEnd(Block::TermRef));
375 doc.text(": ");
376 } else {
377 doc.text("check failed: ");
378 }
379 doc.text(s);
380 }
381
382 Message::NoArgument(x, mv) => match args.get(x + 1) {
385 Some(Arg::Short(_, _, os) | Arg::Long(_, _, os)) => {
386 let arg = &args.items[x];
387 let os = &os.to_string_lossy();
388
389 doc.token(Token::BlockStart(Block::TermRef));
390 doc.write(arg, Style::Literal);
391 doc.token(Token::BlockEnd(Block::TermRef));
392 doc.text(" requires an argument ");
393 doc.token(Token::BlockStart(Block::TermRef));
394 doc.metavar(mv);
395 doc.token(Token::BlockEnd(Block::TermRef));
396 doc.text(", got a flag ");
397 doc.token(Token::BlockStart(Block::TermRef));
398 doc.write(os, Style::Invalid);
399 doc.token(Token::BlockEnd(Block::TermRef));
400 doc.text(", try ");
401 doc.token(Token::BlockStart(Block::TermRef));
402 doc.write(arg, Style::Literal);
403 doc.literal("=");
404 doc.write(os, Style::Literal);
405 doc.token(Token::BlockEnd(Block::TermRef));
406 doc.text(" to use it as an argument");
407 }
408 Some(Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_)) | None => {
410 let arg = &args.items[x];
411 doc.token(Token::BlockStart(Block::TermRef));
412 doc.write(arg, Style::Literal);
413 doc.token(Token::BlockEnd(Block::TermRef));
414 doc.text(" requires an argument ");
415 doc.token(Token::BlockStart(Block::TermRef));
416 doc.metavar(mv);
417 doc.token(Token::BlockEnd(Block::TermRef));
418 }
419 },
420 Message::PureFailed(s) => {
422 doc.text(&s);
423 }
424 Message::Ambiguity(ix, name) => {
427 let mut chars = name.chars();
428 let first = chars.next().unwrap();
429 let rest = chars.as_str();
430 let second = chars.next().unwrap();
431 let s = args.items[ix].os_str().to_str().unwrap();
432
433 if let Some(name) = args.path.first() {
434 doc.literal(name);
435 doc.text(" supports ");
436 } else {
437 doc.text("app supports ");
438 }
439
440 doc.token(Token::BlockStart(Block::TermRef));
441 doc.literal("-");
442 doc.write_char(first, Style::Literal);
443 doc.token(Token::BlockEnd(Block::TermRef));
444 doc.text(" as both an option and an option-argument, try to split ");
445 doc.token(Token::BlockStart(Block::TermRef));
446 doc.write(s, Style::Literal);
447 doc.token(Token::BlockEnd(Block::TermRef));
448 doc.text(" into individual options (");
449 doc.literal("-");
450 doc.write_char(first, Style::Literal);
451 doc.literal(" -");
452 doc.write_char(second, Style::Literal);
453 doc.literal(" ..");
454 doc.text(") or use ");
455 doc.token(Token::BlockStart(Block::TermRef));
456 doc.literal("-");
457 doc.write_char(first, Style::Literal);
458 doc.literal("=");
459 doc.literal(rest);
460 doc.token(Token::BlockEnd(Block::TermRef));
461 doc.text(" syntax to disambiguate");
462 }
463 Message::Suggestion(ix, suggestion) => {
465 let actual = &args.items[ix].to_string();
466 match suggestion {
467 Suggestion::Variant(v) => {
468 let ty = match &args.items[ix] {
469 _ if actual.starts_with('-') => "flag",
470 Arg::Short(_, _, _) | Arg::Long(_, _, _) => "flag",
471 Arg::ArgWord(_) => "argument value",
472 Arg::Word(_) | Arg::PosWord(_) => "command or positional",
473 };
474
475 doc.text("no such ");
476 doc.text(ty);
477 doc.text(": ");
478 doc.token(Token::BlockStart(Block::TermRef));
479 doc.invalid(actual);
480 doc.token(Token::BlockEnd(Block::TermRef));
481 doc.text(", did you mean ");
482 doc.token(Token::BlockStart(Block::TermRef));
483
484 match v {
485 Variant::CommandLong(name) => doc.literal(name),
486 Variant::Flag(ShortLong::Long(l) | ShortLong::Both(_, l)) => {
487 doc.literal("--");
488 doc.literal(l);
489 }
490 Variant::Flag(ShortLong::Short(s)) => {
491 doc.literal("-");
492 doc.write_char(s, Style::Literal);
493 }
494 };
495
496 doc.token(Token::BlockEnd(Block::TermRef));
497 doc.text("?");
498 }
499 Suggestion::MissingDash(name) => {
500 doc.text("no such flag: ");
501 doc.token(Token::BlockStart(Block::TermRef));
502 doc.literal("-");
503 doc.literal(name);
504 doc.token(Token::BlockEnd(Block::TermRef));
505 doc.text(" (with one dash), did you mean ");
506 doc.token(Token::BlockStart(Block::TermRef));
507 doc.literal("--");
508 doc.literal(name);
509 doc.token(Token::BlockEnd(Block::TermRef));
510 doc.text("?");
511 }
512 Suggestion::ExtraDash(name) => {
513 doc.text("no such flag: ");
514 doc.token(Token::BlockStart(Block::TermRef));
515 doc.literal("--");
516 doc.write_char(name, Style::Literal);
517 doc.token(Token::BlockEnd(Block::TermRef));
518 doc.text(" (with two dashes), did you mean ");
519 doc.token(Token::BlockStart(Block::TermRef));
520 doc.literal("-");
521 doc.write_char(name, Style::Literal);
522 doc.token(Token::BlockEnd(Block::TermRef));
523 doc.text("?");
524 }
525 Suggestion::Nested(x, v) => {
526 let ty = match v {
527 Variant::CommandLong(_) => "subcommand",
528 Variant::Flag(_) => "flag",
529 };
530 doc.text(ty);
531 doc.text(" ");
532 doc.token(Token::BlockStart(Block::TermRef));
533 doc.literal(actual);
534 doc.token(Token::BlockEnd(Block::TermRef));
535 doc.text(
536 " is not valid in this context, did you mean to pass it to command ",
537 );
538 doc.token(Token::BlockStart(Block::TermRef));
539 doc.literal(&x);
540 doc.token(Token::BlockEnd(Block::TermRef));
541 doc.text("?");
542 }
543 }
544 }
545 Message::Expected(exp, actual) => {
547 doc.text("expected ");
548 match exp.len() {
549 0 => {
550 doc.text("no arguments");
551 }
552 1 => {
553 doc.token(Token::BlockStart(Block::TermRef));
554 doc.write_item(&exp[0]);
555 doc.token(Token::BlockEnd(Block::TermRef));
556 }
557 2 => {
558 doc.token(Token::BlockStart(Block::TermRef));
559 doc.write_item(&exp[0]);
560 doc.token(Token::BlockEnd(Block::TermRef));
561 doc.text(" or ");
562 doc.token(Token::BlockStart(Block::TermRef));
563 doc.write_item(&exp[1]);
564 doc.token(Token::BlockEnd(Block::TermRef));
565 }
566 _ => {
567 doc.token(Token::BlockStart(Block::TermRef));
568 doc.write_item(&exp[0]);
569 doc.token(Token::BlockEnd(Block::TermRef));
570 doc.text(", ");
571 doc.token(Token::BlockStart(Block::TermRef));
572 doc.write_item(&exp[1]);
573 doc.token(Token::BlockEnd(Block::TermRef));
574 doc.text(", or more");
575 }
576 }
577 match actual {
578 Some(actual) => {
579 doc.text(", got ");
580 doc.token(Token::BlockStart(Block::TermRef));
581 doc.write(&args.items[actual], Style::Invalid);
582 doc.token(Token::BlockEnd(Block::TermRef));
583 doc.text(". Pass ");
584 }
585 None => {
586 doc.text(", pass ");
587 }
588 }
589 doc.token(Token::BlockStart(Block::TermRef));
590 doc.literal("--help");
591 doc.token(Token::BlockEnd(Block::TermRef));
592 doc.text(" for usage information");
593 }
594
595 Message::Conflict(winner, loser) => {
597 doc.token(Token::BlockStart(Block::TermRef));
598 doc.write(&args.items[loser], Style::Literal);
599 doc.token(Token::BlockEnd(Block::TermRef));
600 doc.text(" cannot be used at the same time as ");
601 doc.token(Token::BlockStart(Block::TermRef));
602 doc.write(&args.items[winner], Style::Literal);
603 doc.token(Token::BlockEnd(Block::TermRef));
604 }
605
606 Message::OnlyOnce(_winner, loser) => {
608 doc.text("argument ");
609 doc.token(Token::BlockStart(Block::TermRef));
610 doc.write(&args.items[loser], Style::Literal);
611 doc.token(Token::BlockEnd(Block::TermRef));
612 doc.text(" cannot be used multiple times in this context");
613 }
614 };
615
616 ParseFailure::Stderr(doc)
617 }
618}
619
620pub(crate) fn summarize_missing(items: &[MissingItem], inner: &Meta, args: &State) -> Message {
622 let best_item = match items
624 .iter()
625 .max_by_key(|item| (item.position, item.scope.start))
626 {
627 Some(x) => x,
628 None => return Message::ParseSome("parser requires an extra flag, argument or parameter, but its name is hidden by the author"),
629 };
630
631 let mut best_scope = best_item.scope.clone();
632
633 let mut saw_command = false;
634 let expected = items
635 .iter()
636 .filter_map(|i| {
637 let cmd = matches!(i.item, Item::Command { .. });
638 if i.scope == best_scope && !(saw_command && cmd) {
639 saw_command |= cmd;
640 Some(i.item.clone())
641 } else {
642 None
643 }
644 })
645 .collect::<Vec<_>>();
646
647 best_scope.start = best_scope.start.max(best_item.position);
648 let mut args = args.clone();
649 args.set_scope(best_scope);
650 if let Some((ix, _arg)) = args.items_iter().next() {
651 if let Some((ix, sugg)) = crate::meta_youmean::suggest(&args, inner) {
652 Message::Suggestion(ix, sugg)
653 } else {
654 Message::Expected(expected, Some(ix))
655 }
656 } else {
657 Message::Expected(expected, None)
658 }
659}
660
661