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