1use crate::{printer::YankStatus, Theme};
2use pico_args::Arguments;
3use std::path::{Path, PathBuf};
4
5#[allow(missing_docs)]
6#[derive(Debug)]
7pub enum Error {
8 PkgIdIsLocal,
9
10 FlagRequiresRemote {
11 provided_short: String,
12 provided_long: String,
13 },
14
15 NameRequired,
16
17 Exclusive {
18 bad: Vec<Vec<String>>,
19 },
20
21 ExclusiveWith {
22 bad: Vec<Vec<String>>,
23 provided_short: String,
24 provided_long: String,
25 },
26 Inclusive {
27 bad: Vec<Vec<String>>,
28 provided_short: String,
29 provided_long: String,
30 },
31
32 NoCrateName,
33 NoCrateOrPkgId,
34
35 TooManyCrates {
36 n: usize,
37 },
38
39 UnknownOption {
40 option: String,
41 allowed: Vec<&'static str>,
42 },
43}
44
45impl Error {
46 fn exclusive<I, A, S>(bad: I) -> Self
47 where
48 I: IntoIterator<Item = A>,
49 A: IntoIterator<Item = S>,
50 S: ToString,
51 {
52 Self::Exclusive {
53 bad: bad
54 .into_iter()
55 .map(|s| s.into_iter().map(|s| s.to_string()).collect())
56 .collect(),
57 }
58 }
59
60 fn inclusive_with<I, A, S>(bad: I, short: impl ToString, long: impl ToString) -> Self
61 where
62 I: IntoIterator<Item = A>,
63 A: IntoIterator<Item = S>,
64 S: ToString,
65 {
66 Self::Inclusive {
67 bad: bad
68 .into_iter()
69 .map(|s| s.into_iter().map(|s| s.to_string()).collect())
70 .collect(),
71 provided_short: short.to_string(),
72 provided_long: long.to_string(),
73 }
74 }
75
76 fn exclusive_with<I, A, S>(bad: I, short: impl ToString, long: impl ToString) -> Self
77 where
78 I: IntoIterator<Item = A>,
79 A: IntoIterator<Item = S>,
80 S: ToString,
81 {
82 Self::ExclusiveWith {
83 bad: bad
84 .into_iter()
85 .map(|s| s.into_iter().map(|s| s.to_string()).collect())
86 .collect(),
87 provided_short: short.to_string(),
88 provided_long: long.to_string(),
89 }
90 }
91}
92
93impl std::fmt::Display for Error {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::PkgIdIsLocal => {
97 write!(f, "pkgid must be a `name:semver` pair, not a local path.")?;
98 }
99
100 Self::FlagRequiresRemote {
101 provided_short,
102 provided_long,
103 } => {
104 write!(
105 f,
106 "flag [{}, {}] requires that the crate be remote",
107 provided_short, provided_long
108 )?;
109 }
110
111 Self::NameRequired => {
112 write!(f, "A package name must be supplied")?;
113 }
114
115 Self::Exclusive { bad } => {
116 let flags = join_iter(
117 bad.iter().map(|s| s.as_slice()),
118 |n| format!("`{}`", n),
119 |n| format!("`[{}]`", n.join(", ")),
120 " and ",
121 );
122 write!(f, "the flags {} cannot be used at the same time", flags)?;
123 }
124
125 Self::ExclusiveWith {
126 bad,
127 provided_short,
128 provided_long,
129 } => {
130 let flags = join_iter(
131 bad.iter().map(|s| s.as_slice()),
132 |n| format!("`{}`", n),
133 |n| format!("`[{}]`", n.join(", ")),
134 " or ",
135 );
136 write!(
137 f,
138 "`[{}, {}]` cannot be used with: {}",
139 provided_short, provided_long, flags
140 )?;
141 }
142
143 Self::Inclusive {
144 bad,
145 provided_short,
146 provided_long,
147 } => {
148 let flags = join_iter(
149 bad.iter().map(|s| s.as_slice()),
150 |n| format!("`{}`", n),
151 |n| format!("`[{}]`", n.join(", ")),
152 " or ",
153 );
154 write!(
155 f,
156 "`[{}, {}]` must be used with one of {}",
157 provided_short, provided_long, flags
158 )?;
159 }
160
161 Self::NoCrateName => {
162 write!(f, "a crate name must be provided")?;
163 }
164
165 Self::NoCrateOrPkgId => {
166 write!(f, "no crate name, or pkgid spec provided.")?;
167 }
168
169 Self::TooManyCrates { n } => {
170 write!(
171 f,
172 "too many crate names ({}) were provided. only 1 is allowed",
173 n
174 )?;
175 }
176
177 Self::UnknownOption { option, allowed } => {
178 let options =
179 allowed
180 .iter()
181 .map(|s| format!("'{}'", s))
182 .fold(String::new(), |mut a, c| {
183 if !a.is_empty() {
184 a.push_str(", ");
185 }
186 a.push_str(&c);
187 a
188 });
189
190 write!(
191 f,
192 "unknown option '{}'. only one of [{}] is allowed.",
193 option, options
194 )?;
195 }
196 };
197
198 Ok(())
199 }
200}
201
202impl std::error::Error for Error {}
203
204fn join_iter<'a, I, S, O, M, W>(iter: I, one: O, many: M, with: W) -> String
205where
206 S: AsRef<str> + 'a,
207 I: Iterator<Item = &'a [S]> + 'a,
208
209 O: Fn(&S) -> String,
210 M: Fn(&'a [S]) -> String,
211 W: std::fmt::Display,
212{
213 let sep = with.to_string();
214 iter.map(|s| match s {
215 [n] => one(n),
216 n => many(n),
217 })
218 .fold(String::new(), |mut a, c| {
219 if !a.is_empty() {
220 a.push_str(&sep);
221 }
222 a.push_str(&c);
223 a
224 })
225}
226
227#[derive(PartialEq)]
228enum Color {
229 Always,
230 Auto,
231 Never,
232}
233
234impl std::str::FromStr for Color {
235 type Err = Error;
236 fn from_str(input: &str) -> Result<Self, Self::Err> {
237 match &input.to_lowercase()[..] {
238 "always" => Ok(Self::Always),
239 "auto" => Ok(Self::Auto),
240 "never" => Ok(Self::Never),
241 option => Err(Error::UnknownOption {
242 option: option.to_string(),
243 allowed: vec!["always", "auto", "never"],
244 }),
245 }
246 }
247}
248
249#[derive(Debug)]
253pub enum PkgId {
254 Remote {
256 name: String,
258 semver: Option<String>,
260 }, Local(PathBuf),
263}
264
265impl PkgId {
266 pub fn name(&self) -> &str {
268 match &self {
269 Self::Remote { name, .. } => name.as_str(),
270 Self::Local(s) => s.to_str().unwrap(),
271 }
272 }
273
274 pub fn is_local(&self) -> bool {
276 matches!(self, Self::Local { .. })
277 }
278}
279
280impl std::fmt::Display for PkgId {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match &self {
283 Self::Remote { name, semver } => {
284 write!(f, "{}", name)?;
285 if let Some(ver) = &semver {
286 write!(f, ":{}", ver)?;
287 }
288 Ok(())
289 }
290 Self::Local(l) => write!(f, "{}", l.display()),
291 }
292 }
293}
294
295impl std::str::FromStr for PkgId {
296 type Err = Error;
297 fn from_str(input: &str) -> Result<Self, Self::Err> {
298 let path = Path::new(input);
299 if path.is_dir() || path.is_file() {
300 return Err(Error::PkgIdIsLocal);
301 }
302
303 let mut iter = input.splitn(2, ':');
304 let name = iter.next().ok_or_else(|| Error::NameRequired)?;
305 let semver = iter.next().map(ToString::to_string);
306
307 let path = Path::new(name);
308 if path.is_dir() || path.is_file() {
309 return Err(Error::PkgIdIsLocal);
310 }
311
312 Ok(Self::Remote {
313 name: name.to_string(),
314 semver,
315 })
316 }
317}
318
319#[derive(Debug)]
321pub struct Args {
322 pub verbose: bool,
324
325 pub show_private: bool,
327
328 pub list: bool,
330
331 pub no_features: bool,
333
334 pub show_deps: bool,
336
337 pub show_yanked: Option<YankStatus>,
339
340 pub name_only: bool,
342
343 pub pkgid: PkgId,
345
346 pub offline: bool,
348
349 pub theme: Theme,
351
352 pub local_only: bool,
354
355 pub json: bool,
357}
358
359impl Args {
360 fn try_parse_help(args: &mut Arguments) -> anyhow::Result<()> {
361 if args.contains(["-V", "--version"]) {
362 print_version()
363 }
364
365 if args
366 .subcommand()?
367 .as_deref()
368 .filter(|&s| format!("cargo-{}", s) == env!("CARGO_PKG_NAME"))
369 .is_none()
370 {
371 print_help(Help::Cargo)
372 }
373
374 match (args.contains("-h"), args.contains("--help")) {
375 (true, ..) => print_help(Help::Short),
376 (.., true) => print_help(Help::Long),
377 _ => {}
378 }
379
380 Ok(())
381 }
382
383 fn try_parse_cache(args: &mut Arguments) -> anyhow::Result<()> {
384 if args.contains("--print-cache-dir") {
385 println!("{}", crate::util::cache_dir()?.display());
386 std::process::exit(0);
387 }
388
389 if args.contains("--purge") {
390 let total = crate::Registry::from_local()?.purge_local_cache()?;
391 println!(
392 "purged {} crates from {}",
393 total,
394 crate::util::cache_dir()?.display()
395 );
396 std::process::exit(0)
397 }
398
399 Ok(())
400 }
401
402 fn try_parse_yank_status(args: &mut Arguments) -> anyhow::Result<Option<YankStatus>> {
403 args.opt_value_from_fn(["-y", "--show-yanked"], |s| match s {
404 "exclude" => Ok(YankStatus::Exclude),
405 "include" => Ok(YankStatus::Include),
406 "only" => Ok(YankStatus::Only),
407 s => Err(Error::UnknownOption {
408 option: s.to_string(),
409 allowed: vec!["exclude", "include", "only"],
410 }),
411 })
412 .map_err(Into::into)
413 }
414
415 fn try_parse_color(args: &mut Arguments) -> anyhow::Result<()> {
416 let color: Option<Color> = args.opt_value_from_str(["-c", "--color"])?;
417
418 let disable_colors = std::env::var("NO_COLOR").is_ok();
419 if disable_colors
420 || color == Some(Color::Never)
421 || cfg!(windows) && !yansi::Paint::enable_windows_ascii()
422 {
423 yansi::Paint::disable()
424 }
425
426 Ok(())
427 }
428
429 fn try_parse_theme(args: &mut Arguments) -> anyhow::Result<Theme> {
430 let theme_name: Option<String> = args.opt_value_from_str("--theme")?;
431 match theme_name.as_deref().map(Self::try_parse_theme_name) {
432 Some(Ok(theme)) => Ok(theme),
433 Some(err) => err,
434 None => Ok(Theme::default()),
435 }
436 }
437
438 fn try_parse_theme_name(theme_name: &str) -> anyhow::Result<Theme> {
439 Ok(match &*theme_name.to_lowercase() {
440 "colorful" => Theme::colorful(),
441 "basic" => Theme::basic(),
442 "palette" => Theme::palette(),
443 "none" => Theme::none(),
444 _ => anyhow::bail!("invalid theme name, available: [colorful, basic, palette, none]"),
445 })
446 }
447
448 fn verify_flags(this: Self) -> anyhow::Result<Self> {
449 let Self {
450 list,
451 no_features,
452 show_private,
453 show_deps,
454 name_only,
455 pkgid,
456 show_yanked,
457 ..
458 } = &this;
459
460 if *list {
475 let mut bad = vec![];
476 if *no_features {
477 bad.push(vec!["-n", "--no-features"]);
478 }
479 if *show_deps {
480 bad.push(vec!["-d", "--deps"]);
481 }
482 if *show_private {
483 bad.push(vec!["-r", "--restricted"]);
484 }
485 if pkgid.is_local() {
486 bad.push(vec!["<crate>"]);
487 }
488 if !bad.is_empty() {
489 anyhow::bail!(Error::exclusive_with(bad, "-l", "--list"))
490 }
491 }
492
493 if show_yanked.is_some() && pkgid.is_local() {
494 anyhow::bail!(Error::FlagRequiresRemote {
495 provided_short: "-y".into(),
496 provided_long: "--show-yanked".into(),
497 });
498 }
499
500 if *name_only {
501 let mut bad = vec![];
502 if *show_deps {
503 bad.push(vec!["-d", "--deps"]);
504 }
505
506 if !bad.is_empty() {
507 anyhow::bail!(Error::exclusive_with(bad, "-s", "--short"))
508 }
509 }
510
511 if *no_features && (!show_deps && !*name_only) {
512 anyhow::bail!(Error::inclusive_with(
513 vec![vec!["-d", "--deps"], vec!["-s", "--short"]],
514 "-n",
515 "--no-features"
516 ))
517 }
518
519 if !pkgid.is_local() && *show_private {
520 anyhow::bail!(Error::inclusive_with(
521 vec![vec!["--manifest-path", "or implicit <crate>"]],
522 "-r",
523 "--restricted"
524 ))
525 }
526
527 Ok(this)
528 }
529
530 pub fn parse() -> anyhow::Result<Self> {
532 let mut args = pico_args::Arguments::from_env();
533
534 Self::try_parse_help(&mut args)?;
535 Self::try_parse_cache(&mut args)?;
536 Self::try_parse_color(&mut args)?;
537
538 let show_yanked = Self::try_parse_yank_status(&mut args)?;
539
540 let list = args.contains(["-l", "--list"]);
541 let show_private = args.contains(["-r", "--restricted"]);
542 let name_only = args.contains(["-s", "--short"]);
543 let no_features = args.contains(["-n", "--no-features"]);
544 let show_deps = args.contains(["-d", "--deps"]);
545 let offline = args.contains(["-o", "--offline"]);
546 let verbose = args.contains(["-v", "--verbose"]);
547 let local_only = args.contains(["-t", "--this-crate"]);
548 let json = args.contains(["-j", "--json"]);
549
550 let mut theme = Self::try_parse_theme(&mut args)?;
551
552 if let Some(override_theme) = std::env::var("WHATFEATURES_THEME")
553 .ok()
554 .as_deref()
555 .map(Args::try_parse_theme_name)
556 {
557 theme = override_theme?
558 }
559
560 let manifest_path: Option<PathBuf> = args.opt_value_from_str("--manifest-path")?;
561 let mut pkgid: Option<PkgId> = args.opt_value_from_str(["-p", "--pkgid"])?;
562
563 if pkgid.is_some() && manifest_path.is_some() {
564 anyhow::bail!(Error::exclusive(vec![
567 vec!["-p", "--pkgid"],
568 vec!["--manifest-path"],
569 ]));
570 }
571
572 let mut crate_names = args
574 .finish()
575 .into_iter()
576 .map(|s| s.to_string_lossy().to_string())
577 .collect::<Vec<_>>();
578
579 match crate_names.len() {
580 0 if pkgid.is_some() => {}
581 0 if manifest_path.is_some() => {
582 pkgid.replace(PkgId::Local(manifest_path.unwrap()));
583 }
584 0 => anyhow::bail!(Error::NoCrateName),
585 n if n > 0 && pkgid.is_some() => anyhow::bail!(Error::exclusive(vec![
586 vec!["-p", "--pkgid"],
587 vec!["<crate>"]
588 ])),
589 1 => {
590 let name = crate_names.remove(0);
592 let p = match name.parse() {
593 Ok(pkgid) => pkgid,
594 Err(..) => PkgId::Local(PathBuf::from(name)),
595 };
596 pkgid.replace(p);
597 }
598 n => anyhow::bail!(Error::TooManyCrates { n }),
599 };
600
601 if pkgid.is_none() {
602 anyhow::bail!(Error::NoCrateOrPkgId)
603 }
604
605 Self::verify_flags(Self {
606 verbose,
607
608 list,
609 show_private,
610
611 no_features,
612 show_deps,
613 show_yanked,
614 name_only,
615
616 pkgid: pkgid.unwrap(),
617 local_only,
618
619 offline,
620
621 theme,
622
623 json,
624 })
625 }
626}
627
628pub enum Help {
629 Long,
630 Short,
631 Cargo,
632}
633
634fn print_help(help: Help) -> ! {
635 static CARGO_HELP: &str = "USAGE:
636 cargo <SUBCOMMAND>
637
638FLAGS:
639 -h, --help Prints help information
640 -V, --version Prints version information
641
642SUBCOMMANDS:
643 help Prints this message or the help of the given subcommand(s)
644 whatfeatures the `whatfeatures` command";
645
646 static SHORT_HELP: &str = r#"the `whatfeatures` command
647
648USAGE:
649 cargo whatfeatures [FLAGS] [OPTIONS] <crate>
650
651FLAGS:
652 -h, --help Prints help information
653 -V, --version Displays the program name and version
654 -d, --deps Display dependencies for the crate
655 -n, --no-features Disable listing the features for the crate
656 -r, --restricted When used on a local workspace, also included private packages
657 -t, --this-crate When used on a crate in a local workspace, don't traverse to the root
658 -l, --list List all versions for the crate
659 -s, --short Display only the name and latest version
660 -v, --verbose Print all leaf nodes and optional deps
661 -o, --offline Don't connect to the internet, limits the availities of this.
662 -j, --json Prints json rather than a human-readable format
663 --print-cache-dir Prints out the path to the cache directory
664 --purge Purges the local cache
665 --theme Use a different theme
666
667OPTIONS:
668 -c, --color <WHEN> Attempts to use colors when printing as text [default: auto]
669 -p, --pkgid <SPEC> A `pkgid` spec. e.g. cargo:1.43.0
670 --manifest-path <PATH> A path to the Cargo.toml you want to read, locally.
671 -y, --show-yanked <yanked> Shows any yanked versions when using `--list`. [default: exclude].
672
673ARGS:
674 <crate> The name of a remote crate to retrieve information for.
675 Or local path to a directory containing Cargo.toml, or Cargo.toml itself.
676 This is exclusive with -p, --pkgid and with --manifest-path.
677
678CONFIG:
679 WHATFEATURES_THEME [colorful, basic, palette, none]
680"#;
681
682 static LONG_HELP: &str = r#"the `whatfeatures` command
683
684 USAGE:
685 cargo whatfeatures [FLAGS] [OPTIONS] <crate>
686
687 FLAGS:
688 -h, --help
689 Prints help information
690
691 -V, --version
692 Displays the program name and version
693
694 -d, --deps
695 Display dependencies for the crate
696 This will list the required dependencies
697
698 -n, --no-features
699 Disable listing the features for the crate
700
701 -r, --restricted
702 When used on a local workspace, also included private packages
703
704 -t, --this-crate
705 When used on a crate in a local workspace, don't traverse to the root
706 Normally, if you're in a workspace member, it will traverse to the root
707 and list all sibling crates as well. This flag disabled that behavior
708
709 -l, --list
710 List all versions for the crate.
711 When using the `-y` option, yanked crates can be filtered.
712
713 -s, --short
714 Display only the name and latest version, such as foo = 0.1.2
715
716 -v, --verbose
717 When this is enabled, all 'implied' features will be listed.
718 Also, optional dependencies will be listed. Optional deps are technically features.
719
720 -o, --offline
721 Don't connect to the internet, limits the availities of this.
722 If the crate is in either cargo's local registry, or whatfeatures' cache
723 then this will work normally, otherwise it'll give you a nice error.
724
725 -j, --json
726 This outputs JSON rather than the human readable format
727
728 --print-cache-dir
729 Prints out the path to the cache directory
730
731 --purge
732 Purges the local cache. The command will automatically clean up after
733 itself if it sees the crate in the cargo local registry. If its not
734 in the cargo registry, it'll download the crate from crates.io and place
735 it in its cache. This flag causes that cache to become invalidated.
736
737 The cache is located at these locations:
738 * Linux: $XDG_CACHE_HOME/museun/whatfeatures
739 * Windows: %LOCALAPPDATA/museun/whatfeatures
740 * macOS: $HOME/Library/Caches/museun/whatfeatures
741
742 --theme [colorful, basic, palette, none]
743 use this provided theme
744
745 OPTIONS:
746 -c, --color [always, auto, never]
747 Attempts to use colors when printing as text [default: auto]
748 *NOTE* When NO_COLOR is set to any value, all colors will be disabled
749
750 -p, --pkgid <semver>
751 A specific version to lookup. e.g. foo:0.7.1
752 If this is not provided, then the latest crate is used.
753
754 --manifest-path <PATH>
755 A path to the Cargo.toml you want to read, locally.
756 This can be the root directory to the crate/workspace, or an explicit path to a Cargo.toml
757 Use this to read from a local crate, rather than a remote one.
758
759 -y, --show-yanked <exclude, include, only>
760 Shows any yanked versions when using `--list`. [default: exclude].
761 When 'exclude' is provided, only active releases versions will be listed
762 When 'include' is provided, the listing will include yanked versions along with active releases.
763 When 'only' is provided, only yanked versions will be listed
764
765 ARGS:
766 <crate> The name of the crate to retrieve information for.
767
768 If this is a path to a directory containing a Cargo.toml,
769 or the path to the Cargo.toml then it'll use that directory
770 as the crate to operate one
771
772 This is exclusive with -p, --pkgid and with --manifest-path.
773
774 CONFIG:
775 WHATFEATURES_THEME [colorful, basic, palette, none]
776 This allows you to override the --theme flag with an environmental variable
777"#;
778
779 println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
780
781 match help {
782 Help::Long => println!("{}", LONG_HELP),
783 Help::Short => println!("{}", SHORT_HELP),
784 Help::Cargo => println!("{}", CARGO_HELP),
785 }
786
787 std::process::exit(0)
788}
789
790fn print_version() -> ! {
791 println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
792 std::process::exit(0)
793}