From 1b1e19dc1eac52c688fbaba6855a8126f29fcc9a Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Thu, 12 Mar 2026 12:11:06 +0200 Subject: [PATCH 1/3] - Improve `cd -d` for some use cases --- AGENTS.md | 26 ++++++++++++++++++++++++++ fuzzycd | 14 +++++++++++--- test/approve | 17 +++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e3b32ac --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# AGENTS.md + +## Scope + +These notes apply to work inside this repository. + +## Testing Rules + +- Keep using the existing approval-test style in `test/approve` unless there is a strong reason to introduce a different layer of testing. +- Never edit approval fixtures in `test/approvals` manually. +- If behavior changes intentionally, run the approval flow and let a human approve fixture updates. +- Prefer adding direct assertions in `test/approve` when a regression can be covered without changing fixtures. + +## Repo Utilities + +- Useful repo commands are defined in `op.conf`. +- Run them as `op COMMAND`. +- Current commands include `op shellcheck`, `op shfmt`, `op codespell`, and `op test`. + +## Working Notes + +- Preserve the existing shell style and test conventions. +- If temporary files or directories are needed, use `./tmp` only. +- Do not create temporary folders outside `./tmp`. +- Approval tests may need a temporary writable `HOME` in constrained environments; if so, place it under `./tmp`. +- Completed so far: TODO item 1, making `cd -d` respect `FUZZYCD_HISTORY_FILE` and avoid the fixed `$HOME` temp file. diff --git a/fuzzycd b/fuzzycd index 331c3ee..a445dc3 100644 --- a/fuzzycd +++ b/fuzzycd @@ -108,10 +108,18 @@ fuzzycd_run() { } _fzcd_delete_dir() { + local dir tmpfile status dir="${1:-$PWD}" - grep -Fxv "$dir" "$histfile" >"$HOME/.fuzzycd-history.tmp" - cp "$HOME/.fuzzycd-history.tmp" "$histfile" - rm -f "$HOME/.fuzzycd-history.tmp" + tmpfile=$(mktemp "${histfile}.XXXXXX") || return 1 + + grep -Fxv "$dir" "$histfile" >"$tmpfile" + status=$? + if [[ $status -gt 1 ]]; then + rm -f "$tmpfile" + return "$status" + fi + + mv "$tmpfile" "$histfile" echo "deleted $dir" } diff --git a/test/approve b/test/approve index 4f0960c..8a35138 100755 --- a/test/approve +++ b/test/approve @@ -92,6 +92,23 @@ context "when the history file contains gone directories" it "states that nothing was pruned if there is nothing to do" approve "cd -p" "cd_p_nothing_pruned" + +context "when using a custom history file path" + export FUZZYCD_HISTORY_FILE="$PWD/tmp/custom/history" + mkdir -p "$(dirname "$FUZZYCD_HISTORY_FILE")" + echo /usr/local/bin > "$FUZZYCD_HISTORY_FILE" + echo /usr/local/lib >> "$FUZZYCD_HISTORY_FILE" + rm -f "$HOME/.fuzzycd-history.tmp" + + describe "cd -d DIR" + it "rewrites the configured history file without using a fixed HOME temp file" + cd -d /usr/local/bin > /dev/null + [[ ! -e "$HOME/.fuzzycd-history.tmp" ]] || fail "Expected no temp file in HOME" + [[ "$(cat "$FUZZYCD_HISTORY_FILE")" == "/usr/local/lib" ]] || fail "Expected custom history file to be rewritten" + + export FUZZYCD_HISTORY_FILE="$PWD/tmp/histfile" + + context "when CDPATH contains entries" echo /usr/local/bin > "$FUZZYCD_HISTORY_FILE" export CDPATH=".:/usr:/etc" From e34347f6dd69572741d6d0a9783f4a893384471c Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Thu, 12 Mar 2026 14:01:47 +0200 Subject: [PATCH 2/3] - Fix bash completions --- AGENTS.md | 5 ++++- README.md | 12 ++++++++---- fuzzycd | 35 ++++++++++++++++++++++++++++++++--- setup | 1 + test/approvals/cd_c | 35 ++++++++++++++++++++++++++++++++--- test/approve | 40 ++++++++++++++++++++++++++++++++++++++++ test/approve_setup | 3 +++ 7 files changed, 120 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e3b32ac..30e8491 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,8 @@ These notes apply to work inside this repository. - Keep using the existing approval-test style in `test/approve` unless there is a strong reason to introduce a different layer of testing. - Never edit approval fixtures in `test/approvals` manually. -- If behavior changes intentionally, run the approval flow and let a human approve fixture updates. +- If behavior changes intentionally and a fixture update is needed, leave that to the human approval flow by running `test/approve`. +- If a test change causes an approval diff, keep the approval test in place, stop short of editing the fixture, and let the user run and approve it manually. - Prefer adding direct assertions in `test/approve` when a regression can be covered without changing fixtures. ## Repo Utilities @@ -24,3 +25,5 @@ These notes apply to work inside this repository. - Do not create temporary folders outside `./tmp`. - Approval tests may need a temporary writable `HOME` in constrained environments; if so, place it under `./tmp`. - Completed so far: TODO item 1, making `cd -d` respect `FUZZYCD_HISTORY_FILE` and avoid the fixed `$HOME` temp file. +- Completed so far: TODO item 2, making generated completions tolerate missing `_cd` and preserve paths with spaces. +- Installer behavior note: Bash completions may be installed automatically for Bash startup, but not for Zsh unless a native or explicitly supported compatibility path exists. diff --git a/README.md b/README.md index 73d046c..81c59ae 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,10 @@ matching directories when running in interactive mode, or you will ## Bash completions -To enable fuzzy bash completions, add the following line to your `~/.bashrc`: +If you install using the [`setup`](setup) script, fuzzy bash completions are +enabled automatically for Bash. + +If you install manually, add the following line to your `~/.bashrc`: ```bash eval "$(fuzzycd -c)" @@ -150,9 +153,10 @@ $ curl -Ls get.dannyb.co/fuzzycd/uninstall | bash Or remove manually: 1. Remove the `source /usr/local/bin/fuzzycd` line from your `~/.bashrc`. -2. Delete `/usr/local/bin/fuzzycd`. -3. Optionally, delete the history file (`~/.fuzzycd-history`). -4. Restart your session. +2. Remove the `eval "$(fuzzycd -c)"` line from your `~/.bashrc`, if you added it manually. +3. Delete `/usr/local/bin/fuzzycd`. +4. Optionally, delete the history file (`~/.fuzzycd-history`). +5. Restart your session. ## Contributing / Support diff --git a/fuzzycd b/fuzzycd index a445dc3..701fb8d 100644 --- a/fuzzycd +++ b/fuzzycd @@ -163,9 +163,38 @@ fuzzycd_run() { echo ' local cur=${COMP_WORDS[COMP_CWORD]}' echo ' local histfile=${FUZZYCD_HISTORY_FILE:-"$HOME/.fuzzycd-history"}' echo ' local count=${FUZZYCD_COMPLETIONS_COUNT:-10}' - echo ' _cd' # invoke original completions - echo ' [[ $cur =~ ^(/|\.) ]] && return' - echo ' COMPREPLY+=( $(fzf --filter "$cur" --exit-0 <"$histfile" | head -n$count) )' + echo ' local match existing found' + echo ' local -a fuzzy_matches native_matches' + echo ' if [[ $cur =~ ^(/|\.) ]]; then' + echo ' [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd' + echo ' declare -F _cd >/dev/null && _cd' + echo ' return' + echo ' fi' + echo ' while IFS= read -r match; do' + echo ' fuzzy_matches+=( "$match" )' + echo ' done < <(fzf --filter "$cur" --exit-0 <"$histfile" | head -n "$count")' + echo ' if ! declare -F _cd >/dev/null && declare -F _completion_loader >/dev/null; then' + echo ' _completion_loader cd >/dev/null 2>&1 || true' + echo ' fi' + echo ' if declare -F _cd >/dev/null; then' + echo ' COMPREPLY=()' + echo ' _cd' + echo ' native_matches=( "${COMPREPLY[@]}" )' + echo ' fi' + echo ' [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd' + echo ' COMPREPLY=()' + echo ' for match in "${fuzzy_matches[@]}" "${native_matches[@]}"; do' + echo ' [[ -n $match ]] || continue' + echo ' found=0' + echo ' for existing in "${COMPREPLY[@]}"; do' + echo ' if [[ $existing == "$match" ]]; then' + echo ' found=1' + echo ' break' + echo ' fi' + echo ' done' + echo ' [[ $found -eq 0 ]] && COMPREPLY+=( "$match" )' + echo ' done' + echo ' return 0' echo '}' echo 'complete -o nosort -F _fuzzycd_completions cd' } diff --git a/setup b/setup index 5d972da..d997d78 100755 --- a/setup +++ b/setup @@ -32,6 +32,7 @@ ssi install bin https://github.com/DannyBen/fuzzycd/releases/latest/download/fuz cat <<'EOF' | ssi install startup --shell bash --name fuzzycd.bashrc - if command -v fuzzycd >/dev/null 2>&1; then source "$(command -v fuzzycd)" + eval "$(fuzzycd -c)" # bash completions fi EOF diff --git a/test/approvals/cd_c b/test/approvals/cd_c index e97d345..f1b3036 100644 --- a/test/approvals/cd_c +++ b/test/approvals/cd_c @@ -2,8 +2,37 @@ _fuzzycd_completions() { local cur=${COMP_WORDS[COMP_CWORD]} local histfile=${FUZZYCD_HISTORY_FILE:-"$HOME/.fuzzycd-history"} local count=${FUZZYCD_COMPLETIONS_COUNT:-10} - _cd - [[ $cur =~ ^(/|\.) ]] && return - COMPREPLY+=( $(fzf --filter "$cur" --exit-0 <"$histfile" | head -n$count) ) + local match existing found + local -a fuzzy_matches native_matches + if [[ $cur =~ ^(/|\.) ]]; then + [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd + declare -F _cd >/dev/null && _cd + return + fi + while IFS= read -r match; do + fuzzy_matches+=( "$match" ) + done < <(fzf --filter "$cur" --exit-0 <"$histfile" | head -n "$count") + if ! declare -F _cd >/dev/null && declare -F _completion_loader >/dev/null; then + _completion_loader cd >/dev/null 2>&1 || true + fi + if declare -F _cd >/dev/null; then + COMPREPLY=() + _cd + native_matches=( "${COMPREPLY[@]}" ) + fi + [[ $(complete -p cd 2>/dev/null) == *"_fuzzycd_completions"* ]] || complete -o nosort -F _fuzzycd_completions cd + COMPREPLY=() + for match in "${fuzzy_matches[@]}" "${native_matches[@]}"; do + [[ -n $match ]] || continue + found=0 + for existing in "${COMPREPLY[@]}"; do + if [[ $existing == "$match" ]]; then + found=1 + break + fi + done + [[ $found -eq 0 ]] && COMPREPLY+=( "$match" ) + done + return 0 } complete -o nosort -F _fuzzycd_completions cd diff --git a/test/approve b/test/approve index 8a35138..c9b2a8c 100755 --- a/test/approve +++ b/test/approve @@ -30,6 +30,46 @@ context "when the shell is interactive" it "shows completions function" approve "cd -c" + it "handles spaces when _cd is unavailable" + mkdir -p "tmp/space dir" + printf '%s\n' "$PWD/tmp/space dir" > "$FUZZYCD_HISTORY_FILE" + eval "$(fuzzycd_run -c)" + unset -f _cd 2>/dev/null || true + COMP_WORDS=(cd space) + COMP_CWORD=1 + COMPREPLY=() + _fuzzycd_completions + [[ ${#COMPREPLY[@]} == 1 ]] || fail "Expected one completion, got ${#COMPREPLY[@]}" + [[ "${COMPREPLY[0]}" == "$PWD/tmp/space dir" ]] || fail "Expected spaced path completion" + [[ "$(fuzzycd_run -c)" != *mapfile* ]] || fail "Expected completion function to avoid mapfile dependency" + + it "puts fuzzy matches before standard cd completions and removes duplicates" + mkdir -p "tmp/fuzzy one" "tmp/fuzzy two" "tmp/native three" + printf '%s\n%s\n' "$PWD/tmp/fuzzy one" "$PWD/tmp/fuzzy two" > "$FUZZYCD_HISTORY_FILE" + eval "$(fuzzycd_run -c)" + _cd() { COMPREPLY=("$PWD/tmp/native three" "$PWD/tmp/fuzzy two"); } + COMP_WORDS=(cd fuzzy) + COMP_CWORD=1 + COMPREPLY=() + _fuzzycd_completions + [[ ${#COMPREPLY[@]} == 3 ]] || fail "Expected three merged completions, got ${#COMPREPLY[@]}" + [[ "${COMPREPLY[0]}" == "$PWD/tmp/fuzzy one" ]] || fail "Expected first fuzzy match first" + [[ "${COMPREPLY[1]}" == "$PWD/tmp/fuzzy two" ]] || fail "Expected second fuzzy match second" + [[ "${COMPREPLY[2]}" == "$PWD/tmp/native three" ]] || fail "Expected native completion last" + + it "keeps fuzzycd registered as the cd completion function" + printf '%s\n' "$PWD/tmp/one/two" > "$FUZZYCD_HISTORY_FILE" + eval "$(fuzzycd_run -c)" + _cd() { + COMPREPLY=("$PWD/tmp/one/two") + complete -F _cd cd + } + COMP_WORDS=(cd one) + COMP_CWORD=1 + COMPREPLY=() + _fuzzycd_completions + [[ "$(complete -p cd)" == *"_fuzzycd_completions"* ]] || fail "Expected fuzzycd completion to remain registered" + describe "cd DIR" it "adds it to history" cd tmp/one/two > /dev/null diff --git a/test/approve_setup b/test/approve_setup index e7c3ba0..8e0bda7 100755 --- a/test/approve_setup +++ b/test/approve_setup @@ -22,4 +22,7 @@ describe "cd is a function" describe "~/.bashrc.d contains the source directive file" [[ -f ~/.bashrc.d/fuzzycd.bashrc ]] || fail "Expected to find source directive in ~/.bashrc" +describe "~/.bashrc.d enables fuzzycd bash completions" + grep -q 'eval "$(fuzzycd -c)"' ~/.bashrc.d/fuzzycd.bashrc || fail "Expected bash startup file to enable completions" + ../uninstall From 2bf4b8a6105ef7a3109d3fbf8fb6701d18a976ed Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Thu, 12 Mar 2026 14:08:45 +0200 Subject: [PATCH 3/3] docs --- AGENTS.md | 7 ++----- README.md | 21 +++++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 30e8491..85a595e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,13 +17,10 @@ These notes apply to work inside this repository. - Useful repo commands are defined in `op.conf`. - Run them as `op COMMAND`. - Current commands include `op shellcheck`, `op shfmt`, `op codespell`, and `op test`. - -## Working Notes - - Preserve the existing shell style and test conventions. - If temporary files or directories are needed, use `./tmp` only. - Do not create temporary folders outside `./tmp`. - Approval tests may need a temporary writable `HOME` in constrained environments; if so, place it under `./tmp`. -- Completed so far: TODO item 1, making `cd -d` respect `FUZZYCD_HISTORY_FILE` and avoid the fixed `$HOME` temp file. -- Completed so far: TODO item 2, making generated completions tolerate missing `_cd` and preserve paths with spaces. +- Keep `cd -d` history rewrites tied to `FUZZYCD_HISTORY_FILE`; do not reintroduce fixed temp files under `$HOME`. +- Keep completion behavior aligned with this contract: fuzzy history matches first, native `cd` matches after, no duplicates, and preserve `cd` registration to `_fuzzycd_completions`. - Installer behavior note: Bash completions may be installed automatically for Bash startup, but not for Zsh unless a native or explicitly supported compatibility path exists. diff --git a/README.md b/README.md index 81c59ae..8962c51 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,8 @@ Fuzzy CD requires a recent version of [fzf]. ### Installing using the setup script -This [setup](setup) script will download the `fuzzycd` function to either your system -bin root, or user bin root (whichever is writable) and place a startup file -in your `.bashrc.d` or `.zshrc.d` directory. Note that it will NOT change your -startup script, so you might need to ensure that your startup directories are -sources. +Use the [setup](setup) script for automatic installation. It installs +`fuzzycd` and adds the required shell startup hook. ```shell $ curl -Ls get.dannyb.co/fuzzycd/setup | bash @@ -50,8 +47,8 @@ $ curl -Ls get.dannyb.co/fuzzycd/setup | bash ### Installing manually -1. Place the `fuzzycd` file in `/usr/local/bin/` and make it executable -2. Source it (`source /usr/local/bin/fuzzycd`) from your startup script (for example: `~/.bashrc`) +1. Place the `fuzzycd` file somewhere on your `PATH` and make it executable. +2. Source it from your startup script (for example: `source /usr/local/bin/fuzzycd` in `~/.bashrc`). ## Usage @@ -144,17 +141,17 @@ TAB: menu-complete ## Uninstall -You can either run the uninstall script: +Use the uninstall script for automatic removal: ```shell $ curl -Ls get.dannyb.co/fuzzycd/uninstall | bash ``` -Or remove manually: +Or remove it manually: -1. Remove the `source /usr/local/bin/fuzzycd` line from your `~/.bashrc`. -2. Remove the `eval "$(fuzzycd -c)"` line from your `~/.bashrc`, if you added it manually. -3. Delete `/usr/local/bin/fuzzycd`. +1. Remove the line that sources `fuzzycd` from your startup script. +2. Remove the `eval "$(fuzzycd -c)"` line from your startup script, if you added it manually. +3. Delete the `fuzzycd` executable from your `PATH`. 4. Optionally, delete the history file (`~/.fuzzycd-history`). 5. Restart your session.