Skip to main content

greentic_bundle/cli/
mod.rs

1use std::ffi::OsString;
2
3use anyhow::Result;
4use clap::{Arg, ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand};
5
6pub mod access;
7pub mod add;
8pub mod build;
9pub mod doctor;
10pub mod export;
11pub mod init;
12pub mod inspect;
13pub mod remove;
14pub mod unbundle;
15pub mod wizard;
16
17#[derive(Debug, Parser)]
18#[command(
19    name = "greentic-bundle",
20    about = "cli.root.about",
21    long_about = "cli.root.long_about",
22    version,
23    arg_required_else_help = true
24)]
25pub struct Cli {
26    #[arg(
27        long = "locale",
28        value_name = "LOCALE",
29        global = true,
30        help = "cli.option.locale"
31    )]
32    locale: Option<String>,
33
34    #[arg(
35        long = "offline",
36        global = true,
37        default_value_t = false,
38        help = "cli.option.offline"
39    )]
40    offline: bool,
41
42    #[command(subcommand)]
43    command: Commands,
44}
45
46#[derive(Debug, Subcommand)]
47enum Commands {
48    #[command(about = "cli.wizard.about")]
49    Wizard(wizard::WizardArgs),
50    #[command(about = "cli.doctor.about")]
51    Doctor(doctor::DoctorArgs),
52    #[command(about = "cli.build.about", long_about = "cli.build.long_about")]
53    Build(build::BuildArgs),
54    #[command(about = "cli.export.about", long_about = "cli.export.long_about")]
55    Export(export::ExportArgs),
56    #[command(about = "cli.inspect.about")]
57    Inspect(inspect::InspectArgs),
58    #[command(about = "cli.unbundle.about")]
59    Unbundle(unbundle::UnbundleArgs),
60    #[command(about = "cli.add.about")]
61    Add(add::AddArgs),
62    #[command(about = "cli.remove.about")]
63    Remove(remove::RemoveArgs),
64    #[command(about = "cli.access.about")]
65    Access(access::AccessArgs),
66    #[command(about = "cli.init.about")]
67    Init(init::InitArgs),
68}
69
70pub fn run() -> Result<()> {
71    let argv: Vec<OsString> = std::env::args_os().collect();
72    crate::i18n::init(crate::i18n::cli_locale_from_argv(&argv));
73
74    let mut command = localized_command(true);
75    let matches = match command.try_get_matches_from_mut(argv) {
76        Ok(matches) => matches,
77        Err(err) => err.exit(),
78    };
79    let cli = Cli::from_arg_matches(&matches)?;
80    crate::i18n::init(cli.locale.clone());
81    crate::runtime::set_offline(cli.offline);
82    cli.dispatch()
83}
84
85pub fn localized_command(is_root: bool) -> clap::Command {
86    localize_help(Cli::command(), is_root)
87}
88
89impl Cli {
90    fn dispatch(self) -> Result<()> {
91        match self.command {
92            Commands::Wizard(args) => wizard::run(args),
93            Commands::Doctor(args) => doctor::run(args),
94            Commands::Build(args) => build::run(args),
95            Commands::Export(args) => export::run(args),
96            Commands::Inspect(args) => inspect::run(args),
97            Commands::Unbundle(args) => unbundle::run(args),
98            Commands::Add(args) => add::run(args),
99            Commands::Remove(args) => remove::run(args),
100            Commands::Access(args) => access::run(args),
101            Commands::Init(args) => init::run(args),
102        }
103    }
104}
105
106fn localize_help(mut command: clap::Command, is_root: bool) -> clap::Command {
107    if let Some(about) = command.get_about().map(|s| s.to_string()) {
108        command = command.about(crate::i18n::tr(&about));
109    }
110    if let Some(long_about) = command.get_long_about().map(|s| s.to_string()) {
111        command = command.long_about(crate::i18n::tr(&long_about));
112    }
113    if let Some(before) = command.get_before_help().map(|s| s.to_string()) {
114        command = command.before_help(crate::i18n::tr(&before));
115    }
116    if let Some(after) = command.get_after_help().map(|s| s.to_string()) {
117        command = command.after_help(crate::i18n::tr(&after));
118    }
119
120    command = command
121        .disable_help_subcommand(true)
122        .disable_help_flag(true)
123        .arg(
124            Arg::new("help")
125                .short('h')
126                .long("help")
127                .action(ArgAction::Help)
128                .help(crate::i18n::tr("cli.help.flag")),
129        );
130    if is_root {
131        command = command.disable_version_flag(true).arg(
132            Arg::new("version")
133                .short('V')
134                .long("version")
135                .action(ArgAction::Version)
136                .help(crate::i18n::tr("cli.version.flag")),
137        );
138    }
139
140    let arg_ids = command
141        .get_arguments()
142        .map(|arg| arg.get_id().clone())
143        .collect::<Vec<_>>();
144    for arg_id in arg_ids {
145        command = command.mut_arg(arg_id, |arg| {
146            let mut arg = arg;
147            if let Some(help) = arg.get_help().map(ToString::to_string) {
148                arg = arg.help(crate::i18n::tr(&help));
149            }
150            if let Some(long_help) = arg.get_long_help().map(ToString::to_string) {
151                arg = arg.long_help(crate::i18n::tr(&long_help));
152            }
153            arg
154        });
155    }
156
157    let sub_names = command
158        .get_subcommands()
159        .map(|sub| sub.get_name().to_string())
160        .collect::<Vec<_>>();
161    for name in sub_names {
162        command = command.mut_subcommand(name, |sub| localize_help(sub, false));
163    }
164    command
165}
166
167#[cfg(test)]
168mod tests {
169    use clap::Parser;
170
171    use super::{Cli, Commands};
172
173    #[test]
174    fn parses_global_locale_and_wizard_flags() {
175        let cli = Cli::try_parse_from([
176            "greentic-bundle",
177            "--locale",
178            "en-US",
179            "wizard",
180            "run",
181            "--answers",
182            "answers.json",
183            "--emit-answers",
184            "out.json",
185            "--schema-version",
186            "1.2.3",
187            "--migrate",
188            "--dry-run",
189        ])
190        .expect("cli parses");
191
192        assert_eq!(cli.locale.as_deref(), Some("en-US"));
193        match cli.command {
194            Commands::Wizard(args) => match args.command.expect("wizard subcommand") {
195                super::wizard::WizardCommand::Run(run) => {
196                    assert_eq!(
197                        run.answers.as_deref(),
198                        Some(std::path::Path::new("answers.json"))
199                    );
200                    assert_eq!(
201                        run.emit_answers.as_deref(),
202                        Some(std::path::Path::new("out.json"))
203                    );
204                    assert_eq!(run.schema_version.as_deref(), Some("1.2.3"));
205                    assert!(run.migrate);
206                    assert!(run.dry_run);
207                }
208                _ => panic!("expected run"),
209            },
210            _ => panic!("expected wizard"),
211        }
212    }
213
214    #[test]
215    fn parses_access_allow_execute_flag() {
216        let cli = Cli::try_parse_from([
217            "greentic-bundle",
218            "access",
219            "allow",
220            "tenant-a",
221            "--execute",
222        ])
223        .expect("cli parses");
224
225        match cli.command {
226            Commands::Access(args) => match args.command {
227                super::access::AccessCommand::Allow(allow) => {
228                    assert_eq!(allow.subject, "tenant-a");
229                    assert!(allow.execute);
230                    assert!(!allow.dry_run);
231                }
232                _ => panic!("expected access allow"),
233            },
234            _ => panic!("expected access"),
235        }
236    }
237}