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, conflicts_with = "quiet")]
99    pub verbose: bool,
100
101    /// Opposite of verbose, suppresses errors & warning in output
102    /// Conflicts with verbose, and requires the use of --continue-on-error
103    #[arg(
104        long,
105        short,
106        action,
107        conflicts_with = "verbose",
108        requires = "continue_on_error"
109    )]
110    pub quiet: bool,
111
112    /// Continue if errors in templates are encountered
113    #[arg(long, action)]
114    pub continue_on_error: bool,
115
116    /// Pass template values through a file. Values should be in the format `key=value`, one per
117    /// line
118    #[arg(long="values-file", value_parser, alias="template-values-file", value_name="FILE", help_heading = heading::OUTPUT_PARAMETERS)]
119    pub template_values_file: Option<String>,
120
121    /// If silent mode is set all variables will be extracted from the template_values_file. If a
122    /// value is missing the project generation will fail
123    #[arg(long, short, requires("name"), action)]
124    pub silent: bool,
125
126    /// Use specific configuration file. Defaults to $CARGO_HOME/cargo-generate or
127    /// $HOME/.cargo/cargo-generate
128    #[arg(short, long, value_parser)]
129    pub config: Option<PathBuf>,
130
131    /// Specify the VCS used to initialize the generated template.
132    #[arg(long, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
133    pub vcs: Option<Vcs>,
134
135    /// Populates template variable `crate_type` with value `"lib"`
136    #[arg(long, conflicts_with = "bin", action, help_heading = heading::OUTPUT_PARAMETERS)]
137    pub lib: bool,
138
139    /// Populates a template variable `crate_type` with value `"bin"`
140    #[arg(long, conflicts_with = "lib", action, help_heading = heading::OUTPUT_PARAMETERS)]
141    pub bin: bool,
142
143    /// Use a different ssh identity
144    #[arg(short = 'i', long = "identity", value_parser, value_name="IDENTITY", help_heading = heading::GIT_PARAMETERS)]
145    pub ssh_identity: Option<PathBuf>,
146
147    /// Use a different gitconfig file, if omitted the usual $HOME/.gitconfig will be used
148    #[arg(long = "gitconfig", value_parser, value_name="GITCONFIG_FILE", help_heading = heading::GIT_PARAMETERS)]
149    pub gitconfig: Option<PathBuf>,
150
151    /// Define a value for use during template expansion. E.g `--define foo=bar`
152    #[arg(long, short, number_of_values = 1, value_parser, help_heading = heading::OUTPUT_PARAMETERS)]
153    pub define: Vec<String>,
154
155    /// Generate the template directly into the current dir. No subfolder will be created and no vcs
156    /// is initialized.
157    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
158    pub init: bool,
159
160    /// Generate the template directly at the given path.
161    #[arg(long, value_parser, value_name="PATH", help_heading = heading::OUTPUT_PARAMETERS)]
162    pub destination: Option<PathBuf>,
163
164    /// Will enforce a fresh git init on the generated project
165    #[arg(long, action, help_heading = heading::OUTPUT_PARAMETERS)]
166    pub force_git_init: bool,
167
168    /// Allows running system commands without being prompted. Warning: Setting this flag will
169    /// enable the template to run arbitrary system commands without user confirmation. Use at your
170    /// own risk and be sure to review the template code beforehand.
171    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
172    pub allow_commands: bool,
173
174    /// Allow the template to overwrite existing files in the destination.
175    #[arg(short, long, action, help_heading = heading::OUTPUT_PARAMETERS)]
176    pub overwrite: bool,
177
178    /// Skip downloading git submodules (if there are any)
179    #[arg(long, action, help_heading = heading::GIT_PARAMETERS)]
180    pub skip_submodules: bool,
181
182    /// All args after "--" on the command line.
183    #[arg(skip)]
184    pub other_args: Option<Vec<String>>,
185}
186
187impl Default for GenerateArgs {
188    fn default() -> Self {
189        Self {
190            template_path: TemplatePath::default(),
191            list_favorites: false,
192            name: None,
193            force: false,
194            verbose: false,
195            quiet: false,
196            continue_on_error: false,
197            template_values_file: None,
198            silent: false,
199            config: None,
200            vcs: None,
201            lib: true,
202            bin: false,
203            ssh_identity: None,
204            gitconfig: None,
205            define: Vec::default(),
206            init: false,
207            destination: None,
208            force_git_init: false,
209            allow_commands: false,
210            overwrite: false,
211            skip_submodules: false,
212            other_args: None,
213        }
214    }
215}
216
217#[derive(Default, Debug, Clone, Args)]
218pub struct TemplatePath {
219    /// Auto attempt to use as either `--git` or `--favorite`. If either is specified explicitly,
220    /// use as subfolder.
221    #[arg(required_unless_present_any(&["SpecificPath"]))]
222    pub auto_path: Option<String>,
223
224    /// Specifies the subfolder within the template repository to be used as the actual template.
225    #[arg()]
226    pub subfolder: Option<String>,
227
228    /// Expand $CWD as a template, then run `cargo test` on the expansion (set
229    /// $CARGO_GENERATE_TEST_CMD to override test command).
230    /// implies --verbose
231    ///
232    /// Any arguments given after the `--test` argument, will be used as arguments for the test
233    /// command.
234    #[arg(long, action, group("SpecificPath"))]
235    pub test: bool,
236
237    /// Git repository to clone template from. Can be a URL (like
238    /// `https://github.com/rust-cli/cli-template`), a path (relative or absolute), or an
239    /// `owner/repo` abbreviated GitHub URL (like `rust-cli/cli-template`).
240    ///
241    /// Note that cargo generate will first attempt to interpret the `owner/repo` form as a
242    /// relative path and only try a GitHub URL if the local path doesn't exist.
243    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
244    pub git: Option<String>,
245
246    /// Branch to use when installing from git
247    #[arg(short, long, conflicts_with_all = ["revision", "tag"], help_heading = heading::GIT_PARAMETERS)]
248    pub branch: Option<String>,
249
250    /// Tag to use when installing from git
251    #[arg(short, long, conflicts_with_all = ["revision", "branch"], help_heading = heading::GIT_PARAMETERS)]
252    pub tag: Option<String>,
253
254    /// Git revision to use when installing from git (e.g. a commit hash)
255    #[arg(short, long, conflicts_with_all = ["tag", "branch"], alias = "rev", help_heading = heading::GIT_PARAMETERS)]
256    pub revision: Option<String>,
257
258    /// Local path to copy the template from. Can not be specified together with --git.
259    #[arg(short, long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
260    pub path: Option<String>,
261
262    /// Generate a favorite template as defined in the config. In case the favorite is undefined,
263    /// use in place of the `--git` option, otherwise specifies the subfolder
264    #[arg(long, group("SpecificPath"), help_heading = heading::TEMPLATE_SELECTION)]
265    pub favorite: Option<String>,
266}
267
268impl TemplatePath {
269    /// # Panics
270    /// Will panic if no path to a template has been set at all,
271    /// which is never if Clap is initialized properly.
272    pub fn any_path(&self) -> &str {
273        self.git
274            .as_ref()
275            .or(self.path.as_ref())
276            .or(self.favorite.as_ref())
277            .or(self.auto_path.as_ref())
278            .unwrap()
279    }
280
281    pub const fn git(&self) -> Option<&(impl AsRef<str> + '_)> {
282        self.git.as_ref()
283    }
284
285    pub const fn branch(&self) -> Option<&(impl AsRef<str> + '_)> {
286        self.branch.as_ref()
287    }
288
289    pub const fn tag(&self) -> Option<&(impl AsRef<str> + '_)> {
290        self.tag.as_ref()
291    }
292
293    pub const fn revision(&self) -> Option<&(impl AsRef<str> + '_)> {
294        self.revision.as_ref()
295    }
296
297    pub const fn path(&self) -> Option<&(impl AsRef<str> + '_)> {
298        self.path.as_ref()
299    }
300
301    pub const fn favorite(&self) -> Option<&(impl AsRef<str> + '_)> {
302        self.favorite.as_ref()
303    }
304
305    pub const fn auto_path(&self) -> Option<&(impl AsRef<str> + '_)> {
306        self.auto_path.as_ref()
307    }
308
309    pub const fn subfolder(&self) -> Option<&(impl AsRef<str> + '_)> {
310        if self.git.is_some() || self.path.is_some() || self.favorite.is_some() {
311            self.auto_path.as_ref()
312        } else {
313            self.subfolder.as_ref()
314        }
315    }
316}
317
318#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Deserialize)]
319pub enum Vcs {
320    None,
321    Git,
322}
323
324impl FromStr for Vcs {
325    type Err = anyhow::Error;
326
327    fn from_str(s: &str) -> Result<Self, Self::Err> {
328        match s.to_uppercase().as_str() {
329            "NONE" => Ok(Self::None),
330            "GIT" => Ok(Self::Git),
331            _ => Err(anyhow!("Must be one of 'git' or 'none'")),
332        }
333    }
334}
335
336impl Vcs {
337    pub fn initialize(&self, project_dir: &Path, branch: Option<&str>, force: bool) -> Result<()> {
338        match self {
339            Self::None => Ok(()),
340            Self::Git => git::init(project_dir, branch, force)
341                .map(|_| ())
342                .map_err(anyhow::Error::from),
343        }
344    }
345
346    pub const fn is_none(&self) -> bool {
347        matches!(self, Self::None)
348    }
349}
350
351#[cfg(test)]
352mod cli_tests {
353    use super::*;
354
355    #[test]
356    fn test_cli() {
357        use clap::CommandFactory;
358        Cli::command().debug_assert()
359    }
360}