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