cli_xtask/
args.rs

1//! Data structures for command line arguments parsing.
2
3use std::{env, iter};
4
5use cargo_metadata::{camino::Utf8PathBuf, Metadata, Package};
6use clap::ArgAction;
7use eyre::eyre;
8use tracing::Level;
9
10use crate::{
11    workspace::{self, FeatureOption, MetadataExt, PackageExt},
12    Result,
13};
14
15/// Commmand line arguments to control log verbosity level.
16///
17/// # Examples
18///
19/// To get `--quiet` (`-q`) and `--verbose` (or `-v`) flags through your entire
20/// program, just `flattern` this struct:
21///
22/// ```rust
23/// use cli_xtask::{args::Verbosity, clap};
24///
25/// #[derive(Debug, clap::Parser)]
26/// struct App {
27///     #[clap(flatten)]
28///     verbosity: Verbosity,
29/// }
30/// ```
31///
32/// The [`LogLevel`](crate::tracing::Level) values returned by
33/// [`Verbosity::get()`](crate::args::Verbosity::get) are:
34///
35/// * `None`: `-qqq`
36/// * `Some(Level::ERROR)`: `-qq`
37/// * `Some(Level::WARN)`: `-q`
38/// * `Some(Level::INFO)`: no arguments
39/// * `Some(Level::DEBUG)`: `-v`
40/// * `Some(Level::TRACE)`: `-vv`
41#[derive(Debug, Clone, Default, clap::Args)]
42pub struct Verbosity {
43    /// More output per occurrence
44    #[clap(long, short = 'v', action = ArgAction::Count, global = true)]
45    verbose: u8,
46    /// Less output per occurrence
47    #[clap(
48        long,
49        short = 'q',
50        action = ArgAction::Count,
51        global = true,
52        conflicts_with = "verbose"
53    )]
54    quiet: u8,
55}
56
57impl Verbosity {
58    /// Returns the log verbosity level.
59    pub fn get(&self) -> Option<Level> {
60        let level = i8::try_from(self.verbose).unwrap_or(i8::MAX)
61            - i8::try_from(self.quiet).unwrap_or(i8::MAX);
62        match level {
63            i8::MIN..=-3 => None,
64            -2 => Some(Level::ERROR),
65            -1 => Some(Level::WARN),
66            0 => Some(Level::INFO),
67            1 => Some(Level::DEBUG),
68            2..=i8::MAX => Some(Level::TRACE),
69        }
70    }
71}
72
73/// Command line arguments to specify the environment variables to set for the
74/// subcommand.
75#[derive(Debug, Clone, Default, clap::Args)]
76pub struct EnvArgs {
77    /// Environment variables to set for the subcommand.
78    #[clap(
79        long,
80        short = 'e',
81        value_name = "KEY>=<VALUE", // hack
82        value_parser = EnvArgs::parse_parts,
83    )]
84    pub env: Vec<(String, String)>,
85}
86
87impl EnvArgs {
88    /// Creates a new `EnvArgs` from an iterator of `(key, value)` pairs.
89    pub fn new(iter: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
90        Self {
91            env: iter
92                .into_iter()
93                .map(|(k, v)| (k.into(), v.into()))
94                .collect(),
95        }
96    }
97
98    fn parse_parts(s: &str) -> Result<(String, String)> {
99        match s.split_once('=') {
100            Some((key, value)) => Ok((key.into(), value.into())),
101            None => Ok((s.into(), "".into())),
102        }
103    }
104}
105
106/// Command line arguments to specify the workspaces where the subcommand runs
107/// on.
108#[derive(Debug, Clone, Default, clap::Args)]
109#[non_exhaustive]
110pub struct WorkspaceArgs {
111    /// Same as `--all-workspaces --workspace --each-feature`.
112    #[clap(long)]
113    pub exhaustive: bool,
114    /// Run the subcommand on all workspaces.
115    #[clap(long, conflicts_with = "exhaustive")]
116    pub all_workspaces: bool,
117    /// Run the subcommand on each workspace other than the current workspace.
118    #[clap(long)]
119    pub exclude_current_workspace: bool,
120}
121
122impl WorkspaceArgs {
123    /// `WorkspaceArgs` value with `--exhaustive` flag enabled.
124    pub const EXHAUSTIVE: Self = Self {
125        exhaustive: true,
126        all_workspaces: false,
127        exclude_current_workspace: false,
128    };
129
130    /// Returns the workspaces to run the subcommand on.
131    pub fn workspaces(&self) -> impl Iterator<Item = &'static Metadata> {
132        let workspaces = if self.exhaustive || self.all_workspaces {
133            if self.exclude_current_workspace {
134                &workspace::all()[1..]
135            } else {
136                workspace::all()
137            }
138        } else if self.exclude_current_workspace {
139            &workspace::all()[..0]
140        } else {
141            &workspace::all()[..1]
142        };
143        workspaces.iter()
144    }
145}
146
147/// Command line arguments to specify the packages to run the subcommand for.
148#[derive(Debug, Clone, Default, clap::Args)]
149#[non_exhaustive]
150pub struct PackageArgs {
151    /// Command line arguments to specify the workspaces where the subcommand
152    /// runs on.
153    #[clap(flatten)]
154    pub workspace_args: WorkspaceArgs,
155    /// Run the subcommand for all packages in the workspace
156    #[clap(long, conflicts_with = "exhaustive")]
157    pub workspace: bool,
158    /// Package name to run the subcommand for
159    #[clap(long = "package", short = 'p', conflicts_with = "exhaustive")]
160    pub package: Option<String>,
161}
162
163impl PackageArgs {
164    /// `PackageArgs` value with `--exhaustive` flag enabled.
165    pub const EXHAUSTIVE: Self = Self {
166        workspace_args: WorkspaceArgs::EXHAUSTIVE,
167        workspace: false,
168        package: None,
169    };
170
171    /// Returns the packages to run the subcommand on.
172    pub fn packages(
173        &self,
174    ) -> impl Iterator<Item = Result<(&'static Metadata, &'static Package)>> + '_ {
175        self.workspace_args
176            .workspaces()
177            .map(move |workspace| {
178                let packages = if self.workspace_args.exhaustive || self.workspace {
179                    workspace.workspace_packages()
180                } else if let Some(name) = &self.package {
181                    let pkg = workspace
182                        .workspace_package_by_name(name)
183                        .ok_or_else(|| eyre!("Package not found"))?;
184                    vec![pkg]
185                } else {
186                    let current_dir = Utf8PathBuf::try_from(env::current_dir()?)?;
187                    if let Some(pkg) = workspace.workspace_package_by_path(&current_dir) {
188                        vec![pkg]
189                    } else {
190                        workspace.workspace_default_packages()
191                    }
192                };
193                let it = packages
194                    .into_iter()
195                    .map(move |package| (workspace, package));
196                Ok(it)
197            })
198            .flat_map(|res| -> Box<dyn Iterator<Item = _>> {
199                match res {
200                    Ok(it) => Box::new(it.map(Ok)),
201                    Err(err) => Box::new(iter::once(Err(err))),
202                }
203            })
204    }
205}
206
207/// Command line arguments to specify the features to run the subcommand with.
208#[derive(Debug, Clone, Default, clap::Args)]
209#[non_exhaustive]
210pub struct FeatureArgs {
211    /// Command line arguments to specify the packages to run the subcommand
212    /// for.
213    #[clap(flatten)]
214    pub package_args: PackageArgs,
215    /// Run the subcommand with each feature enabled
216    #[clap(long, conflicts_with = "exhaustive")]
217    pub each_feature: bool,
218}
219
220impl FeatureArgs {
221    /// `FeatureArgs` value with `--exhaustive` flag enabled.
222    pub const EXHAUSTIVE: Self = Self {
223        package_args: PackageArgs::EXHAUSTIVE,
224        each_feature: false,
225    };
226
227    /// Returns the features to run the subcommand with.
228    pub fn features(
229        &self,
230    ) -> impl Iterator<
231        Item = Result<(
232            &'static Metadata,
233            &'static Package,
234            Option<FeatureOption<'static>>,
235        )>,
236    > + '_ {
237        self.package_args
238            .packages()
239            .map(move |res| {
240                res.map(move |(workspace, package)| -> Box<dyn Iterator<Item = _>> {
241                    let exhaustive = self.package_args.workspace_args.exhaustive;
242                    if (exhaustive || self.each_feature) && !package.features.is_empty() {
243                        Box::new(
244                            package
245                                .each_feature()
246                                .map(move |feature| (workspace, package, Some(feature))),
247                        )
248                    } else {
249                        Box::new(iter::once((workspace, package, None)))
250                    }
251                })
252            })
253            .flat_map(|res| -> Box<dyn Iterator<Item = _>> {
254                match res {
255                    Ok(it) => Box::new(it.map(Ok)),
256                    Err(err) => Box::new(iter::once(Err(err))),
257                }
258            })
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn verbosity() {
268        use clap::Parser;
269        #[derive(Debug, clap::Parser)]
270        struct App {
271            #[clap(flatten)]
272            verbosity: Verbosity,
273        }
274
275        let cases: &[(&[&str], Option<Level>)] = &[
276            (&["-qqqq"], None),
277            (&["-qqq"], None),
278            (&["-qq"], Some(Level::ERROR)),
279            (&["-q"], Some(Level::WARN)),
280            (&[], Some(Level::INFO)),
281            (&["-v"], Some(Level::DEBUG)),
282            (&["-vv"], Some(Level::TRACE)),
283        ];
284
285        for (arg, level) in cases {
286            let args = App::parse_from(["app"].into_iter().chain(arg.iter().copied()));
287            assert_eq!(args.verbosity.get(), *level, "arg: {}", arg.join(" "));
288        }
289    }
290}