cargo_whatfeatures/
args.rs

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// TODO verify that 'local' contains a Cargo.toml
250// TODO verify that 'semver' is a correct semver
251/// A 'pkgid' spec, either local or 'remote'
252#[derive(Debug)]
253pub enum PkgId {
254    /// Remote path (e.g. look it up in the registry)
255    Remote {
256        /// Name of the crate
257        name: String,
258        /// Specified semver
259        semver: Option<String>,
260    }, // TODO supports more registries than just crates.io
261    /// Local directory or file
262    Local(PathBuf),
263}
264
265impl PkgId {
266    /// Name of the package
267    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    /// Whether this is a local package
275    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/// Input for the program
320#[derive(Debug)]
321pub struct Args {
322    /// Verbose output (list leaves, etc.)
323    pub verbose: bool,
324
325    /// Should we show private crates?
326    pub show_private: bool,
327
328    /// Should we list all versions?
329    pub list: bool,
330
331    /// Should no features be printed out?
332    pub no_features: bool,
333
334    /// Should we should the dependencies?
335    pub show_deps: bool,
336
337    /// Should we show yanked versions?
338    pub show_yanked: Option<YankStatus>,
339
340    /// Should we show only the name and version?
341    pub name_only: bool,
342
343    /// The pkgid specified
344    pub pkgid: PkgId,
345
346    /// Don't try to connect to the internet
347    pub offline: bool,
348
349    /// The theme to use
350    pub theme: Theme,
351
352    /// Don't treat this crate as a member of a workspace
353    pub local_only: bool,
354
355    /// Output json instead of human readable
356    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        /*
461        list is exclusive with:
462            no_features
463            show_deps
464            crate_name
465
466        name_only is exclusive with:
467            show_deps
468
469        no_features is inclusive with:
470            no_features
471            show_deps
472        */
473
474        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    /// Parse the arguments
531    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            // "both `[-p, --pkgid]` and `--manifest-path` cannot be used at the same time"
565            // TODO this could be done with like 3 less allocations
566            anyhow::bail!(Error::exclusive(vec![
567                vec!["-p", "--pkgid"],
568                vec!["--manifest-path"],
569            ]));
570        }
571
572        // TODO redo all of this
573        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                // TODO make this determine if its a remote or local package (prefer remote)
591                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}