1use std::io::{IsTerminal, stderr, stdout};
8
9use clap::{Parser, Subcommand, ValueEnum};
10
11use crate::exit_code::ExitCode;
12use crate::output::OutputConfig;
13
14mod admin;
15mod alias;
16mod anonymous;
17mod bucket;
18mod cat;
19mod completions;
20mod cors;
21pub mod cp;
22pub mod diff;
23mod event;
24mod find;
25mod head;
26mod ilm;
27mod ls;
28mod mb;
29mod mirror;
30mod mv;
31mod object;
32mod pipe;
33mod quota;
34mod rb;
35mod replicate;
36mod rm;
37mod share;
38mod stat;
39mod tag;
40mod tree;
41mod version;
42
43#[derive(Parser, Debug)]
48#[command(name = "rc")]
49#[command(author, version, about, long_about = None)]
50#[command(propagate_version = true)]
51pub struct Cli {
52 #[arg(long, global = true, value_enum)]
54 pub format: Option<OutputFormat>,
55
56 #[arg(long, global = true, default_value = "false")]
58 pub json: bool,
59
60 #[arg(long, global = true, default_value = "false")]
62 pub no_color: bool,
63
64 #[arg(long, global = true, default_value = "false")]
66 pub no_progress: bool,
67
68 #[arg(short, long, global = true, default_value = "false")]
70 pub quiet: bool,
71
72 #[arg(long, global = true, default_value = "false")]
74 pub debug: bool,
75
76 #[command(subcommand)]
77 pub command: Commands,
78}
79
80#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
81pub enum OutputFormat {
82 Auto,
83 Human,
84 Json,
85}
86
87#[derive(Copy, Clone, Debug, Eq, PartialEq)]
88enum OutputBehavior {
89 HumanDefault,
90 StructuredDefault,
91}
92
93#[derive(Copy, Clone, Debug)]
94struct GlobalOutputOptions {
95 format: Option<OutputFormat>,
96 json: bool,
97 no_color: bool,
98 no_progress: bool,
99 quiet: bool,
100}
101
102impl GlobalOutputOptions {
103 fn from_cli(cli: &Cli) -> Self {
104 Self {
105 format: cli.format,
106 json: cli.json,
107 no_color: cli.no_color,
108 no_progress: cli.no_progress,
109 quiet: cli.quiet,
110 }
111 }
112
113 fn resolve(self, behavior: OutputBehavior) -> OutputConfig {
114 let stdout_is_tty = stdout().is_terminal();
115 let stderr_is_tty = stderr().is_terminal();
116
117 let selected_format = if self.json {
118 OutputFormat::Json
119 } else {
120 self.format.unwrap_or(match behavior {
121 OutputBehavior::HumanDefault => OutputFormat::Human,
122 OutputBehavior::StructuredDefault => OutputFormat::Auto,
123 })
124 };
125
126 let json = match selected_format {
127 OutputFormat::Json => true,
128 OutputFormat::Human => false,
129 OutputFormat::Auto => !stdout_is_tty,
130 };
131
132 OutputConfig {
133 json,
134 no_color: self.no_color || !stdout_is_tty || json,
135 no_progress: self.no_progress || !stderr_is_tty || json,
136 quiet: self.quiet,
137 }
138 }
139}
140
141#[derive(Subcommand, Debug)]
142pub enum Commands {
143 #[command(subcommand)]
145 Alias(alias::AliasCommands),
146
147 #[command(subcommand)]
149 Admin(admin::AdminCommands),
150
151 Bucket(bucket::BucketArgs),
153
154 Object(object::ObjectArgs),
156
157 Ls(ls::LsArgs),
160
161 Mb(mb::MbArgs),
163
164 Rb(rb::RbArgs),
166
167 Cat(cat::CatArgs),
169
170 Head(head::HeadArgs),
172
173 Stat(stat::StatArgs),
175
176 Cp(cp::CpArgs),
179
180 Mv(mv::MvArgs),
182
183 Rm(rm::RmArgs),
185
186 Pipe(pipe::PipeArgs),
188
189 Find(find::FindArgs),
192
193 Event(event::EventArgs),
195
196 #[command(subcommand)]
198 Cors(cors::CorsCommands),
199
200 Diff(diff::DiffArgs),
202
203 Mirror(mirror::MirrorArgs),
205
206 Tree(tree::TreeArgs),
208
209 Share(share::ShareArgs),
211
212 #[command(subcommand)]
215 Version(version::VersionCommands),
216
217 #[command(subcommand)]
219 Tag(tag::TagCommands),
220
221 #[command(subcommand)]
223 Anonymous(anonymous::AnonymousCommands),
224
225 #[command(subcommand)]
227 Quota(quota::QuotaCommands),
228
229 Ilm(ilm::IlmArgs),
231
232 Replicate(replicate::ReplicateArgs),
234
235 Completions(completions::CompletionsArgs),
238 }
245
246pub async fn execute(cli: Cli) -> ExitCode {
248 let output_options = GlobalOutputOptions::from_cli(&cli);
249
250 match cli.command {
251 Commands::Alias(cmd) => {
252 alias::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
253 }
254 Commands::Admin(cmd) => {
255 admin::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
256 }
257 Commands::Bucket(args) => {
258 bucket::execute(
259 args,
260 output_options.resolve(OutputBehavior::StructuredDefault),
261 )
262 .await
263 }
264 Commands::Object(args) => {
265 let behavior = match &args.command {
266 object::ObjectCommands::Show(_) | object::ObjectCommands::Head(_) => {
267 OutputBehavior::HumanDefault
268 }
269 _ => OutputBehavior::StructuredDefault,
270 };
271 object::execute(args, output_options.resolve(behavior)).await
272 }
273 Commands::Ls(args) => {
274 ls::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
275 }
276 Commands::Mb(args) => {
277 mb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
278 }
279 Commands::Rb(args) => {
280 rb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
281 }
282 Commands::Cat(args) => {
283 cat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
284 }
285 Commands::Head(args) => {
286 head::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
287 }
288 Commands::Stat(args) => {
289 stat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
290 }
291 Commands::Cp(args) => {
292 cp::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
293 }
294 Commands::Mv(args) => {
295 mv::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
296 }
297 Commands::Rm(args) => {
298 rm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
299 }
300 Commands::Pipe(args) => {
301 pipe::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
302 }
303 Commands::Find(args) => {
304 find::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
305 }
306 Commands::Event(args) => {
307 event::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
308 }
309 Commands::Cors(cmd) => {
310 cors::execute(
311 cors::CorsArgs { command: cmd },
312 output_options.resolve(OutputBehavior::HumanDefault),
313 )
314 .await
315 }
316 Commands::Diff(args) => {
317 diff::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
318 }
319 Commands::Mirror(args) => {
320 mirror::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
321 }
322 Commands::Tree(args) => {
323 tree::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
324 }
325 Commands::Share(args) => {
326 share::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
327 }
328 Commands::Version(cmd) => {
329 version::execute(
330 version::VersionArgs { command: cmd },
331 output_options.resolve(OutputBehavior::HumanDefault),
332 )
333 .await
334 }
335 Commands::Tag(cmd) => {
336 tag::execute(
337 tag::TagArgs { command: cmd },
338 output_options.resolve(OutputBehavior::HumanDefault),
339 )
340 .await
341 }
342 Commands::Anonymous(cmd) => {
343 anonymous::execute(
344 anonymous::AnonymousArgs { command: cmd },
345 output_options.resolve(OutputBehavior::HumanDefault),
346 )
347 .await
348 }
349 Commands::Quota(cmd) => {
350 quota::execute(
351 quota::QuotaArgs { command: cmd },
352 output_options.resolve(OutputBehavior::HumanDefault),
353 )
354 .await
355 }
356 Commands::Ilm(args) => {
357 ilm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
358 }
359 Commands::Replicate(args) => {
360 replicate::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
361 }
362 Commands::Completions(args) => completions::execute(args),
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use clap::Parser;
370
371 #[test]
372 fn structured_default_uses_auto_format_when_not_explicit() {
373 let options = GlobalOutputOptions {
374 format: None,
375 json: false,
376 no_color: false,
377 no_progress: false,
378 quiet: false,
379 };
380
381 let resolved = options.resolve(OutputBehavior::StructuredDefault);
382 assert_eq!(resolved.json, !std::io::stdout().is_terminal());
383 }
384
385 #[test]
386 fn human_default_keeps_human_format_when_not_explicit() {
387 let options = GlobalOutputOptions {
388 format: None,
389 json: false,
390 no_color: false,
391 no_progress: false,
392 quiet: false,
393 };
394
395 let resolved = options.resolve(OutputBehavior::HumanDefault);
396 assert!(!resolved.json);
397 }
398
399 #[test]
400 fn explicit_json_overrides_behavior_defaults() {
401 let options = GlobalOutputOptions {
402 format: Some(OutputFormat::Human),
403 json: true,
404 no_color: false,
405 no_progress: false,
406 quiet: false,
407 };
408
409 let resolved = options.resolve(OutputBehavior::HumanDefault);
410 assert!(resolved.json);
411 }
412
413 #[test]
414 fn cli_accepts_bucket_cors_subcommand() {
415 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket"])
416 .expect("parse bucket cors");
417
418 match cli.command {
419 Commands::Bucket(args) => match args.command {
420 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
421 assert_eq!(arg.path, "local/my-bucket");
422 }
423 other => panic!("expected bucket cors list command, got {:?}", other),
424 },
425 other => panic!("expected bucket command, got {:?}", other),
426 }
427 }
428
429 #[test]
430 fn cli_accepts_top_level_cors_subcommand() {
431 let cli = Cli::try_parse_from(["rc", "cors", "remove", "local/my-bucket"])
432 .expect("parse top-level cors");
433
434 match cli.command {
435 Commands::Cors(cors::CorsCommands::Remove(arg)) => {
436 assert_eq!(arg.path, "local/my-bucket");
437 }
438 other => panic!("expected top-level cors remove command, got {:?}", other),
439 }
440 }
441
442 #[test]
443 fn cli_accepts_bucket_cors_get_alias() {
444 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "get", "local/my-bucket"])
445 .expect("parse bucket cors get");
446
447 match cli.command {
448 Commands::Bucket(args) => match args.command {
449 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
450 assert_eq!(arg.path, "local/my-bucket");
451 }
452 other => panic!("expected bucket cors get alias, got {:?}", other),
453 },
454 other => panic!("expected bucket command, got {:?}", other),
455 }
456 }
457
458 #[test]
459 fn cli_accepts_bucket_cors_set_with_positional_source() {
460 let cli =
461 Cli::try_parse_from(["rc", "bucket", "cors", "set", "local/my-bucket", "cors.xml"])
462 .expect("parse bucket cors set with positional source");
463
464 match cli.command {
465 Commands::Bucket(args) => match args.command {
466 bucket::BucketCommands::Cors(cors::CorsCommands::Set(arg)) => {
467 assert_eq!(arg.path, "local/my-bucket");
468 assert_eq!(arg.source.as_deref(), Some("cors.xml"));
469 }
470 other => panic!("expected bucket cors set command, got {:?}", other),
471 },
472 other => panic!("expected bucket command, got {:?}", other),
473 }
474 }
475}