cargo_generate/
args.rs

1use std::{
2    path::{Path, PathBuf},
3    str::FromStr,
4};
5
6use anyhow::{anyhow, Result};
7use clap::{Args, Parser};
8use serde::Deserialize;
9
10use crate::git;
11
12/// Styles from <https://github.com/rust-lang/cargo/blob/master/src/cargo/util/style.rs>
13mod style {
14    use anstyle::*;
15    use clap::builder::Styles;
16
17    const HEADER: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
18    const USAGE: Style = AnsiColor::Green.on_default().effects(Effects::BOLD);
19    const LITERAL: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
20    const PLACEHOLDER: Style = AnsiColor::Cyan.on_default();
21    const ERROR: Style = AnsiColor::Red.on_default().effects(Effects::BOLD);
22    const VALID: Style = AnsiColor::Cyan.on_default().effects(Effects::BOLD);
23    const INVALID: Style = AnsiColor::Yellow.on_default().effects(Effects::BOLD);
24
25    pub const STYLES: Styles = {
26        Styles::styled()
27            .header(HEADER)
28            .usage(USAGE)
29            .literal(LITERAL)
30            .placeholder(PLACEHOLDER)
31            .error(ERROR)
32            .valid(VALID)
33            .invalid(INVALID)
34            .error(ERROR)
35    };
36}
37
38mod heading {
39    pub const GIT_PARAMETERS: &str = "Git Parameters";
40    pub const TEMPLATE_SELECTION: &str = "Template Selection";
41    pub const OUTPUT_PARAMETERS: &str = "Output Parameters";
42}
43
44#[derive(Parser)]
45#[command(
46    name = "cargo generate",
47    bin_name = "cargo",
48    arg_required_else_help(true),
49    version,
50    about,
51    next_line_help(false),
52    styles(style::STYLES)
53)]
54pub enum Cli {
55    #[command(name = "generate", visible_alias = "gen")]
56    Generate(GenerateArgs),
57}
58
59#[derive(Clone, Debug, Args)]
60#[command(arg_required_else_help(true), version, about)]
61pub struct GenerateArgs {
62    #[command(flatten)]
63    pub template_path: TemplatePath,
64
65    /// List defined favorite templates from the config
66    #[arg(
67        long,
68        action,
69        group("SpecificPath"),
70        conflicts_with_all(&[
71            "git", "path", "subfolder", "branch",
72            "name",
73            "force",
74            "silent",
75            "vcs",
76            "lib",
77            "bin",
78            "define",
79            "init",
80            "template_values_file",
81            "ssh_identity",
82            "test",
83        ])
84    )]
85    pub list_favorites: bool,
86
87    /// Directory to create / project name; if the name isn't in kebab-case, it will be converted
88    /// to kebab-case unless `--force` is given.
89    #[arg(long, short, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
90    pub name: Option<String>,
91
92    /// Don't convert the project name to kebab-case before creating the directory. Note that cargo
93    /// generate won't overwrite an existing directory, even if `--force` is given.
94    #[arg(long, short, action, help_heading = heading::OUTPUT_PARAMETERS)]
95    pub force: bool,
96
97    /// Enables more verbose output.
98    #[arg(long, short, action)]
99    pub verbose: bool,
100
101    /// Pass template values through a file. Values should be in the format `key=value`, one per
102    /// line
103    #[arg(long="values-file", value_parser, alias="template-values-file", value_name="FILE", help_heading = heading::OUTPUT_PARAMETERS)]
104    pub template_values_file: Option<String>,
105
106    /// If silent mode is set all variables will be extracted from the template_values_file. If a
107    /// value is missing the project generation will fail
108    #[arg(long, short, requires("name"), action)]
109    pub silent: bool,
110
111    /// Use specific configuration file. Defaults to $CARGO_HOME/cargo-generate or
112    /// $HOME/.cargo/cargo-generate
113    #[arg(short, long, value_parser)]
114    pub config: Option<PathBuf>,
115
116    /// Specify the VCS used to initialize the generated template.
117    #[arg(long, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
118    pub vcs: Option<Vcs>,
119
120    /// Populates template variable `crate_type` with value `"lib"`
121    #[arg(long, conflicts_with = "bin", action, help_heading = heading::OUTPUT_PARAMETERS)]
122    pub lib: bool,
123
124    /// Populates a template variable `crate_type` with value `"bin"`
125    #[arg(long, conflicts_with = "lib", action, help_heading = heading::OUTPUT_PARAMETERS)]
126    pub bin: bool,
127
128    /// Use a different ssh identity
129    #[arg(short = 'i', long = "identity", value_parser, value_name="IDENTITY", help_heading = heading::GIT_PARAMETERS)]
130    pub ssh_identity: Option<PathBuf>,
131
132    /// Use a different gitconfig file, if omitted the usual $HOME/.gitconfig will be used
133    #[arg(long = "gitconfig", value_parser, value_name="GITCONFIG_FILE", help_heading = heading::GIT_PARAMETERS)]
134    pub gitconfig: Option<PathBuf>,
135
136    /// Define a value for use during template expansion. E.g `--define foo=bar`
137    #[arg(long, short, number_of_values = 1, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
138    pub define: Vec<String>,
139
140    /// Generate the template directly into the current dir. No subfolder will be created and no vcs
141    /// is initialized.
142    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
143    pub init: bool,
144
145    /// Generate the template directly at the given path.
146    #[arg(long, value_parser, value_name="PATH", help_heading = heading::OUTPUT_PARAMETERS)]
147    pub destination: Option<PathBuf>,
148
149    /// Will enforce a fresh git init on the generated project
150    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
151    pub force_git_init: bool,
152
153    /// Allows running system commands without being prompted. Warning: Setting this flag will
154    /// enable the template to run arbitrary system commands without user confirmation. Use at your
155    /// own risk and be sure to review the template code beforehand.
156    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
157    pub allow_commands: bool,
158
159    /// Allow the template to overwrite existing files in the destination.
160    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
161    pub overwrite: bool,
162
163    /// Skip downloading git submodules (if there are any)
164    #[arg(long, action, help_heading = heading::GIT_PARAMETERS)]
165    pub skip_submodules: bool,
166
167    /// All args after "--" on the command line.
168    #[arg(skip)]
169    pub other_args: Option<Vec<String>>,
170}
171
172impl Default for GenerateArgs {
173    fn default() -> Self {
174        Self {
175            template_path: TemplatePath::default(),
176            list_favorites: false,
177            name: None,
178            force: false,
179            verbose: false,
180            template_values_file: None,
181            silent: false,
182            config: None,
183            vcs: None,
184            lib: true,
185            bin: false,
186            ssh_identity: None,
187            gitconfig: None,
188            define: Vec::default(),
189            init: false,
190            destination: None,
191            force_git_init: false,
192            allow_commands: false,
193            overwrite: false,
194            skip_submodules: false,
195            other_args: None,
196        }
197    }
198}
199
200#[derive(Default, Debug, Clone, Args)]
201pub struct TemplatePath {
202    /// Auto attempt to use as either `--git` or `--favorite`. If either is specified explicitly,
203    /// use as subfolder.
204    #[arg(required_unless_present_any(&["SpecificPath"]))]
205    pub auto_path: Option<String>,
206
207    /// Specifies the subfolder within the template repository to be used as the actual template.
208    #[arg()]
209    pub subfolder: Option<String>,
210
211    /// Expand $CWD as a template, then run `cargo test` on the expansion (set
212    /// $CARGO_GENERATE_TEST_CMD to override test command).
213    ///
214    /// Any arguments given after the `--test` argument, will be used as arguments for the test
215    /// command.
216    #[arg(long, action, group("SpecificPath"))]
217    pub test: bool,
218
219    /// Git repository to clone template from. Can be a URL (like
220    /// `https://github.com/rust-cli/cli-template`), a path (relative or absolute), or an
221    /// `owner/repo` abbreviated GitHub URL (like `rust-cli/cli-template`).
222    ///
223    /// Note that cargo generate will first attempt to interpret the `owner/repo` form as a
224    /// relative path and only try a GitHub URL if the local path doesn't exist.
225    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
226    pub git: Option<String>,
227
228    /// Branch to use when installing from git
229    #[arg(short, long, conflicts_with_all = ["revision", "tag"], help_heading = heading::GIT_PARAMETERS)]
230    pub branch: Option<String>,
231
232    /// Tag to use when installing from git
233    #[arg(short, long, conflicts_with_all = ["revision", "branch"], help_heading = heading::GIT_PARAMETERS)]
234    pub tag: Option<String>,
235
236    /// Git revision to use when installing from git (e.g. a commit hash)
237    #[arg(short, long, conflicts_with_all = ["tag", "branch"], alias = "rev", help_heading = heading::GIT_PARAMETERS)]
238    pub revision: Option<String>,
239
240    /// Local path to copy the template from. Can not be specified together with --git.
241    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
242    pub path: Option<String>,
243
244    /// Generate a favorite template as defined in the config. In case the favorite is undefined,
245    /// use in place of the `--git` option, otherwise specifies the subfolder
246    #[arg(long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
247    pub favorite: Option<String>,
248}
249
250impl TemplatePath {
251    /// # Panics
252    /// Will panic if no path to a template has been set at all,
253    /// which is never if Clap is initialized properly.
254    pub fn any_path(&self) -> &str {
255        self.git
256            .as_ref()
257            .or(self.path.as_ref())
258            .or(self.favorite.as_ref())
259            .or(self.auto_path.as_ref())
260            .unwrap()
261    }
262
263    pub const fn git(&self) -> Option<&(impl AsRef<str> + '_)> {
264        self.git.as_ref()
265    }
266
267    pub const fn branch(&self) -> Option<&(impl AsRef<str> + '_)> {
268        self.branch.as_ref()
269    }
270
271    pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
272        self.tag.as_ref()
273    }
274
275    pub const fn revision(&self) -> Option<&(impl AsRef<str> + '_)> {
276        self.revision.as_ref()
277    }
278
279    pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
280        self.path.as_ref()
281    }
282
283    pub const fn favorite(&self) -> Option<&(impl AsRef<str> + '_)> {
284        self.favorite.as_ref()
285    }
286
287    pub const fn auto_path(&self) -> Option<&(impl AsRef<str> + '_)> {
288        self.auto_path.as_ref()
289    }
290
291    pub const fn subfolder(&self) -> Option<&(impl AsRef<str> + '_)> {
292        if self.git.is_some() || self.path.is_some() || self.favorite.is_some() {
293            self.auto_path.as_ref()
294        } else {
295            self.subfolder.as_ref()
296        }
297    }
298}
299
300#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Deserialize)]
301pub enum Vcs {
302    None,
303    Git,
304}
305
306impl FromStr for Vcs {
307    type Err = anyhow::Error;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        match s.to_uppercase().as_str() {
311            "NONE" => Ok(Self::None),
312            "GIT" => Ok(Self::Git),
313            _ => Err(anyhow!("Must be one of 'git' or 'none'")),
314        }
315    }
316}
317
318impl Vcs {
319    pub fn initialize(&self, project_dir: &Path, branch: Option<&str>, force: bool) -> Result<()> {
320        match self {
321            Self::None => Ok(()),
322            Self::Git => git::init(project_dir, branch, force)
323                .map(|_| ())
324                .map_err(anyhow::Error::from),
325        }
326    }
327
328    pub const fn is_none(&self) -> bool {
329        matches!(self, Self::None)
330    }
331}
332
333#[cfg(test)]
334mod cli_tests {
335    use super::*;
336
337    #[test]
338    fn test_cli() {
339        use clap::CommandFactory;
340        Cli::command().debug_assert()
341    }
342}