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 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}