Skip to main content

rustfs_cli/commands/
mod.rs

1//! CLI command definitions and execution
2//!
3//! This module contains all CLI commands and their implementations.
4//! Commands are organized by functionality and follow the pattern established
5//! in the command implementation template.
6
7use 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/// rc - Rust S3 CLI Client
44///
45/// A command-line interface for S3-compatible object storage services.
46/// Supports RustFS, AWS S3, and other S3-compatible backends.
47#[derive(Parser, Debug)]
48#[command(name = "rc")]
49#[command(author, version, about, long_about = None)]
50#[command(propagate_version = true)]
51pub struct Cli {
52    /// Output format: auto-detect, human-readable, or JSON
53    #[arg(long, global = true, value_enum)]
54    pub format: Option<OutputFormat>,
55
56    /// Output format: human-readable or JSON
57    #[arg(long, global = true, default_value = "false")]
58    pub json: bool,
59
60    /// Disable colored output
61    #[arg(long, global = true, default_value = "false")]
62    pub no_color: bool,
63
64    /// Disable progress bar
65    #[arg(long, global = true, default_value = "false")]
66    pub no_progress: bool,
67
68    /// Suppress non-error output
69    #[arg(short, long, global = true, default_value = "false")]
70    pub quiet: bool,
71
72    /// Enable debug logging
73    #[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    /// Manage storage service aliases
144    #[command(subcommand)]
145    Alias(alias::AliasCommands),
146
147    /// Manage IAM users, policies, groups, and service accounts
148    #[command(subcommand)]
149    Admin(admin::AdminCommands),
150
151    /// Manage bucket-oriented workflows
152    Bucket(bucket::BucketArgs),
153
154    /// Manage object-oriented workflows
155    Object(object::ObjectArgs),
156
157    // Phase 2: Basic commands
158    /// Deprecated: use `rc bucket list` or `rc object list`
159    Ls(ls::LsArgs),
160
161    /// Deprecated: use `rc bucket create`
162    Mb(mb::MbArgs),
163
164    /// Deprecated: use `rc bucket remove`
165    Rb(rb::RbArgs),
166
167    /// Deprecated: use `rc object show`
168    Cat(cat::CatArgs),
169
170    /// Deprecated: use `rc object head`
171    Head(head::HeadArgs),
172
173    /// Deprecated: use `rc object stat`
174    Stat(stat::StatArgs),
175
176    // Phase 3: Transfer commands
177    /// Deprecated: use `rc object copy`
178    Cp(cp::CpArgs),
179
180    /// Deprecated: use `rc object move`
181    Mv(mv::MvArgs),
182
183    /// Deprecated: use `rc object remove`
184    Rm(rm::RmArgs),
185
186    /// Stream stdin to an object
187    Pipe(pipe::PipeArgs),
188
189    // Phase 4: Advanced commands
190    /// Deprecated: use `rc object find`
191    Find(find::FindArgs),
192
193    /// Deprecated: use `rc bucket event`
194    Event(event::EventArgs),
195
196    /// Deprecated: use `rc bucket cors`
197    #[command(subcommand)]
198    Cors(cors::CorsCommands),
199
200    /// Show differences between locations
201    Diff(diff::DiffArgs),
202
203    /// Mirror objects between locations
204    Mirror(mirror::MirrorArgs),
205
206    /// Deprecated: use `rc object tree`
207    Tree(tree::TreeArgs),
208
209    /// Deprecated: use `rc object share`
210    Share(share::ShareArgs),
211
212    // Phase 5: Optional commands (capability-dependent)
213    /// Deprecated: use `rc bucket version`
214    #[command(subcommand)]
215    Version(version::VersionCommands),
216
217    /// Manage bucket and object tags
218    #[command(subcommand)]
219    Tag(tag::TagCommands),
220
221    /// Deprecated: use `rc bucket anonymous`
222    #[command(subcommand)]
223    Anonymous(anonymous::AnonymousCommands),
224
225    /// Deprecated: use `rc bucket quota`
226    #[command(subcommand)]
227    Quota(quota::QuotaCommands),
228
229    /// Deprecated: use `rc bucket lifecycle`
230    Ilm(ilm::IlmArgs),
231
232    /// Deprecated: use `rc bucket replication`
233    Replicate(replicate::ReplicateArgs),
234
235    // Phase 6: Utilities
236    /// Generate shell completion scripts
237    Completions(completions::CompletionsArgs),
238    // /// Manage object retention
239    // Retention(retention::RetentionArgs),
240    // /// Watch for object events
241    // Watch(watch::WatchArgs),
242    // /// Run S3 Select queries
243    // Sql(sql::SqlArgs),
244}
245
246/// Execute the CLI command and return an exit code
247pub 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 explicit_human_overrides_structured_default() {
415        let options = GlobalOutputOptions {
416            format: Some(OutputFormat::Human),
417            json: false,
418            no_color: false,
419            no_progress: false,
420            quiet: false,
421        };
422
423        let resolved = options.resolve(OutputBehavior::StructuredDefault);
424        assert!(!resolved.json);
425    }
426
427    #[test]
428    fn explicit_auto_overrides_human_default() {
429        let options = GlobalOutputOptions {
430            format: Some(OutputFormat::Auto),
431            json: false,
432            no_color: false,
433            no_progress: false,
434            quiet: false,
435        };
436
437        let resolved = options.resolve(OutputBehavior::HumanDefault);
438        assert_eq!(resolved.json, !std::io::stdout().is_terminal());
439    }
440
441    #[test]
442    fn cli_accepts_bucket_cors_subcommand() {
443        let cli = Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket"])
444            .expect("parse bucket cors");
445
446        match cli.command {
447            Commands::Bucket(args) => match args.command {
448                bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
449                    assert_eq!(arg.path, "local/my-bucket");
450                }
451                other => panic!("expected bucket cors list command, got {:?}", other),
452            },
453            other => panic!("expected bucket command, got {:?}", other),
454        }
455    }
456
457    #[test]
458    fn cli_accepts_bucket_list_alias() {
459        let cli =
460            Cli::try_parse_from(["rc", "bucket", "ls", "local/"]).expect("parse bucket ls alias");
461
462        match cli.command {
463            Commands::Bucket(args) => match args.command {
464                bucket::BucketCommands::List(arg) => {
465                    assert_eq!(arg.path, "local/");
466                }
467                other => panic!("expected bucket list alias, got {:?}", other),
468            },
469            other => panic!("expected bucket command, got {:?}", other),
470        }
471    }
472
473    #[test]
474    fn cli_accepts_top_level_cors_subcommand() {
475        let cli = Cli::try_parse_from(["rc", "cors", "remove", "local/my-bucket"])
476            .expect("parse top-level cors");
477
478        match cli.command {
479            Commands::Cors(cors::CorsCommands::Remove(arg)) => {
480                assert_eq!(arg.path, "local/my-bucket");
481            }
482            other => panic!("expected top-level cors remove command, got {:?}", other),
483        }
484    }
485
486    #[test]
487    fn cli_accepts_top_level_cors_get_alias() {
488        let cli =
489            Cli::try_parse_from(["rc", "cors", "get", "local/my-bucket"]).expect("parse cors get");
490
491        match cli.command {
492            Commands::Cors(cors::CorsCommands::List(arg)) => {
493                assert_eq!(arg.path, "local/my-bucket");
494            }
495            other => panic!("expected top-level cors get alias, got {:?}", other),
496        }
497    }
498
499    #[test]
500    fn cli_accepts_top_level_event_subcommand() {
501        let cli = Cli::try_parse_from(["rc", "event", "list", "local/my-bucket"])
502            .expect("parse top-level event");
503
504        match cli.command {
505            Commands::Event(event::EventArgs {
506                command: event::EventCommands::List(arg),
507            }) => {
508                assert_eq!(arg.path, "local/my-bucket");
509            }
510            other => panic!("expected top-level event list command, got {:?}", other),
511        }
512    }
513
514    #[test]
515    fn cli_accepts_top_level_event_add_subcommand() {
516        let cli = Cli::try_parse_from([
517            "rc",
518            "event",
519            "add",
520            "local/my-bucket",
521            "arn:aws:sqs:us-east-1:123456789012:jobs",
522            "--event",
523            "put,delete",
524            "--force",
525        ])
526        .expect("parse top-level event add");
527
528        match cli.command {
529            Commands::Event(event::EventArgs {
530                command: event::EventCommands::Add(arg),
531            }) => {
532                assert_eq!(arg.path, "local/my-bucket");
533                assert_eq!(arg.arn, "arn:aws:sqs:us-east-1:123456789012:jobs");
534                assert_eq!(arg.events, vec!["put,delete".to_string()]);
535                assert!(arg.force);
536            }
537            other => panic!("expected top-level event add command, got {:?}", other),
538        }
539    }
540
541    #[test]
542    fn cli_accepts_object_list_alias() {
543        let cli = Cli::try_parse_from(["rc", "object", "ls", "local/my-bucket/logs/"])
544            .expect("parse object ls alias");
545
546        match cli.command {
547            Commands::Object(args) => match args.command {
548                object::ObjectCommands::List(arg) => {
549                    assert_eq!(arg.path, "local/my-bucket/logs/");
550                }
551                other => panic!("expected object list alias, got {:?}", other),
552            },
553            other => panic!("expected object command, got {:?}", other),
554        }
555    }
556
557    #[test]
558    fn cli_accepts_top_level_event_remove_subcommand() {
559        let cli = Cli::try_parse_from([
560            "rc",
561            "event",
562            "remove",
563            "local/my-bucket",
564            "arn:aws:sns:us-east-1:123456789012:alerts",
565            "--force",
566        ])
567        .expect("parse top-level event remove");
568
569        match cli.command {
570            Commands::Event(event::EventArgs {
571                command: event::EventCommands::Remove(arg),
572            }) => {
573                assert_eq!(arg.path, "local/my-bucket");
574                assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
575                assert!(arg.force);
576            }
577            other => panic!("expected top-level event remove command, got {:?}", other),
578        }
579    }
580
581    #[test]
582    fn cli_accepts_bucket_cors_get_alias() {
583        let cli = Cli::try_parse_from(["rc", "bucket", "cors", "get", "local/my-bucket"])
584            .expect("parse bucket cors get");
585
586        match cli.command {
587            Commands::Bucket(args) => match args.command {
588                bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
589                    assert_eq!(arg.path, "local/my-bucket");
590                }
591                other => panic!("expected bucket cors get alias, got {:?}", other),
592            },
593            other => panic!("expected bucket command, got {:?}", other),
594        }
595    }
596
597    #[test]
598    fn cli_accepts_bucket_cors_set_with_positional_source() {
599        let cli =
600            Cli::try_parse_from(["rc", "bucket", "cors", "set", "local/my-bucket", "cors.xml"])
601                .expect("parse bucket cors set with positional source");
602
603        match cli.command {
604            Commands::Bucket(args) => match args.command {
605                bucket::BucketCommands::Cors(cors::CorsCommands::Set(arg)) => {
606                    assert_eq!(arg.path, "local/my-bucket");
607                    assert_eq!(arg.source.as_deref(), Some("cors.xml"));
608                }
609                other => panic!("expected bucket cors set command, got {:?}", other),
610            },
611            other => panic!("expected bucket command, got {:?}", other),
612        }
613    }
614
615    #[test]
616    fn cli_accepts_top_level_cors_set_with_positional_source() {
617        let cli = Cli::try_parse_from(["rc", "cors", "set", "local/my-bucket", "cors.xml"])
618            .expect("parse top-level cors set with positional source");
619
620        match cli.command {
621            Commands::Cors(cors::CorsCommands::Set(arg)) => {
622                assert_eq!(arg.path, "local/my-bucket");
623                assert_eq!(arg.source.as_deref(), Some("cors.xml"));
624                assert_eq!(arg.file, None);
625                assert!(!arg.force);
626            }
627            other => panic!("expected top-level cors set command, got {:?}", other),
628        }
629    }
630
631    #[test]
632    fn cli_accepts_top_level_cors_set_with_legacy_file_flag() {
633        let cli = Cli::try_parse_from([
634            "rc",
635            "cors",
636            "set",
637            "local/my-bucket",
638            "--file",
639            "cors.json",
640            "--force",
641        ])
642        .expect("parse top-level cors set with --file");
643
644        match cli.command {
645            Commands::Cors(cors::CorsCommands::Set(arg)) => {
646                assert_eq!(arg.path, "local/my-bucket");
647                assert_eq!(arg.source, None);
648                assert_eq!(arg.file.as_deref(), Some("cors.json"));
649                assert!(arg.force);
650            }
651            other => panic!("expected top-level cors set command, got {:?}", other),
652        }
653    }
654
655    #[test]
656    fn cli_accepts_bucket_cors_list_force_flag() {
657        let cli =
658            Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket", "--force"])
659                .expect("parse bucket cors list with force");
660
661        match cli.command {
662            Commands::Bucket(args) => match args.command {
663                bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
664                    assert_eq!(arg.path, "local/my-bucket");
665                    assert!(arg.force);
666                }
667                other => panic!("expected bucket cors list command, got {:?}", other),
668            },
669            other => panic!("expected bucket command, got {:?}", other),
670        }
671    }
672
673    #[test]
674    fn cli_accepts_bucket_lifecycle_subcommand() {
675        let cli = Cli::try_parse_from([
676            "rc",
677            "bucket",
678            "lifecycle",
679            "rule",
680            "list",
681            "local/my-bucket",
682        ])
683        .expect("parse bucket lifecycle rule list");
684
685        match cli.command {
686            Commands::Bucket(args) => match args.command {
687                bucket::BucketCommands::Lifecycle(ilm::IlmArgs {
688                    command: ilm::IlmCommands::Rule(ilm::rule::RuleCommands::List(arg)),
689                }) => {
690                    assert_eq!(arg.path, "local/my-bucket");
691                    assert!(!arg.force);
692                }
693                other => panic!(
694                    "expected bucket lifecycle rule list command, got {:?}",
695                    other
696                ),
697            },
698            other => panic!("expected bucket command, got {:?}", other),
699        }
700    }
701
702    #[test]
703    fn cli_accepts_bucket_replication_subcommand() {
704        let cli = Cli::try_parse_from(["rc", "bucket", "replication", "status", "local/my-bucket"])
705            .expect("parse bucket replication status");
706
707        match cli.command {
708            Commands::Bucket(args) => match args.command {
709                bucket::BucketCommands::Replication(replicate::ReplicateArgs {
710                    command: replicate::ReplicateCommands::Status(arg),
711                }) => {
712                    assert_eq!(arg.path, "local/my-bucket");
713                    assert!(!arg.force);
714                }
715                other => panic!(
716                    "expected bucket replication status command, got {:?}",
717                    other
718                ),
719            },
720            other => panic!("expected bucket command, got {:?}", other),
721        }
722    }
723
724    #[test]
725    fn cli_accepts_bucket_remove_subcommand() {
726        let cli = Cli::try_parse_from(["rc", "bucket", "remove", "local/my-bucket"])
727            .expect("parse bucket remove");
728
729        match cli.command {
730            Commands::Bucket(args) => match args.command {
731                bucket::BucketCommands::Remove(arg) => {
732                    assert_eq!(arg.target, "local/my-bucket");
733                }
734                other => panic!("expected bucket remove command, got {:?}", other),
735            },
736            other => panic!("expected bucket command, got {:?}", other),
737        }
738    }
739
740    #[test]
741    fn cli_accepts_object_remove_subcommand() {
742        let cli = Cli::try_parse_from([
743            "rc",
744            "object",
745            "remove",
746            "local/my-bucket/report.csv",
747            "--dry-run",
748        ])
749        .expect("parse object remove");
750
751        match cli.command {
752            Commands::Object(args) => match args.command {
753                object::ObjectCommands::Remove(arg) => {
754                    assert_eq!(arg.paths, vec!["local/my-bucket/report.csv".to_string()]);
755                    assert!(arg.dry_run);
756                }
757                other => panic!("expected object remove command, got {:?}", other),
758            },
759            other => panic!("expected object command, got {:?}", other),
760        }
761    }
762
763    #[test]
764    fn cli_accepts_bucket_event_remove_subcommand() {
765        let cli = Cli::try_parse_from([
766            "rc",
767            "bucket",
768            "event",
769            "remove",
770            "local/my-bucket",
771            "arn:aws:sns:us-east-1:123456789012:alerts",
772        ])
773        .expect("parse bucket event remove");
774
775        match cli.command {
776            Commands::Bucket(args) => match args.command {
777                bucket::BucketCommands::Event(event::EventCommands::Remove(arg)) => {
778                    assert_eq!(arg.path, "local/my-bucket");
779                    assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
780                }
781                other => panic!("expected bucket event remove command, got {:?}", other),
782            },
783            other => panic!("expected bucket command, got {:?}", other),
784        }
785    }
786
787    #[test]
788    fn cli_accepts_rm_purge_flag() {
789        let cli = Cli::try_parse_from(["rc", "rm", "local/my-bucket/object.txt", "--purge"])
790            .expect("parse rm purge");
791
792        match cli.command {
793            Commands::Rm(arg) => {
794                assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
795                assert!(arg.purge);
796            }
797            other => panic!("expected rm command, got {:?}", other),
798        }
799    }
800
801    #[test]
802    fn cli_accepts_object_remove_purge_flag() {
803        let cli = Cli::try_parse_from([
804            "rc",
805            "object",
806            "remove",
807            "local/my-bucket/object.txt",
808            "--purge",
809        ])
810        .expect("parse object remove purge");
811
812        match cli.command {
813            Commands::Object(args) => match args.command {
814                object::ObjectCommands::Remove(arg) => {
815                    assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
816                    assert!(arg.purge);
817                }
818                other => panic!("expected object remove command, got {:?}", other),
819            },
820            other => panic!("expected object command, got {:?}", other),
821        }
822    }
823
824    #[test]
825    fn cli_accepts_object_stat_subcommand() {
826        let cli = Cli::try_parse_from(["rc", "object", "stat", "local/my-bucket/report.json"])
827            .expect("parse object stat");
828
829        match cli.command {
830            Commands::Object(args) => match args.command {
831                object::ObjectCommands::Stat(arg) => {
832                    assert_eq!(arg.path, "local/my-bucket/report.json");
833                }
834                other => panic!("expected object stat command, got {:?}", other),
835            },
836            other => panic!("expected object command, got {:?}", other),
837        }
838    }
839
840    #[test]
841    fn cli_accepts_object_copy_with_transfer_options() {
842        let cli = Cli::try_parse_from([
843            "rc",
844            "object",
845            "copy",
846            "./report.json",
847            "local/my-bucket/reports/",
848            "--content-type",
849            "application/json",
850            "--storage-class",
851            "STANDARD_IA",
852            "--dry-run",
853        ])
854        .expect("parse object copy with transfer options");
855
856        match cli.command {
857            Commands::Object(args) => match args.command {
858                object::ObjectCommands::Copy(arg) => {
859                    assert_eq!(arg.source, "./report.json");
860                    assert_eq!(arg.target, "local/my-bucket/reports/");
861                    assert_eq!(arg.content_type.as_deref(), Some("application/json"));
862                    assert_eq!(arg.storage_class.as_deref(), Some("STANDARD_IA"));
863                    assert!(arg.dry_run);
864                }
865                other => panic!("expected object copy command, got {:?}", other),
866            },
867            other => panic!("expected object command, got {:?}", other),
868        }
869    }
870
871    #[test]
872    fn cli_accepts_object_move_with_recursive_dry_run() {
873        let cli = Cli::try_parse_from([
874            "rc",
875            "object",
876            "move",
877            "local/source-bucket/logs/",
878            "local/archive-bucket/logs/",
879            "--recursive",
880            "--dry-run",
881            "--continue-on-error",
882        ])
883        .expect("parse object move with recursive dry-run");
884
885        match cli.command {
886            Commands::Object(args) => match args.command {
887                object::ObjectCommands::Move(arg) => {
888                    assert_eq!(arg.source, "local/source-bucket/logs/");
889                    assert_eq!(arg.target, "local/archive-bucket/logs/");
890                    assert!(arg.recursive);
891                    assert!(arg.dry_run);
892                    assert!(arg.continue_on_error);
893                }
894                other => panic!("expected object move command, got {:?}", other),
895            },
896            other => panic!("expected object command, got {:?}", other),
897        }
898    }
899
900    #[test]
901    fn cli_accepts_object_show_and_head_options() {
902        let show_cli = Cli::try_parse_from([
903            "rc",
904            "object",
905            "show",
906            "local/my-bucket/report.json",
907            "--version-id",
908            "v1",
909            "--rewind",
910            "1h",
911        ])
912        .expect("parse object show options");
913
914        match show_cli.command {
915            Commands::Object(args) => match args.command {
916                object::ObjectCommands::Show(arg) => {
917                    assert_eq!(arg.path, "local/my-bucket/report.json");
918                    assert_eq!(arg.version_id.as_deref(), Some("v1"));
919                    assert_eq!(arg.rewind.as_deref(), Some("1h"));
920                }
921                other => panic!("expected object show command, got {:?}", other),
922            },
923            other => panic!("expected object command, got {:?}", other),
924        }
925
926        let head_cli = Cli::try_parse_from([
927            "rc",
928            "object",
929            "head",
930            "local/my-bucket/report.json",
931            "--bytes",
932            "128",
933            "--version-id",
934            "v2",
935        ])
936        .expect("parse object head options");
937
938        match head_cli.command {
939            Commands::Object(args) => match args.command {
940                object::ObjectCommands::Head(arg) => {
941                    assert_eq!(arg.path, "local/my-bucket/report.json");
942                    assert_eq!(arg.bytes, Some(128));
943                    assert_eq!(arg.version_id.as_deref(), Some("v2"));
944                }
945                other => panic!("expected object head command, got {:?}", other),
946            },
947            other => panic!("expected object command, got {:?}", other),
948        }
949    }
950
951    #[test]
952    fn cli_accepts_object_find_and_tree_options() {
953        let find_cli = Cli::try_parse_from([
954            "rc",
955            "object",
956            "find",
957            "local/my-bucket/logs/",
958            "--name",
959            "*.json",
960            "--maxdepth",
961            "2",
962            "--count",
963            "--print",
964        ])
965        .expect("parse object find options");
966
967        match find_cli.command {
968            Commands::Object(args) => match args.command {
969                object::ObjectCommands::Find(arg) => {
970                    assert_eq!(arg.path, "local/my-bucket/logs/");
971                    assert_eq!(arg.name.as_deref(), Some("*.json"));
972                    assert_eq!(arg.maxdepth, 2);
973                    assert!(arg.count);
974                    assert!(arg.print);
975                }
976                other => panic!("expected object find command, got {:?}", other),
977            },
978            other => panic!("expected object command, got {:?}", other),
979        }
980
981        let tree_cli = Cli::try_parse_from([
982            "rc",
983            "object",
984            "tree",
985            "local/my-bucket/logs/",
986            "--level",
987            "4",
988            "--size",
989            "--pattern",
990            "*.json",
991            "--full-path",
992        ])
993        .expect("parse object tree options");
994
995        match tree_cli.command {
996            Commands::Object(args) => match args.command {
997                object::ObjectCommands::Tree(arg) => {
998                    assert_eq!(arg.path, "local/my-bucket/logs/");
999                    assert_eq!(arg.level, 4);
1000                    assert!(arg.size);
1001                    assert_eq!(arg.pattern.as_deref(), Some("*.json"));
1002                    assert!(arg.full_path);
1003                }
1004                other => panic!("expected object tree command, got {:?}", other),
1005            },
1006            other => panic!("expected object command, got {:?}", other),
1007        }
1008    }
1009}