1use std::io::{IsTerminal, stderr, stdout};
8
9use clap::{Parser, Subcommand, ValueEnum};
10use rc_core::{RequestHeader, set_global_request_headers};
11
12use crate::exit_code::ExitCode;
13use crate::output::OutputConfig;
14
15mod admin;
16mod alias;
17mod anonymous;
18mod bucket;
19mod cat;
20mod completions;
21mod cors;
22pub mod cp;
23pub mod diff;
24mod event;
25mod find;
26mod head;
27mod ilm;
28mod ls;
29mod mb;
30mod mirror;
31mod mv;
32mod object;
33mod pipe;
34mod quota;
35mod rb;
36mod replicate;
37mod rm;
38mod share;
39mod sql;
40mod stat;
41mod tag;
42mod tree;
43mod version;
44
45#[derive(Parser, Debug)]
50#[command(name = "rc")]
51#[command(author, version, about, long_about = None)]
52#[command(propagate_version = true)]
53pub struct Cli {
54 #[arg(long, global = true, value_enum)]
56 pub format: Option<OutputFormat>,
57
58 #[arg(long, global = true, default_value = "false")]
60 pub json: bool,
61
62 #[arg(long, global = true, default_value = "false")]
64 pub no_color: bool,
65
66 #[arg(long, global = true, default_value = "false")]
68 pub no_progress: bool,
69
70 #[arg(short, long, global = true, default_value = "false")]
72 pub quiet: bool,
73
74 #[arg(long, global = true, default_value = "false")]
76 pub debug: bool,
77
78 #[arg(short = 'H', long = "header", global = true, value_parser = parse_request_header)]
80 pub request_headers: Vec<RequestHeader>,
81
82 #[command(subcommand)]
83 pub command: Commands,
84}
85
86fn parse_request_header(value: &str) -> Result<RequestHeader, String> {
87 RequestHeader::parse(value).map_err(|error| error.to_string())
88}
89
90#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
91pub enum OutputFormat {
92 Auto,
93 Human,
94 Json,
95}
96
97#[derive(Copy, Clone, Debug, Eq, PartialEq)]
98enum OutputBehavior {
99 HumanDefault,
100 StructuredDefault,
101}
102
103#[derive(Copy, Clone, Debug)]
104struct GlobalOutputOptions {
105 format: Option<OutputFormat>,
106 json: bool,
107 no_color: bool,
108 no_progress: bool,
109 quiet: bool,
110}
111
112impl GlobalOutputOptions {
113 fn from_cli(cli: &Cli) -> Self {
114 Self {
115 format: cli.format,
116 json: cli.json,
117 no_color: cli.no_color,
118 no_progress: cli.no_progress,
119 quiet: cli.quiet,
120 }
121 }
122
123 fn resolve(self, behavior: OutputBehavior) -> OutputConfig {
124 let stdout_is_tty = stdout().is_terminal();
125 let stderr_is_tty = stderr().is_terminal();
126
127 let selected_format = if self.json {
128 OutputFormat::Json
129 } else {
130 self.format.unwrap_or(match behavior {
131 OutputBehavior::HumanDefault => OutputFormat::Human,
132 OutputBehavior::StructuredDefault => OutputFormat::Auto,
133 })
134 };
135
136 let json = match selected_format {
137 OutputFormat::Json => true,
138 OutputFormat::Human => false,
139 OutputFormat::Auto => !stdout_is_tty,
140 };
141
142 OutputConfig {
143 json,
144 no_color: self.no_color || !stdout_is_tty || json,
145 no_progress: self.no_progress || !stderr_is_tty || json,
146 quiet: self.quiet,
147 }
148 }
149}
150
151#[derive(Subcommand, Debug)]
152pub enum Commands {
153 #[command(subcommand)]
155 Alias(alias::AliasCommands),
156
157 #[command(subcommand)]
159 Admin(admin::AdminCommands),
160
161 Bucket(bucket::BucketArgs),
163
164 Object(object::ObjectArgs),
166
167 Ls(ls::LsArgs),
170
171 Mb(mb::MbArgs),
173
174 Rb(rb::RbArgs),
176
177 Cat(cat::CatArgs),
179
180 Head(head::HeadArgs),
182
183 Stat(stat::StatArgs),
185
186 Cp(cp::CpArgs),
189
190 Mv(mv::MvArgs),
192
193 Rm(rm::RmArgs),
195
196 Pipe(pipe::PipeArgs),
198
199 Find(find::FindArgs),
202
203 Event(event::EventArgs),
205
206 #[command(subcommand)]
208 Cors(cors::CorsCommands),
209
210 Diff(diff::DiffArgs),
212
213 Mirror(mirror::MirrorArgs),
215
216 Tree(tree::TreeArgs),
218
219 Share(share::ShareArgs),
221
222 Sql(sql::SqlArgs),
224
225 #[command(subcommand)]
228 Version(version::VersionCommands),
229
230 #[command(subcommand)]
232 Tag(tag::TagCommands),
233
234 #[command(subcommand)]
236 Anonymous(anonymous::AnonymousCommands),
237
238 #[command(subcommand)]
240 Quota(quota::QuotaCommands),
241
242 Ilm(ilm::IlmArgs),
244
245 Replicate(replicate::ReplicateArgs),
247
248 Completions(completions::CompletionsArgs),
251 }
256
257pub async fn execute(cli: Cli) -> ExitCode {
259 set_global_request_headers(cli.request_headers.clone());
260 let output_options = GlobalOutputOptions::from_cli(&cli);
261
262 match cli.command {
263 Commands::Alias(cmd) => {
264 alias::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
265 }
266 Commands::Admin(cmd) => {
267 admin::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
268 }
269 Commands::Bucket(args) => {
270 bucket::execute(
271 args,
272 output_options.resolve(OutputBehavior::StructuredDefault),
273 )
274 .await
275 }
276 Commands::Object(args) => {
277 let behavior = match &args.command {
278 object::ObjectCommands::Show(_) | object::ObjectCommands::Head(_) => {
279 OutputBehavior::HumanDefault
280 }
281 _ => OutputBehavior::StructuredDefault,
282 };
283 object::execute(args, output_options.resolve(behavior)).await
284 }
285 Commands::Ls(args) => {
286 ls::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
287 }
288 Commands::Mb(args) => {
289 mb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
290 }
291 Commands::Rb(args) => {
292 rb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
293 }
294 Commands::Cat(args) => {
295 cat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
296 }
297 Commands::Head(args) => {
298 head::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
299 }
300 Commands::Stat(args) => {
301 stat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
302 }
303 Commands::Cp(args) => {
304 cp::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
305 }
306 Commands::Mv(args) => {
307 mv::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
308 }
309 Commands::Rm(args) => {
310 rm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
311 }
312 Commands::Pipe(args) => {
313 pipe::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
314 }
315 Commands::Find(args) => {
316 find::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
317 }
318 Commands::Event(args) => {
319 event::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
320 }
321 Commands::Cors(cmd) => {
322 cors::execute(
323 cors::CorsArgs { command: cmd },
324 output_options.resolve(OutputBehavior::HumanDefault),
325 )
326 .await
327 }
328 Commands::Diff(args) => {
329 diff::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
330 }
331 Commands::Mirror(args) => {
332 mirror::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
333 }
334 Commands::Tree(args) => {
335 tree::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
336 }
337 Commands::Share(args) => {
338 share::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
339 }
340 Commands::Sql(args) => {
341 sql::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
342 }
343 Commands::Version(cmd) => {
344 version::execute(
345 version::VersionArgs { command: cmd },
346 output_options.resolve(OutputBehavior::HumanDefault),
347 )
348 .await
349 }
350 Commands::Tag(cmd) => {
351 tag::execute(
352 tag::TagArgs { command: cmd },
353 output_options.resolve(OutputBehavior::HumanDefault),
354 )
355 .await
356 }
357 Commands::Anonymous(cmd) => {
358 anonymous::execute(
359 anonymous::AnonymousArgs { command: cmd },
360 output_options.resolve(OutputBehavior::HumanDefault),
361 )
362 .await
363 }
364 Commands::Quota(cmd) => {
365 quota::execute(
366 quota::QuotaArgs { command: cmd },
367 output_options.resolve(OutputBehavior::HumanDefault),
368 )
369 .await
370 }
371 Commands::Ilm(args) => {
372 ilm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
373 }
374 Commands::Replicate(args) => {
375 replicate::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
376 }
377 Commands::Completions(args) => completions::execute(args),
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use clap::Parser;
385
386 #[test]
387 fn structured_default_uses_auto_format_when_not_explicit() {
388 let options = GlobalOutputOptions {
389 format: None,
390 json: false,
391 no_color: false,
392 no_progress: false,
393 quiet: false,
394 };
395
396 let resolved = options.resolve(OutputBehavior::StructuredDefault);
397 assert_eq!(resolved.json, !std::io::stdout().is_terminal());
398 }
399
400 #[test]
401 fn human_default_keeps_human_format_when_not_explicit() {
402 let options = GlobalOutputOptions {
403 format: None,
404 json: false,
405 no_color: false,
406 no_progress: false,
407 quiet: false,
408 };
409
410 let resolved = options.resolve(OutputBehavior::HumanDefault);
411 assert!(!resolved.json);
412 }
413
414 #[test]
415 fn explicit_json_overrides_behavior_defaults() {
416 let options = GlobalOutputOptions {
417 format: Some(OutputFormat::Human),
418 json: true,
419 no_color: false,
420 no_progress: false,
421 quiet: false,
422 };
423
424 let resolved = options.resolve(OutputBehavior::HumanDefault);
425 assert!(resolved.json);
426 }
427
428 #[test]
429 fn explicit_human_overrides_structured_default() {
430 let options = GlobalOutputOptions {
431 format: Some(OutputFormat::Human),
432 json: false,
433 no_color: false,
434 no_progress: false,
435 quiet: false,
436 };
437
438 let resolved = options.resolve(OutputBehavior::StructuredDefault);
439 assert!(!resolved.json);
440 }
441
442 #[test]
443 fn explicit_auto_overrides_human_default() {
444 let options = GlobalOutputOptions {
445 format: Some(OutputFormat::Auto),
446 json: false,
447 no_color: false,
448 no_progress: false,
449 quiet: false,
450 };
451
452 let resolved = options.resolve(OutputBehavior::HumanDefault);
453 assert_eq!(resolved.json, !std::io::stdout().is_terminal());
454 }
455
456 #[test]
457 fn cli_accepts_global_custom_amz_header() {
458 let cli = Cli::try_parse_from([
459 "rc",
460 "-H",
461 "x-amz-bucket-encrypt-enabled:1",
462 "bucket",
463 "list",
464 "local/",
465 ])
466 .expect("parse custom header");
467
468 assert_eq!(cli.request_headers.len(), 1);
469 assert_eq!(cli.request_headers[0].name, "x-amz-bucket-encrypt-enabled");
470 assert_eq!(cli.request_headers[0].value, "1");
471 }
472
473 #[test]
474 fn cli_rejects_non_amz_custom_header() {
475 let error = Cli::try_parse_from(["rc", "-H", "authorization:secret", "ls", "local/"])
476 .expect_err("non amz header should fail");
477
478 assert!(
479 error
480 .to_string()
481 .contains("Only x-amz-* custom request headers are supported")
482 );
483 }
484
485 #[test]
486 fn cli_accepts_bucket_cors_subcommand() {
487 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket"])
488 .expect("parse bucket cors");
489
490 match cli.command {
491 Commands::Bucket(args) => match args.command {
492 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
493 assert_eq!(arg.path, "local/my-bucket");
494 }
495 other => panic!("expected bucket cors list command, got {:?}", other),
496 },
497 other => panic!("expected bucket command, got {:?}", other),
498 }
499 }
500
501 #[test]
502 fn cli_accepts_bucket_list_alias() {
503 let cli =
504 Cli::try_parse_from(["rc", "bucket", "ls", "local/"]).expect("parse bucket ls alias");
505
506 match cli.command {
507 Commands::Bucket(args) => match args.command {
508 bucket::BucketCommands::List(arg) => {
509 assert_eq!(arg.path, "local/");
510 }
511 other => panic!("expected bucket list alias, got {:?}", other),
512 },
513 other => panic!("expected bucket command, got {:?}", other),
514 }
515 }
516
517 #[test]
518 fn cli_accepts_top_level_cors_subcommand() {
519 let cli = Cli::try_parse_from(["rc", "cors", "remove", "local/my-bucket"])
520 .expect("parse top-level cors");
521
522 match cli.command {
523 Commands::Cors(cors::CorsCommands::Remove(arg)) => {
524 assert_eq!(arg.path, "local/my-bucket");
525 }
526 other => panic!("expected top-level cors remove command, got {:?}", other),
527 }
528 }
529
530 #[test]
531 fn cli_accepts_top_level_cors_get_alias() {
532 let cli =
533 Cli::try_parse_from(["rc", "cors", "get", "local/my-bucket"]).expect("parse cors get");
534
535 match cli.command {
536 Commands::Cors(cors::CorsCommands::List(arg)) => {
537 assert_eq!(arg.path, "local/my-bucket");
538 }
539 other => panic!("expected top-level cors get alias, got {:?}", other),
540 }
541 }
542
543 #[test]
544 fn cli_accepts_top_level_event_subcommand() {
545 let cli = Cli::try_parse_from(["rc", "event", "list", "local/my-bucket"])
546 .expect("parse top-level event");
547
548 match cli.command {
549 Commands::Event(event::EventArgs {
550 command: event::EventCommands::List(arg),
551 }) => {
552 assert_eq!(arg.path, "local/my-bucket");
553 }
554 other => panic!("expected top-level event list command, got {:?}", other),
555 }
556 }
557
558 #[test]
559 fn cli_accepts_top_level_event_add_subcommand() {
560 let cli = Cli::try_parse_from([
561 "rc",
562 "event",
563 "add",
564 "local/my-bucket",
565 "arn:aws:sqs:us-east-1:123456789012:jobs",
566 "--event",
567 "put,delete",
568 "--force",
569 ])
570 .expect("parse top-level event add");
571
572 match cli.command {
573 Commands::Event(event::EventArgs {
574 command: event::EventCommands::Add(arg),
575 }) => {
576 assert_eq!(arg.path, "local/my-bucket");
577 assert_eq!(arg.arn, "arn:aws:sqs:us-east-1:123456789012:jobs");
578 assert_eq!(arg.events, vec!["put,delete".to_string()]);
579 assert!(arg.force);
580 }
581 other => panic!("expected top-level event add command, got {:?}", other),
582 }
583 }
584
585 #[test]
586 fn cli_accepts_sql_select_options() {
587 let cli = Cli::try_parse_from([
588 "rc",
589 "sql",
590 "local/reports/data.jsonl",
591 "--query",
592 "SELECT * FROM S3Object",
593 "--input-format",
594 "json",
595 "--output-format",
596 "json",
597 "--compression",
598 "gzip",
599 ])
600 .expect("parse sql command");
601
602 match cli.command {
603 Commands::Sql(arg) => {
604 assert_eq!(arg.path, "local/reports/data.jsonl");
605 assert_eq!(arg.query, "SELECT * FROM S3Object");
606 assert!(matches!(arg.input_format, sql::InputFormatArg::Json));
607 assert!(matches!(arg.output_format, sql::OutputFormatArg::Json));
608 assert!(matches!(arg.compression, sql::CompressionArg::Gzip));
609 }
610 other => panic!("expected sql command, got {:?}", other),
611 }
612 }
613
614 #[test]
615 fn cli_accepts_sql_defaults() {
616 let cli = Cli::try_parse_from([
617 "rc",
618 "sql",
619 "local/reports/data.csv",
620 "--query",
621 "SELECT s._1 FROM S3Object s",
622 ])
623 .expect("parse sql command defaults");
624
625 match cli.command {
626 Commands::Sql(arg) => {
627 assert_eq!(arg.path, "local/reports/data.csv");
628 assert_eq!(arg.query, "SELECT s._1 FROM S3Object s");
629 assert!(matches!(arg.input_format, sql::InputFormatArg::Csv));
630 assert!(matches!(arg.output_format, sql::OutputFormatArg::Csv));
631 assert!(matches!(arg.compression, sql::CompressionArg::None));
632 }
633 other => panic!("expected sql command, got {:?}", other),
634 }
635 }
636
637 #[test]
638 fn cli_accepts_object_list_alias() {
639 let cli = Cli::try_parse_from(["rc", "object", "ls", "local/my-bucket/logs/"])
640 .expect("parse object ls alias");
641
642 match cli.command {
643 Commands::Object(args) => match args.command {
644 object::ObjectCommands::List(arg) => {
645 assert_eq!(arg.path, "local/my-bucket/logs/");
646 }
647 other => panic!("expected object list alias, got {:?}", other),
648 },
649 other => panic!("expected object command, got {:?}", other),
650 }
651 }
652
653 #[test]
654 fn cli_accepts_top_level_event_remove_subcommand() {
655 let cli = Cli::try_parse_from([
656 "rc",
657 "event",
658 "remove",
659 "local/my-bucket",
660 "arn:aws:sns:us-east-1:123456789012:alerts",
661 "--force",
662 ])
663 .expect("parse top-level event remove");
664
665 match cli.command {
666 Commands::Event(event::EventArgs {
667 command: event::EventCommands::Remove(arg),
668 }) => {
669 assert_eq!(arg.path, "local/my-bucket");
670 assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
671 assert!(arg.force);
672 }
673 other => panic!("expected top-level event remove command, got {:?}", other),
674 }
675 }
676
677 #[test]
678 fn cli_accepts_bucket_cors_get_alias() {
679 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "get", "local/my-bucket"])
680 .expect("parse bucket cors get");
681
682 match cli.command {
683 Commands::Bucket(args) => match args.command {
684 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
685 assert_eq!(arg.path, "local/my-bucket");
686 }
687 other => panic!("expected bucket cors get alias, got {:?}", other),
688 },
689 other => panic!("expected bucket command, got {:?}", other),
690 }
691 }
692
693 #[test]
694 fn cli_accepts_bucket_cors_set_with_positional_source() {
695 let cli =
696 Cli::try_parse_from(["rc", "bucket", "cors", "set", "local/my-bucket", "cors.xml"])
697 .expect("parse bucket cors set with positional source");
698
699 match cli.command {
700 Commands::Bucket(args) => match args.command {
701 bucket::BucketCommands::Cors(cors::CorsCommands::Set(arg)) => {
702 assert_eq!(arg.path, "local/my-bucket");
703 assert_eq!(arg.source.as_deref(), Some("cors.xml"));
704 }
705 other => panic!("expected bucket cors set command, got {:?}", other),
706 },
707 other => panic!("expected bucket command, got {:?}", other),
708 }
709 }
710
711 #[test]
712 fn cli_accepts_top_level_cors_set_with_positional_source() {
713 let cli = Cli::try_parse_from(["rc", "cors", "set", "local/my-bucket", "cors.xml"])
714 .expect("parse top-level cors set with positional source");
715
716 match cli.command {
717 Commands::Cors(cors::CorsCommands::Set(arg)) => {
718 assert_eq!(arg.path, "local/my-bucket");
719 assert_eq!(arg.source.as_deref(), Some("cors.xml"));
720 assert_eq!(arg.file, None);
721 assert!(!arg.force);
722 }
723 other => panic!("expected top-level cors set command, got {:?}", other),
724 }
725 }
726
727 #[test]
728 fn cli_accepts_top_level_cors_set_with_legacy_file_flag() {
729 let cli = Cli::try_parse_from([
730 "rc",
731 "cors",
732 "set",
733 "local/my-bucket",
734 "--file",
735 "cors.json",
736 "--force",
737 ])
738 .expect("parse top-level cors set with --file");
739
740 match cli.command {
741 Commands::Cors(cors::CorsCommands::Set(arg)) => {
742 assert_eq!(arg.path, "local/my-bucket");
743 assert_eq!(arg.source, None);
744 assert_eq!(arg.file.as_deref(), Some("cors.json"));
745 assert!(arg.force);
746 }
747 other => panic!("expected top-level cors set command, got {:?}", other),
748 }
749 }
750
751 #[test]
752 fn cli_accepts_bucket_cors_list_force_flag() {
753 let cli =
754 Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket", "--force"])
755 .expect("parse bucket cors list with force");
756
757 match cli.command {
758 Commands::Bucket(args) => match args.command {
759 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
760 assert_eq!(arg.path, "local/my-bucket");
761 assert!(arg.force);
762 }
763 other => panic!("expected bucket cors list command, got {:?}", other),
764 },
765 other => panic!("expected bucket command, got {:?}", other),
766 }
767 }
768
769 #[test]
770 fn cli_accepts_bucket_lifecycle_subcommand() {
771 let cli = Cli::try_parse_from([
772 "rc",
773 "bucket",
774 "lifecycle",
775 "rule",
776 "list",
777 "local/my-bucket",
778 ])
779 .expect("parse bucket lifecycle rule list");
780
781 match cli.command {
782 Commands::Bucket(args) => match args.command {
783 bucket::BucketCommands::Lifecycle(ilm::IlmArgs {
784 command: ilm::IlmCommands::Rule(ilm::rule::RuleCommands::List(arg)),
785 }) => {
786 assert_eq!(arg.path, "local/my-bucket");
787 assert!(!arg.force);
788 }
789 other => panic!(
790 "expected bucket lifecycle rule list command, got {:?}",
791 other
792 ),
793 },
794 other => panic!("expected bucket command, got {:?}", other),
795 }
796 }
797
798 #[test]
799 fn cli_accepts_bucket_replication_subcommand() {
800 let cli = Cli::try_parse_from(["rc", "bucket", "replication", "status", "local/my-bucket"])
801 .expect("parse bucket replication status");
802
803 match cli.command {
804 Commands::Bucket(args) => match args.command {
805 bucket::BucketCommands::Replication(replicate::ReplicateArgs {
806 command: replicate::ReplicateCommands::Status(arg),
807 }) => {
808 assert_eq!(arg.path, "local/my-bucket");
809 assert!(!arg.force);
810 }
811 other => panic!(
812 "expected bucket replication status command, got {:?}",
813 other
814 ),
815 },
816 other => panic!("expected bucket command, got {:?}", other),
817 }
818 }
819
820 #[test]
821 fn cli_accepts_bucket_remove_subcommand() {
822 let cli = Cli::try_parse_from(["rc", "bucket", "remove", "local/my-bucket"])
823 .expect("parse bucket remove");
824
825 match cli.command {
826 Commands::Bucket(args) => match args.command {
827 bucket::BucketCommands::Remove(arg) => {
828 assert_eq!(arg.target, "local/my-bucket");
829 }
830 other => panic!("expected bucket remove command, got {:?}", other),
831 },
832 other => panic!("expected bucket command, got {:?}", other),
833 }
834 }
835
836 #[test]
837 fn cli_accepts_object_remove_subcommand() {
838 let cli = Cli::try_parse_from([
839 "rc",
840 "object",
841 "remove",
842 "local/my-bucket/report.csv",
843 "--dry-run",
844 ])
845 .expect("parse object remove");
846
847 match cli.command {
848 Commands::Object(args) => match args.command {
849 object::ObjectCommands::Remove(arg) => {
850 assert_eq!(arg.paths, vec!["local/my-bucket/report.csv".to_string()]);
851 assert!(arg.dry_run);
852 }
853 other => panic!("expected object remove command, got {:?}", other),
854 },
855 other => panic!("expected object command, got {:?}", other),
856 }
857 }
858
859 #[test]
860 fn cli_accepts_bucket_event_remove_subcommand() {
861 let cli = Cli::try_parse_from([
862 "rc",
863 "bucket",
864 "event",
865 "remove",
866 "local/my-bucket",
867 "arn:aws:sns:us-east-1:123456789012:alerts",
868 ])
869 .expect("parse bucket event remove");
870
871 match cli.command {
872 Commands::Bucket(args) => match args.command {
873 bucket::BucketCommands::Event(event::EventCommands::Remove(arg)) => {
874 assert_eq!(arg.path, "local/my-bucket");
875 assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
876 }
877 other => panic!("expected bucket event remove command, got {:?}", other),
878 },
879 other => panic!("expected bucket command, got {:?}", other),
880 }
881 }
882
883 #[test]
884 fn cli_accepts_rm_purge_flag() {
885 let cli = Cli::try_parse_from(["rc", "rm", "local/my-bucket/object.txt", "--purge"])
886 .expect("parse rm purge");
887
888 match cli.command {
889 Commands::Rm(arg) => {
890 assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
891 assert!(arg.purge);
892 }
893 other => panic!("expected rm command, got {:?}", other),
894 }
895 }
896
897 #[test]
898 fn cli_accepts_object_remove_purge_flag() {
899 let cli = Cli::try_parse_from([
900 "rc",
901 "object",
902 "remove",
903 "local/my-bucket/object.txt",
904 "--purge",
905 ])
906 .expect("parse object remove purge");
907
908 match cli.command {
909 Commands::Object(args) => match args.command {
910 object::ObjectCommands::Remove(arg) => {
911 assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
912 assert!(arg.purge);
913 }
914 other => panic!("expected object remove command, got {:?}", other),
915 },
916 other => panic!("expected object command, got {:?}", other),
917 }
918 }
919
920 #[test]
921 fn cli_accepts_object_stat_subcommand() {
922 let cli = Cli::try_parse_from(["rc", "object", "stat", "local/my-bucket/report.json"])
923 .expect("parse object stat");
924
925 match cli.command {
926 Commands::Object(args) => match args.command {
927 object::ObjectCommands::Stat(arg) => {
928 assert_eq!(arg.path, "local/my-bucket/report.json");
929 }
930 other => panic!("expected object stat command, got {:?}", other),
931 },
932 other => panic!("expected object command, got {:?}", other),
933 }
934 }
935
936 #[test]
937 fn cli_accepts_object_copy_with_transfer_options() {
938 let cli = Cli::try_parse_from([
939 "rc",
940 "object",
941 "copy",
942 "./report.json",
943 "local/my-bucket/reports/",
944 "--content-type",
945 "application/json",
946 "--storage-class",
947 "STANDARD_IA",
948 "--dry-run",
949 ])
950 .expect("parse object copy with transfer options");
951
952 match cli.command {
953 Commands::Object(args) => match args.command {
954 object::ObjectCommands::Copy(arg) => {
955 assert_eq!(arg.source, "./report.json");
956 assert_eq!(arg.target, "local/my-bucket/reports/");
957 assert_eq!(arg.content_type.as_deref(), Some("application/json"));
958 assert_eq!(arg.storage_class.as_deref(), Some("STANDARD_IA"));
959 assert!(arg.dry_run);
960 }
961 other => panic!("expected object copy command, got {:?}", other),
962 },
963 other => panic!("expected object command, got {:?}", other),
964 }
965 }
966
967 #[test]
968 fn cli_accepts_object_move_with_recursive_dry_run() {
969 let cli = Cli::try_parse_from([
970 "rc",
971 "object",
972 "move",
973 "local/source-bucket/logs/",
974 "local/archive-bucket/logs/",
975 "--recursive",
976 "--dry-run",
977 "--continue-on-error",
978 ])
979 .expect("parse object move with recursive dry-run");
980
981 match cli.command {
982 Commands::Object(args) => match args.command {
983 object::ObjectCommands::Move(arg) => {
984 assert_eq!(arg.source, "local/source-bucket/logs/");
985 assert_eq!(arg.target, "local/archive-bucket/logs/");
986 assert!(arg.recursive);
987 assert!(arg.dry_run);
988 assert!(arg.continue_on_error);
989 }
990 other => panic!("expected object move command, got {:?}", other),
991 },
992 other => panic!("expected object command, got {:?}", other),
993 }
994 }
995
996 #[test]
997 fn cli_accepts_object_show_and_head_options() {
998 let show_cli = Cli::try_parse_from([
999 "rc",
1000 "object",
1001 "show",
1002 "local/my-bucket/report.json",
1003 "--version-id",
1004 "v1",
1005 "--rewind",
1006 "1h",
1007 ])
1008 .expect("parse object show options");
1009
1010 match show_cli.command {
1011 Commands::Object(args) => match args.command {
1012 object::ObjectCommands::Show(arg) => {
1013 assert_eq!(arg.path, "local/my-bucket/report.json");
1014 assert_eq!(arg.version_id.as_deref(), Some("v1"));
1015 assert_eq!(arg.rewind.as_deref(), Some("1h"));
1016 }
1017 other => panic!("expected object show command, got {:?}", other),
1018 },
1019 other => panic!("expected object command, got {:?}", other),
1020 }
1021
1022 let head_cli = Cli::try_parse_from([
1023 "rc",
1024 "object",
1025 "head",
1026 "local/my-bucket/report.json",
1027 "--bytes",
1028 "128",
1029 "--version-id",
1030 "v2",
1031 ])
1032 .expect("parse object head options");
1033
1034 match head_cli.command {
1035 Commands::Object(args) => match args.command {
1036 object::ObjectCommands::Head(arg) => {
1037 assert_eq!(arg.path, "local/my-bucket/report.json");
1038 assert_eq!(arg.bytes, Some(128));
1039 assert_eq!(arg.version_id.as_deref(), Some("v2"));
1040 }
1041 other => panic!("expected object head command, got {:?}", other),
1042 },
1043 other => panic!("expected object command, got {:?}", other),
1044 }
1045 }
1046
1047 #[test]
1048 fn cli_accepts_object_find_and_tree_options() {
1049 let find_cli = Cli::try_parse_from([
1050 "rc",
1051 "object",
1052 "find",
1053 "local/my-bucket/logs/",
1054 "--name",
1055 "*.json",
1056 "--maxdepth",
1057 "2",
1058 "--count",
1059 "--print",
1060 ])
1061 .expect("parse object find options");
1062
1063 match find_cli.command {
1064 Commands::Object(args) => match args.command {
1065 object::ObjectCommands::Find(arg) => {
1066 assert_eq!(arg.path, "local/my-bucket/logs/");
1067 assert_eq!(arg.name.as_deref(), Some("*.json"));
1068 assert_eq!(arg.maxdepth, 2);
1069 assert!(arg.count);
1070 assert!(arg.print);
1071 }
1072 other => panic!("expected object find command, got {:?}", other),
1073 },
1074 other => panic!("expected object command, got {:?}", other),
1075 }
1076
1077 let tree_cli = Cli::try_parse_from([
1078 "rc",
1079 "object",
1080 "tree",
1081 "local/my-bucket/logs/",
1082 "--level",
1083 "4",
1084 "--size",
1085 "--pattern",
1086 "*.json",
1087 "--full-path",
1088 ])
1089 .expect("parse object tree options");
1090
1091 match tree_cli.command {
1092 Commands::Object(args) => match args.command {
1093 object::ObjectCommands::Tree(arg) => {
1094 assert_eq!(arg.path, "local/my-bucket/logs/");
1095 assert_eq!(arg.level, 4);
1096 assert!(arg.size);
1097 assert_eq!(arg.pattern.as_deref(), Some("*.json"));
1098 assert!(arg.full_path);
1099 }
1100 other => panic!("expected object tree command, got {:?}", other),
1101 },
1102 other => panic!("expected object command, got {:?}", other),
1103 }
1104 }
1105}