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_sql_select_options() {
548        let cli = Cli::try_parse_from([
549            "rc",
550            "sql",
551            "local/reports/data.jsonl",
552            "--query",
553            "SELECT * FROM S3Object",
554            "--input-format",
555            "json",
556            "--output-format",
557            "json",
558            "--compression",
559            "gzip",
560        ])
561        .expect("parse sql command");
562
563        match cli.command {
564            Commands::Sql(arg) => {
565                assert_eq!(arg.path, "local/reports/data.jsonl");
566                assert_eq!(arg.query, "SELECT * FROM S3Object");
567                assert!(matches!(arg.input_format, sql::InputFormatArg::Json));
568                assert!(matches!(arg.output_format, sql::OutputFormatArg::Json));
569                assert!(matches!(arg.compression, sql::CompressionArg::Gzip));
570            }
571            other => panic!("expected sql command, got {:?}", other),
572        }
573    }
574
575    #[test]
576    fn cli_accepts_sql_defaults() {
577        let cli = Cli::try_parse_from([
578            "rc",
579            "sql",
580            "local/reports/data.csv",
581            "--query",
582            "SELECT s._1 FROM S3Object s",
583        ])
584        .expect("parse sql command defaults");
585
586        match cli.command {
587            Commands::Sql(arg) => {
588                assert_eq!(arg.path, "local/reports/data.csv");
589                assert_eq!(arg.query, "SELECT s._1 FROM S3Object s");
590                assert!(matches!(arg.input_format, sql::InputFormatArg::Csv));
591                assert!(matches!(arg.output_format, sql::OutputFormatArg::Csv));
592                assert!(matches!(arg.compression, sql::CompressionArg::None));
593            }
594            other => panic!("expected sql command, got {:?}", other),
595        }
596    }
597
598    #[test]
599    fn cli_accepts_object_list_alias() {
600        let cli = Cli::try_parse_from(["rc", "object", "ls", "local/my-bucket/logs/"])
601            .expect("parse object ls alias");
602
603        match cli.command {
604            Commands::Object(args) => match args.command {
605                object::ObjectCommands::List(arg) => {
606                    assert_eq!(arg.path, "local/my-bucket/logs/");
607                }
608                other => panic!("expected object list alias, got {:?}", other),
609            },
610            other => panic!("expected object command, got {:?}", other),
611        }
612    }
613
614    #[test]
615    fn cli_accepts_top_level_event_remove_subcommand() {
616        let cli = Cli::try_parse_from([
617            "rc",
618            "event",
619            "remove",
620            "local/my-bucket",
621            "arn:aws:sns:us-east-1:123456789012:alerts",
622            "--force",
623        ])
624        .expect("parse top-level event remove");
625
626        match cli.command {
627            Commands::Event(event::EventArgs {
628                command: event::EventCommands::Remove(arg),
629            }) => {
630                assert_eq!(arg.path, "local/my-bucket");
631                assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
632                assert!(arg.force);
633            }
634            other => panic!("expected top-level event remove command, got {:?}", other),
635        }
636    }
637
638    #[test]
639    fn cli_accepts_bucket_cors_get_alias() {
640        let cli = Cli::try_parse_from(["rc", "bucket", "cors", "get", "local/my-bucket"])
641            .expect("parse bucket cors get");
642
643        match cli.command {
644            Commands::Bucket(args) => match args.command {
645                bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
646                    assert_eq!(arg.path, "local/my-bucket");
647                }
648                other => panic!("expected bucket cors get alias, got {:?}", other),
649            },
650            other => panic!("expected bucket command, got {:?}", other),
651        }
652    }
653
654    #[test]
655    fn cli_accepts_bucket_cors_set_with_positional_source() {
656        let cli =
657            Cli::try_parse_from(["rc", "bucket", "cors", "set", "local/my-bucket", "cors.xml"])
658                .expect("parse bucket cors set with positional source");
659
660        match cli.command {
661            Commands::Bucket(args) => match args.command {
662                bucket::BucketCommands::Cors(cors::CorsCommands::Set(arg)) => {
663                    assert_eq!(arg.path, "local/my-bucket");
664                    assert_eq!(arg.source.as_deref(), Some("cors.xml"));
665                }
666                other => panic!("expected bucket cors set command, got {:?}", other),
667            },
668            other => panic!("expected bucket command, got {:?}", other),
669        }
670    }
671
672    #[test]
673    fn cli_accepts_top_level_cors_set_with_positional_source() {
674        let cli = Cli::try_parse_from(["rc", "cors", "set", "local/my-bucket", "cors.xml"])
675            .expect("parse top-level cors set with positional source");
676
677        match cli.command {
678            Commands::Cors(cors::CorsCommands::Set(arg)) => {
679                assert_eq!(arg.path, "local/my-bucket");
680                assert_eq!(arg.source.as_deref(), Some("cors.xml"));
681                assert_eq!(arg.file, None);
682                assert!(!arg.force);
683            }
684            other => panic!("expected top-level cors set command, got {:?}", other),
685        }
686    }
687
688    #[test]
689    fn cli_accepts_top_level_cors_set_with_legacy_file_flag() {
690        let cli = Cli::try_parse_from([
691            "rc",
692            "cors",
693            "set",
694            "local/my-bucket",
695            "--file",
696            "cors.json",
697            "--force",
698        ])
699        .expect("parse top-level cors set with --file");
700
701        match cli.command {
702            Commands::Cors(cors::CorsCommands::Set(arg)) => {
703                assert_eq!(arg.path, "local/my-bucket");
704                assert_eq!(arg.source, None);
705                assert_eq!(arg.file.as_deref(), Some("cors.json"));
706                assert!(arg.force);
707            }
708            other => panic!("expected top-level cors set command, got {:?}", other),
709        }
710    }
711
712    #[test]
713    fn cli_accepts_bucket_cors_list_force_flag() {
714        let cli =
715            Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket", "--force"])
716                .expect("parse bucket cors list with force");
717
718        match cli.command {
719            Commands::Bucket(args) => match args.command {
720                bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
721                    assert_eq!(arg.path, "local/my-bucket");
722                    assert!(arg.force);
723                }
724                other => panic!("expected bucket cors list command, got {:?}", other),
725            },
726            other => panic!("expected bucket command, got {:?}", other),
727        }
728    }
729
730    #[test]
731    fn cli_accepts_bucket_lifecycle_subcommand() {
732        let cli = Cli::try_parse_from([
733            "rc",
734            "bucket",
735            "lifecycle",
736            "rule",
737            "list",
738            "local/my-bucket",
739        ])
740        .expect("parse bucket lifecycle rule list");
741
742        match cli.command {
743            Commands::Bucket(args) => match args.command {
744                bucket::BucketCommands::Lifecycle(ilm::IlmArgs {
745                    command: ilm::IlmCommands::Rule(ilm::rule::RuleCommands::List(arg)),
746                }) => {
747                    assert_eq!(arg.path, "local/my-bucket");
748                    assert!(!arg.force);
749                }
750                other => panic!(
751                    "expected bucket lifecycle rule list command, got {:?}",
752                    other
753                ),
754            },
755            other => panic!("expected bucket command, got {:?}", other),
756        }
757    }
758
759    #[test]
760    fn cli_accepts_bucket_replication_subcommand() {
761        let cli = Cli::try_parse_from(["rc", "bucket", "replication", "status", "local/my-bucket"])
762            .expect("parse bucket replication status");
763
764        match cli.command {
765            Commands::Bucket(args) => match args.command {
766                bucket::BucketCommands::Replication(replicate::ReplicateArgs {
767                    command: replicate::ReplicateCommands::Status(arg),
768                }) => {
769                    assert_eq!(arg.path, "local/my-bucket");
770                    assert!(!arg.force);
771                }
772                other => panic!(
773                    "expected bucket replication status command, got {:?}",
774                    other
775                ),
776            },
777            other => panic!("expected bucket command, got {:?}", other),
778        }
779    }
780
781    #[test]
782    fn cli_accepts_bucket_remove_subcommand() {
783        let cli = Cli::try_parse_from(["rc", "bucket", "remove", "local/my-bucket"])
784            .expect("parse bucket remove");
785
786        match cli.command {
787            Commands::Bucket(args) => match args.command {
788                bucket::BucketCommands::Remove(arg) => {
789                    assert_eq!(arg.target, "local/my-bucket");
790                }
791                other => panic!("expected bucket remove command, got {:?}", other),
792            },
793            other => panic!("expected bucket command, got {:?}", other),
794        }
795    }
796
797    #[test]
798    fn cli_accepts_object_remove_subcommand() {
799        let cli = Cli::try_parse_from([
800            "rc",
801            "object",
802            "remove",
803            "local/my-bucket/report.csv",
804            "--dry-run",
805        ])
806        .expect("parse object remove");
807
808        match cli.command {
809            Commands::Object(args) => match args.command {
810                object::ObjectCommands::Remove(arg) => {
811                    assert_eq!(arg.paths, vec!["local/my-bucket/report.csv".to_string()]);
812                    assert!(arg.dry_run);
813                }
814                other => panic!("expected object remove command, got {:?}", other),
815            },
816            other => panic!("expected object command, got {:?}", other),
817        }
818    }
819
820    #[test]
821    fn cli_accepts_bucket_event_remove_subcommand() {
822        let cli = Cli::try_parse_from([
823            "rc",
824            "bucket",
825            "event",
826            "remove",
827            "local/my-bucket",
828            "arn:aws:sns:us-east-1:123456789012:alerts",
829        ])
830        .expect("parse bucket event remove");
831
832        match cli.command {
833            Commands::Bucket(args) => match args.command {
834                bucket::BucketCommands::Event(event::EventCommands::Remove(arg)) => {
835                    assert_eq!(arg.path, "local/my-bucket");
836                    assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
837                }
838                other => panic!("expected bucket event remove command, got {:?}", other),
839            },
840            other => panic!("expected bucket command, got {:?}", other),
841        }
842    }
843
844    #[test]
845    fn cli_accepts_rm_purge_flag() {
846        let cli = Cli::try_parse_from(["rc", "rm", "local/my-bucket/object.txt", "--purge"])
847            .expect("parse rm purge");
848
849        match cli.command {
850            Commands::Rm(arg) => {
851                assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
852                assert!(arg.purge);
853            }
854            other => panic!("expected rm command, got {:?}", other),
855        }
856    }
857
858    #[test]
859    fn cli_accepts_object_remove_purge_flag() {
860        let cli = Cli::try_parse_from([
861            "rc",
862            "object",
863            "remove",
864            "local/my-bucket/object.txt",
865            "--purge",
866        ])
867        .expect("parse object remove purge");
868
869        match cli.command {
870            Commands::Object(args) => match args.command {
871                object::ObjectCommands::Remove(arg) => {
872                    assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
873                    assert!(arg.purge);
874                }
875                other => panic!("expected object remove command, got {:?}", other),
876            },
877            other => panic!("expected object command, got {:?}", other),
878        }
879    }
880
881    #[test]
882    fn cli_accepts_object_stat_subcommand() {
883        let cli = Cli::try_parse_from(["rc", "object", "stat", "local/my-bucket/report.json"])
884            .expect("parse object stat");
885
886        match cli.command {
887            Commands::Object(args) => match args.command {
888                object::ObjectCommands::Stat(arg) => {
889                    assert_eq!(arg.path, "local/my-bucket/report.json");
890                }
891                other => panic!("expected object stat command, got {:?}", other),
892            },
893            other => panic!("expected object command, got {:?}", other),
894        }
895    }
896
897    #[test]
898    fn cli_accepts_object_copy_with_transfer_options() {
899        let cli = Cli::try_parse_from([
900            "rc",
901            "object",
902            "copy",
903            "./report.json",
904            "local/my-bucket/reports/",
905            "--content-type",
906            "application/json",
907            "--storage-class",
908            "STANDARD_IA",
909            "--dry-run",
910        ])
911        .expect("parse object copy with transfer options");
912
913        match cli.command {
914            Commands::Object(args) => match args.command {
915                object::ObjectCommands::Copy(arg) => {
916                    assert_eq!(arg.source, "./report.json");
917                    assert_eq!(arg.target, "local/my-bucket/reports/");
918                    assert_eq!(arg.content_type.as_deref(), Some("application/json"));
919                    assert_eq!(arg.storage_class.as_deref(), Some("STANDARD_IA"));
920                    assert!(arg.dry_run);
921                }
922                other => panic!("expected object copy command, got {:?}", other),
923            },
924            other => panic!("expected object command, got {:?}", other),
925        }
926    }
927
928    #[test]
929    fn cli_accepts_object_move_with_recursive_dry_run() {
930        let cli = Cli::try_parse_from([
931            "rc",
932            "object",
933            "move",
934            "local/source-bucket/logs/",
935            "local/archive-bucket/logs/",
936            "--recursive",
937            "--dry-run",
938            "--continue-on-error",
939        ])
940        .expect("parse object move with recursive dry-run");
941
942        match cli.command {
943            Commands::Object(args) => match args.command {
944                object::ObjectCommands::Move(arg) => {
945                    assert_eq!(arg.source, "local/source-bucket/logs/");
946                    assert_eq!(arg.target, "local/archive-bucket/logs/");
947                    assert!(arg.recursive);
948                    assert!(arg.dry_run);
949                    assert!(arg.continue_on_error);
950                }
951                other => panic!("expected object move command, got {:?}", other),
952            },
953            other => panic!("expected object command, got {:?}", other),
954        }
955    }
956
957    #[test]
958    fn cli_accepts_object_show_and_head_options() {
959        let show_cli = Cli::try_parse_from([
960            "rc",
961            "object",
962            "show",
963            "local/my-bucket/report.json",
964            "--version-id",
965            "v1",
966            "--rewind",
967            "1h",
968        ])
969        .expect("parse object show options");
970
971        match show_cli.command {
972            Commands::Object(args) => match args.command {
973                object::ObjectCommands::Show(arg) => {
974                    assert_eq!(arg.path, "local/my-bucket/report.json");
975                    assert_eq!(arg.version_id.as_deref(), Some("v1"));
976                    assert_eq!(arg.rewind.as_deref(), Some("1h"));
977                }
978                other => panic!("expected object show command, got {:?}", other),
979            },
980            other => panic!("expected object command, got {:?}", other),
981        }
982
983        let head_cli = Cli::try_parse_from([
984            "rc",
985            "object",
986            "head",
987            "local/my-bucket/report.json",
988            "--bytes",
989            "128",
990            "--version-id",
991            "v2",
992        ])
993        .expect("parse object head options");
994
995        match head_cli.command {
996            Commands::Object(args) => match args.command {
997                object::ObjectCommands::Head(arg) => {
998                    assert_eq!(arg.path, "local/my-bucket/report.json");
999                    assert_eq!(arg.bytes, Some(128));
1000                    assert_eq!(arg.version_id.as_deref(), Some("v2"));
1001                }
1002                other => panic!("expected object head command, got {:?}", other),
1003            },
1004            other => panic!("expected object command, got {:?}", other),
1005        }
1006    }
1007
1008    #[test]
1009    fn cli_accepts_object_find_and_tree_options() {
1010        let find_cli = Cli::try_parse_from([
1011            "rc",
1012            "object",
1013            "find",
1014            "local/my-bucket/logs/",
1015            "--name",
1016            "*.json",
1017            "--maxdepth",
1018            "2",
1019            "--count",
1020            "--print",
1021        ])
1022        .expect("parse object find options");
1023
1024        match find_cli.command {
1025            Commands::Object(args) => match args.command {
1026                object::ObjectCommands::Find(arg) => {
1027                    assert_eq!(arg.path, "local/my-bucket/logs/");
1028                    assert_eq!(arg.name.as_deref(), Some("*.json"));
1029                    assert_eq!(arg.maxdepth, 2);
1030                    assert!(arg.count);
1031                    assert!(arg.print);
1032                }
1033                other => panic!("expected object find command, got {:?}", other),
1034            },
1035            other => panic!("expected object command, got {:?}", other),
1036        }
1037
1038        let tree_cli = Cli::try_parse_from([
1039            "rc",
1040            "object",
1041            "tree",
1042            "local/my-bucket/logs/",
1043            "--level",
1044            "4",
1045            "--size",
1046            "--pattern",
1047            "*.json",
1048            "--full-path",
1049        ])
1050        .expect("parse object tree options");
1051
1052        match tree_cli.command {
1053            Commands::Object(args) => match args.command {
1054                object::ObjectCommands::Tree(arg) => {
1055                    assert_eq!(arg.path, "local/my-bucket/logs/");
1056                    assert_eq!(arg.level, 4);
1057                    assert!(arg.size);
1058                    assert_eq!(arg.pattern.as_deref(), Some("*.json"));
1059                    assert!(arg.full_path);
1060                }
1061                other => panic!("expected object tree command, got {:?}", other),
1062            },
1063            other => panic!("expected object command, got {:?}", other),
1064        }
1065    }
1066}