diff --git a/CHANGELOG.md b/CHANGELOG.md index 900852401..54720348e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Categories Used: - Support `.lzma` decompression (and fix `.lzma` being a wrong alias for `.xz`) [\#838](https://github.com/ouch-org/ouch/pull/838) ([zzzsyyy](https://github.com/zzzsyyy)) - Support `.lz` compression [\#867](https://github.com/ouch-org/ouch/pull/867) ([sorairolake](https://github.com/sorairolake)) - Support `.lzma` compression [\#867](https://github.com/ouch-org/ouch/pull/867) ([sorairolake](https://github.com/sorairolake)) +- Add `--completions` flag to generate shell completion scripts ### Improvements diff --git a/Cargo.toml b/Cargo.toml index 2930a38f3..bff0994c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ bytesize = "1.3.0" bzip2 = "0.4.4" bzip3 = { version = "0.9.0", features = ["bundled"], optional = true } clap = { version = "4.5.20", features = ["derive", "env"] } +clap_complete = "4.5.28" filetime_creation = "0.2" flate2 = { version = "1.0.30", default-features = false } fs-err = "2.11.0" diff --git a/README.md b/README.md index 0902d3448..6c6ec7b92 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,18 @@ ouch help ouch --help # equivalent ``` +## Shell Completions + +You can generate shell completion scripts using the `--completions` flag: + +```sh +# Generate bash completions +ouch --completions bash + +# Generate zsh completions +ouch --completions zsh +``` + ## Decompressing Use the `decompress` subcommand, `ouch` will detect the extensions automatically. diff --git a/ouch.code-workspace b/ouch.code-workspace new file mode 100644 index 000000000..876a1499c --- /dev/null +++ b/ouch.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/cli/args.rs b/src/cli/args.rs index bb2e8cee1..eec2fb850 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -49,9 +49,13 @@ pub struct CliArgs { #[arg(short = 'c', long, global = true)] pub threads: Option, + /// Generate shell completion scripts + #[arg(long, exclusive = true, value_enum)] + pub completions: Option, + // Ouch and claps subcommands #[command(subcommand)] - pub cmd: Subcommand, + pub cmd: Option, } #[derive(Parser, PartialEq, Eq, Debug)] @@ -151,12 +155,13 @@ mod tests { // This is usually replaced in assertion tests password: None, threads: None, - cmd: Subcommand::Decompress { + cmd: Some(Subcommand::Decompress { // Put a crazy value here so no test can assert it unintentionally files: vec!["\x00\x11\x22".into()], output_dir: None, remove: false, - }, + }), + completions: None, } } @@ -165,33 +170,33 @@ mod tests { test!( "ouch decompress file.tar.gz", CliArgs { - cmd: Subcommand::Decompress { + cmd: Some(Subcommand::Decompress { files: to_paths(["file.tar.gz"]), output_dir: None, remove: false, - }, + }), ..mock_cli_args() } ); test!( "ouch d file.tar.gz", CliArgs { - cmd: Subcommand::Decompress { + cmd: Some(Subcommand::Decompress { files: to_paths(["file.tar.gz"]), output_dir: None, remove: false, - }, + }), ..mock_cli_args() } ); test!( "ouch d a b c", CliArgs { - cmd: Subcommand::Decompress { + cmd: Some(Subcommand::Decompress { files: to_paths(["a", "b", "c"]), output_dir: None, remove: false, - }, + }), ..mock_cli_args() } ); @@ -199,42 +204,42 @@ mod tests { test!( "ouch compress file file.tar.gz", CliArgs { - cmd: Subcommand::Compress { + cmd: Some(Subcommand::Compress { files: to_paths(["file"]), output: PathBuf::from("file.tar.gz"), level: None, fast: false, slow: false, follow_symlinks: false, - }, + }), ..mock_cli_args() } ); test!( "ouch compress a b c archive.tar.gz", CliArgs { - cmd: Subcommand::Compress { + cmd: Some(Subcommand::Compress { files: to_paths(["a", "b", "c"]), output: PathBuf::from("archive.tar.gz"), level: None, fast: false, slow: false, follow_symlinks: false, - }, + }), ..mock_cli_args() } ); test!( "ouch compress a b c archive.tar.gz", CliArgs { - cmd: Subcommand::Compress { + cmd: Some(Subcommand::Compress { files: to_paths(["a", "b", "c"]), output: PathBuf::from("archive.tar.gz"), level: None, fast: false, slow: false, follow_symlinks: false, - }, + }), ..mock_cli_args() } ); @@ -252,19 +257,44 @@ mod tests { test!( input, CliArgs { - cmd: Subcommand::Compress { + cmd: Some(Subcommand::Compress { files: to_paths(["a", "b", "c"]), output: PathBuf::from("output"), level: None, fast: false, slow: false, follow_symlinks: false, - }, + }), format: Some("tar.gz".into()), ..mock_cli_args() } ); } + + test!( + "ouch --completions bash", + CliArgs { + completions: Some(clap_complete::Shell::Bash), + cmd: None, + ..mock_cli_args() + } + ); + test!( + "ouch --completions zsh", + CliArgs { + completions: Some(clap_complete::Shell::Zsh), + cmd: None, + ..mock_cli_args() + } + ); + test!( + "ouch --completions fish", + CliArgs { + completions: Some(clap_complete::Shell::Fish), + cmd: None, + ..mock_cli_args() + } + ); } #[test] diff --git a/src/cli/mod.rs b/src/cli/mod.rs index b5eb702a4..71b5f8ba7 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -7,7 +7,8 @@ use std::{ path::{Path, PathBuf}, }; -use clap::Parser; +use clap::{error::ErrorKind, CommandFactory, Parser}; +use clap_complete::generate; use fs_err as fs; pub use self::args::{CliArgs, Subcommand}; @@ -26,6 +27,19 @@ impl CliArgs { pub fn parse_and_validate_args() -> crate::Result<(Self, QuestionPolicy, FileVisibilityPolicy)> { let mut args = Self::parse(); + if let Some(shell) = args.completions { + let mut cmd = Self::command(); + let name = cmd.get_name().to_string(); + generate(shell, &mut cmd, name, &mut io::stdout()); + std::process::exit(0); + } + + if args.cmd.is_none() { + let mut cmd = Self::command(); + cmd.error(ErrorKind::MissingSubcommand, "A subcommand is required (compress, decompress, list)...") + .exit(); + } + set_accessible(args.accessible); set_log_display_level(args.quiet); @@ -36,7 +50,7 @@ impl CliArgs { let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. } - | Subcommand::List { archives: files, .. }) = &mut args.cmd; + | Subcommand::List { archives: files, .. }) = args.cmd.as_mut().unwrap(); *files = canonicalize_files(files)?; let skip_questions_positively = match (args.yes, args.no) { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4a6d1843b..5809a81b4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -60,7 +60,7 @@ pub fn run( .unwrap(); } - match args.cmd { + match args.cmd.unwrap() { Subcommand::Compress { files, output: output_path, diff --git a/tests/snapshots/ui__ui_test_usage_help_flag-2.snap b/tests/snapshots/ui__ui_test_usage_help_flag-2.snap index 75a873ef7..f4e80f9dd 100644 --- a/tests/snapshots/ui__ui_test_usage_help_flag-2.snap +++ b/tests/snapshots/ui__ui_test_usage_help_flag-2.snap @@ -1,11 +1,11 @@ --- source: tests/ui.rs +assertion_line: 176 expression: "output_to_string(ouch!(\"-h\"))" -snapshot_kind: text --- A command-line utility for easily compressing and decompressing files and directories. -Usage: [OPTIONS] +Usage: [OPTIONS] [COMMAND] Commands: compress Compress one or more files into one output file [aliases: c] @@ -14,14 +14,15 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -y, --yes Skip [Y/n] questions, default to yes - -n, --no Skip [Y/n] questions, default to no - -A, --accessible Activate accessibility mode, reducing visual noise [env: ACCESSIBLE=] - -H, --hidden Ignore hidden files - -q, --quiet Silence output - -g, --gitignore Ignore files matched by git's ignore files - -f, --format Specify the format of the archive - -p, --password Decompress or list with password - -c, --threads Concurrent working threads - -h, --help Print help (see more with '--help') - -V, --version Print version + -y, --yes Skip [Y/n] questions, default to yes + -n, --no Skip [Y/n] questions, default to no + -A, --accessible Activate accessibility mode, reducing visual noise [env: ACCESSIBLE=] + -H, --hidden Ignore hidden files + -q, --quiet Silence output + -g, --gitignore Ignore files matched by git's ignore files + -f, --format Specify the format of the archive + -p, --password Decompress or list with password + -c, --threads Concurrent working threads + --completions Generate shell completion scripts [possible values: bash, elvish, fish, powershell, zsh] + -h, --help Print help (see more with '--help') + -V, --version Print version diff --git a/tests/snapshots/ui__ui_test_usage_help_flag.snap b/tests/snapshots/ui__ui_test_usage_help_flag.snap index 9f3f8dbb5..0bd6222a3 100644 --- a/tests/snapshots/ui__ui_test_usage_help_flag.snap +++ b/tests/snapshots/ui__ui_test_usage_help_flag.snap @@ -1,7 +1,7 @@ --- source: tests/ui.rs +assertion_line: 175 expression: "output_to_string(ouch!(\"--help\"))" -snapshot_kind: text --- A command-line utility for easily compressing and decompressing files and directories. @@ -9,7 +9,7 @@ Supported formats: tar, zip, gz, 7z, xz, lzma, lzip, bz/bz2, bz3, lz4, sz (Snapp Repository: https://github.com/ouch-org/ouch -Usage: [OPTIONS] +Usage: [OPTIONS] [COMMAND] Commands: compress Compress one or more files into one output file [aliases: c] @@ -47,6 +47,11 @@ Options: -c, --threads Concurrent working threads + --completions + Generate shell completion scripts + + [possible values: bash, elvish, fish, powershell, zsh] + -h, --help Print help (see a summary with '-h')