Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/generate_formula.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Why < Formula

def install
system "cargo", "install", "--locked", *std_cargo_args
generate_completions_from_executable(bin/"why", "--completion")
end

test do
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,6 @@ jobs:

- name: Build Nix Package
run: nix build

- name: Run Nix Checks
run: nix flake check
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ Simply run `why` followed by the command name.
why <command_name>
```

### Shell Completion

`why` can print completion scripts for bash, zsh, and fish:

```bash
why --completion bash
why --completion zsh
why --completion fish
```

The generated completion delegates `why <TAB>` and `why l<TAB>` to the shell's
native command completion instead of invoking `why` on every tab press.

For one-off bash usage:

```bash
source <(why --completion bash)
```

## Examples

### 1. Version Managers (e.g., Mise, Volta)
Expand Down
11 changes: 11 additions & 0 deletions completions/_why
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#compdef why

_why() {
_arguments \
'--completion[print a shell completion script]:shell:(bash zsh fish)' \
'--help[show help]' \
'--version[show version]' \
'1:command:_path_commands'
}

_why "$@"
24 changes: 24 additions & 0 deletions completions/why.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
_why() {
local cur prev
COMPREPLY=()

cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"

if [[ "$prev" == "--completion" ]]; then
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
return 0
fi

if [[ "$cur" == --* ]]; then
COMPREPLY=($(compgen -W "--completion --help --version" -- "$cur"))
return 0
fi

if (( COMP_CWORD == 1 )); then
COMPREPLY=($(compgen -c -- "$cur"))
return 0
fi
}

complete -F _why why
18 changes: 18 additions & 0 deletions completions/why.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function __why_needs_command
set -l tokens (commandline -opc)

for token in $tokens
switch $token
case --completion --help --version
return 1
end
end

test (count $tokens) -eq 1
end

complete -c why -f
complete -c why -l completion -x -a "bash zsh fish" -d "Print a shell completion script"
complete -c why -l help -d "Show help"
complete -c why -l version -d "Show version"
complete -c why -n "__why_needs_command" -a "(__fish_complete_command)" -d "Command"
22 changes: 22 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 29 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@

inputs = {
flake-utils.url = "github:numtide/flake-utils";
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};

outputs =
{ self, flake-utils, nixpkgs }:
{
self,
flake-utils,
home-manager,
nixpkgs,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
Expand All @@ -16,6 +25,21 @@
};

why-cli = pkgs.callPackage ./nix/package.nix { };
homeManagerCompletionConfig = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
self.homeManagerModules.default
{
home.username = "why-e2e";
home.homeDirectory = "/tmp/why-e2e-home";
home.stateVersion = "26.05";

programs.why.enable = true;
programs.zsh.enable = true;
programs.fish.enable = true;
}
];
};
in
{
packages = {
Expand All @@ -28,6 +52,10 @@
program = "${why-cli}/bin/why";
meta.description = "Run why";
};

checks.home-manager-completions = pkgs.callPackage ./tests/e2e/home-manager/completions.nix {
homePath = homeManagerCompletionConfig.config.home.path;
};
}
)
// {
Expand Down
12 changes: 12 additions & 0 deletions nix/package.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
lib,
installShellFiles,
rustPlatform,
}:

Expand All @@ -10,6 +11,17 @@ rustPlatform.buildRustPackage {
src = lib.cleanSource ../.;
cargoLock.lockFile = ../Cargo.lock;

nativeBuildInputs = [
installShellFiles
];

postInstall = ''
installShellCompletion --cmd why \
--bash completions/why.bash \
--fish completions/why.fish \
--zsh completions/_why
'';

meta = {
description = "Tells you why a command is installed on your system";
homepage = "https://github.com/akriaueno/why-cli";
Expand Down
69 changes: 66 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::fs;
use std::path::Path;
use std::process::{Command, ExitCode};

use clap::Parser;
use clap::{Parser, ValueEnum, ValueHint};

use crate::core::{DirEntryKind, ExecResult, WhyCtx, why_core};

Expand All @@ -16,8 +16,20 @@ use crate::core::{DirEntryKind, ExecResult, WhyCtx, why_core};
about = "Identify why a command is installed on your system"
)]
struct Args {
/// Print a shell completion script.
#[arg(long, value_enum, value_name = "SHELL", conflicts_with = "command")]
completion: Option<CompletionShell>,

/// The command to investigate, for example 'node' or 'ls'.
command: String,
#[arg(value_hint = ValueHint::CommandName, required_unless_present = "completion")]
command: Option<String>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum CompletionShell {
Bash,
Zsh,
Fish,
}

struct DefaultCtx;
Expand Down Expand Up @@ -126,9 +138,18 @@ impl WhyCtx for DefaultCtx {

fn main() -> ExitCode {
let args = Args::parse();
if let Some(shell) = args.completion {
print!("{}", completion_script(shell));
return ExitCode::SUCCESS;
}

let Some(command) = args.command else {
return ExitCode::from(2);
};

let ctx = DefaultCtx;

match why_core(&args.command, &ctx) {
match why_core(&command, &ctx) {
Ok(result) => {
if !result.hint.is_empty() {
println!("{}", result.hint);
Expand All @@ -145,3 +166,45 @@ fn main() -> ExitCode {
}
}
}

fn completion_script(shell: CompletionShell) -> &'static str {
match shell {
CompletionShell::Bash => BASH_COMPLETION,
CompletionShell::Zsh => ZSH_COMPLETION,
CompletionShell::Fish => FISH_COMPLETION,
}
}

const BASH_COMPLETION: &str = include_str!("../completions/why.bash");
const ZSH_COMPLETION: &str = include_str!("../completions/_why");
const FISH_COMPLETION: &str = include_str!("../completions/why.fish");

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_completion_without_command() {
let args = Args::try_parse_from(["why", "--completion", "bash"]).unwrap();

assert_eq!(args.completion, Some(CompletionShell::Bash));
assert_eq!(args.command, None);
}

#[test]
fn requires_command_without_completion() {
assert!(Args::try_parse_from(["why"]).is_err());
}

#[test]
fn completion_conflicts_with_command() {
assert!(Args::try_parse_from(["why", "--completion", "bash", "ls"]).is_err());
}

#[test]
fn completion_scripts_use_shell_native_command_completion() {
assert!(completion_script(CompletionShell::Bash).contains("compgen -c"));
assert!(completion_script(CompletionShell::Zsh).contains("_path_commands"));
assert!(completion_script(CompletionShell::Fish).contains("__fish_complete_command"));
}
}
62 changes: 62 additions & 0 deletions tests/e2e/home-manager/completions.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
bash,
homePath,
runCommand,
}:

runCommand "why-home-manager-completions-e2e"
{
nativeBuildInputs = [
bash
homePath
];
}
''
set -eu

test -x "${homePath}/bin/why"
test -x "${homePath}/bin/zsh"
test -x "${homePath}/bin/fish"

test -f "${homePath}/share/bash-completion/completions/why.bash"
test -f "${homePath}/share/zsh/site-functions/_why"
test -f "${homePath}/share/fish/vendor_completions.d/why.fish"

"${homePath}/bin/why" --version | grep -E '^why [0-9]+\.[0-9]+\.[0-9]+'

BASH_COMPLETION_FILE="${homePath}/share/bash-completion/completions/why.bash" \
PATH="${homePath}/bin:$PATH" \
bash --noprofile --norc > "$TMPDIR/bash.out" <<'BASH'
set -euo pipefail
source "$BASH_COMPLETION_FILE"
COMP_WORDS=(why l)
COMP_CWORD=1
_why
printf '%s\n' "''${COMPREPLY[@]}"
BASH
grep -Fx "ls" "$TMPDIR/bash.out"

ZSH_COMPLETION_DIR="${homePath}/share/zsh/site-functions" \
PATH="${homePath}/bin:$PATH" \
zsh -f > "$TMPDIR/zsh.out" <<'ZSH'
set -e
fpath=("$ZSH_COMPLETION_DIR" $fpath)
autoload -Uz compinit
compinit -D
test "$_comps[why]" = "_why"
autoload -Uz _why
autoload +X _why
functions _why
ZSH
grep -F "_path_commands" "$TMPDIR/zsh.out"

FISH_COMPLETION_FILE="${homePath}/share/fish/vendor_completions.d/why.fish" \
PATH="${homePath}/bin:$PATH" \
fish --no-config > "$TMPDIR/fish.out" <<'FISH'
source "$FISH_COMPLETION_FILE"
complete -C "why l"
FISH
cut -f1 "$TMPDIR/fish.out" | grep -Fx "ls"

touch "$out"
''