greentic_bundle/cli/
mod.rs1use 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}