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