cargo_hatch/
cli.rs

1use std::{
2    fs::OpenOptions,
3    io::{self, Write},
4};
5
6use anyhow::{ensure, Context, Result};
7use camino::{Utf8Path, Utf8PathBuf};
8use clap::{Args, CommandFactory, Parser, ValueHint};
9use clap_complete::Shell;
10
11#[derive(Parser)]
12#[command(name = "cargo", bin_name = "cargo")]
13enum Cli {
14    #[command(subcommand)]
15    Hatch(Command),
16}
17
18#[derive(Parser)]
19#[command(about, author, version)]
20pub enum Command {
21    /// Initialize a new template with a sample configuration.
22    Init {
23        /// Name of the new template, using the current working directory if omitted.
24        name: Option<String>,
25    },
26    /// List all configured bookmarks with name and description.
27    List,
28    /// Create a new project from configured bookmarks.
29    New {
30        /// Bookmark as defined in the global configuration.
31        bookmark: String,
32        #[command(flatten)]
33        flags: CreationFlags,
34    },
35    /// Create a new project from a template located in a remote Git repository.
36    Git {
37        /// An optional sub-folder within the repository that contains the template.
38        #[arg(long)]
39        folder: Option<Utf8PathBuf>,
40        /// HTTP or Git URL to the remote repository.
41        url: String,
42        #[command(flatten)]
43        flags: CreationFlags,
44    },
45    /// Create a new project from a template located in the local file system.
46    Local {
47        /// Location of the template directory.
48        #[arg(value_hint = ValueHint::DirPath)]
49        path: Utf8PathBuf,
50        #[command(flatten)]
51        flags: CreationFlags,
52    },
53    /// Generate auto-completion scripts for various shells.
54    Completions {
55        /// Shell to generate an auto-completion script for.
56        #[arg(value_enum)]
57        shell: Shell,
58    },
59    /// Generate man pages into the given directory.
60    Manpages {
61        /// Target directory, that must already exist and be empty. If the any file with the same
62        /// name as any of the man pages already exist, it'll not be overwritten, but instead an
63        /// error be returned.
64        #[arg(value_hint = ValueHint::DirPath)]
65        dir: Utf8PathBuf,
66    },
67}
68
69#[derive(Args)]
70pub struct CreationFlags {
71    /// Name of the new project, using the current working directory if omitted.
72    ///
73    /// If the name contains slashes `/`, it is treated as a file path and the target directory is
74    /// created automatically if missing. The final project name will become the last part of the
75    /// path.
76    pub name: Option<Utf8PathBuf>,
77    /// Update all dependencies to the latest compatible version after project creation.
78    #[arg(short, long)]
79    pub update_deps: bool,
80}
81
82#[must_use]
83pub fn parse() -> Command {
84    let Cli::Hatch(cmd) = Cli::parse();
85    cmd
86}
87
88/// Generate shell completions, written to the standard output.
89pub fn completions(shell: Shell) {
90    clap_complete::generate(
91        shell,
92        &mut Cli::command(),
93        env!("CARGO_PKG_NAME"),
94        &mut io::stdout().lock(),
95    );
96}
97
98/// Generate man pages in the target directory. The directory must already exist and none of the
99/// files exist, or an error is returned.
100pub fn manpages(dir: &Utf8Path) -> Result<()> {
101    fn print(dir: &Utf8Path, app: &clap::Command) -> Result<()> {
102        let name = app.get_display_name().unwrap_or_else(|| app.get_name());
103        let out = dir.join(format!("{name}.1"));
104        let mut out = OpenOptions::new()
105            .write(true)
106            .create_new(true)
107            .open(&out)
108            .with_context(|| format!("the file `{out}` already exists"))?;
109
110        clap_mangen::Man::new(app.clone()).render(&mut out)?;
111        out.flush()?;
112
113        for sub in app.get_subcommands() {
114            print(dir, sub)?;
115        }
116
117        Ok(())
118    }
119
120    ensure!(dir.try_exists()?, "target directory doesn't exist");
121
122    let mut app = Command::command();
123    app.build();
124
125    print(dir, &app)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::Cli;
131
132    #[test]
133    fn verify_cli() {
134        use clap::CommandFactory;
135        Cli::command().debug_assert();
136    }
137}