From c3124c271c4c87d040c4a990e9d7fe55ff8872a5 Mon Sep 17 00:00:00 2001 From: Ken Tobias <634380+l1a@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:44:57 -0800 Subject: [PATCH 1/3] chore: Add basic VS Code workspace configuration. --- ouch.code-workspace | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ouch.code-workspace 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 From 61d579b61ad9d9b3faf4fd95fb472415a9f425d8 Mon Sep 17 00:00:00 2001 From: Ken Tobias <634380+l1a@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:13:17 -0800 Subject: [PATCH 2/3] feat: add --completions flag --- Cargo.toml | 1 + src/cli/args.rs | 64 ++++++++++++++----- src/cli/mod.rs | 18 +++++- src/commands/mod.rs | 2 +- .../ui__ui_test_usage_help_flag-2.snap | 27 ++++---- .../ui__ui_test_usage_help_flag.snap | 9 ++- 6 files changed, 86 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 51f858a9d..d90d6653e 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/src/cli/args.rs b/src/cli/args.rs index 105f004be..f9c7fc993 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)] @@ -155,13 +159,14 @@ 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, no_smart_unpack: false, - }, + }), + completions: None, } } @@ -170,36 +175,36 @@ 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, no_smart_unpack: 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, no_smart_unpack: 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, no_smart_unpack: false, - }, + }), ..mock_cli_args() } ); @@ -207,42 +212,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() } ); @@ -260,19 +265,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 185fae6cd..834135d61 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,11 +27,24 @@ 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); 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 a753fd143..1513ea75c 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') From d92d1ba21d6521b3b618cf6769c2520cccd664b4 Mon Sep 17 00:00:00 2001 From: Ken Tobias <634380+l1a@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:13:25 -0800 Subject: [PATCH 3/3] docs: update documentation for --completions --- CHANGELOG.md | 1 + README.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffbe89ab..13ae29fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,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/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.