fzf icon indicating copy to clipboard operation
fzf copied to clipboard

I need help to remove entries from history

Open amigthea opened this issue 2 years ago • 15 comments

  • [X] I have read through the manual page (man fzf)
  • [X] I have the latest version of fzf
  • [X] I have searched through the existing issues

Info

  • OS
    • [X] Linux
    • [ ] Mac OS X
    • [ ] Windows
    • [ ] Etc.
  • Shell
    • [ ] bash
    • [X] zsh
    • [ ] fish

Problem / Steps to reproduce

For some days now, I'm trying to implement a shortcut into the CTRL-R function, unfortunately to no avail. What I'm trying to achieve is the ability to remove commands from history (enabling multiselection too), within the fzf view; refreshing the fzf results once done.

This is the farthest I could go:

~/.zshrc export FZF_CTRL_R_OPTS="--bind=\"ctrl-d:execute-silent(awk '{print \$1}' {+f1..} | while read -r linenum; do sed -i "\${linenum}d" "$HISTFILE"; done),ctrl-d:+refresh-preview\" --header \"CTRL-D to remove command from history\""

it can successfully delete the highlighted command from history file but:

  • multiselection do not work (I even enabled -m in /usr/share/fzf/key-bindings.zsh but it still remove the first selected element only)
  • I don't know how to implement the results refresh (I should add something like ctrl-d:+reload(command here)?

Thank you, amazing piece of software

amigthea avatar Dec 02 '23 12:12 amigthea

@amigthea Have you seen #2800, or perhaps this discussion?

Neither does what you want right out of the box, but might give you some ideas.

I am interested in creating something similar for myself - the ability to, from within CTRL-R, use some keybinding to dynamically delete one or more history entries and have the output refresh in real time.

If I'm successful I'll try to share my implementation here. Or if you beat me to it, perhaps you could do the same?

cohml avatar Dec 14 '23 16:12 cohml

thank you for joining @cohml Unfortunately I didn't come up without a working solution yet.

amigthea avatar Dec 15 '23 14:12 amigthea

Here is a tool I just found which may help a lot: https://github.com/marlonrichert/zsh-hist

Please check back in with your implementation if you get something working!

cohml avatar Dec 15 '23 19:12 cohml

ability to remove commands from history

sed -i "${linenum}d" "$HISTFILE"

The idea of removing entries based on their number may not be as straightforward as it seems, given that a history entry can span multiple lines, or can have blank lines. Consequently, the line number you see may not be a reliable indicator to work with.

In bash, it appears much simpler to use history -d <num> to remove an entry. However, zsh does not provide a straightforward solution to accomplish this[^1].

the results refresh

loading the history with fc -pa $HISTFILE; fc -rl 1 [^2][^3] will work


There are limited options that you can assign to FZF_CTRL_R_OPTS, besides a few unstable hacks and extensive workarounds.

It's generally safer and more efficient to use built-in zsh features like a zshaddhistory[^4] hook or HISTORY_IGNORE [^5] to manage your history, rather than trying to modify the history file directly with sed.

[^1]: Re: how to clean a history entry? [^2]: Delete history entry while browsing matches menu · junegunn/fzf · Discussion #2800 · GitHub [^3]: zsh: 17 Shell Builtin Commands [^4]: Re: How to (properly) remove the last entry from history with command_not_found_handler [^5]: Re: Deleting entries in history

LangLangBart avatar Dec 25 '23 06:12 LangLangBart

thank you for your contribution @LangLangBart the bash history -d <num> grief was real when I saw that, it would have been so simple if zsh had the same. I agree with your analisys, there're only ugly workaround to achieve that and that's the reason I abandoned the idea at all, at least for now.

amigthea avatar Dec 25 '23 10:12 amigthea

Here is a tool I just found which may help a lot: https://github.com/marlonrichert/zsh-hist

Please check back in with your implementation if you get something working!

Additional to the marlonrichert's plugin, I use this as follows albeit it is not exactly what you want since it is assigned to a different key instead of reusing the CTRL+R.

fzf-delete-history-widget() {
    local selected num
    setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
    local selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --multi --bind 'enter:become(echo {+1})'" $(__fzfcmd)) )
    local ret=$?
    if [ -n "$selected[*]" ]; then
      hist delete $selected[*]
    fi
    zle reset-prompt
    return $ret
}

zle     -N            fzf-delete-history-widget
bindkey -M emacs '^H' fzf-delete-history-widget
bindkey -M vicmd '^H' fzf-delete-history-widget
bindkey -M viins '^H' fzf-delete-history-widget

cenk1cenk2 avatar Dec 28 '23 17:12 cenk1cenk2

The dedicated widget by cenk1cenk2, which makes use of the plugin from marlonrichert, works well and might be the best solution yet for removing entries from the history file using fzf in zsh.

The only issue I've noticed is that when you run a command in other shells and then return to your original shell, pressing the assigned key to open the widget won't display the entries from the history file. Instead, it only shows entries from your current shell history. To resolve this, you may need to add fc -pa $HISTFILE to cenk1cenk2's widget. Otherwise you may loose commands from other terminal shells that have already been added to your $HISTFILE file.

 fzf-delete-history-widget() {
     local selected num
     setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
+    fc -pa $HISTFILE
     local selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
 FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --multi --bind 'enter:become(echo {+1})'" $(__fzfcmd)) )
     local ret=$?

One might consider closing this issue, as there is no actual bug in fzf. It's more of a question for the zsh user forum.

LangLangBart avatar Dec 28 '23 20:12 LangLangBart

The dedicated widget by cenk1cenk2, which makes use of the plugin from marlonrichert, works well and might be the best solution yet for removing entries from the history file using fzf in zsh.

The only issue I've noticed is that when you run a command in other shells and then return to your original shell, pressing the assigned key to open the widget won't display the entries from the history file. Instead, it only shows entries from your current shell history. To resolve this, you may need to add fc -pa $HISTFILE to cenk1cenk2's widget. Otherwise you may loose commands from other terminal shells that have already been added to your $HISTFILE file.

 fzf-delete-history-widget() {
     local selected num
     setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
+    fc -pa $HISTFILE
     local selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
 FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --multi --bind 'enter:become(echo {+1})'" $(__fzfcmd)) )
     local ret=$?

One might consider closing this issue, as there is no actual bug in fzf. It's more of a question for the zsh user forum.

setting setopt share_history in .zshrc should achieve the same, even outside fzf, if I recall correctly

amigthea avatar Dec 29 '23 13:12 amigthea

setting setopt share_history in .zshrc should achieve the same, even outside fzf, if I recall correctly

Open one terminal, let's call it ALPHA, then open another terminal, let's call it BETA. Type an arbitrary command into BETA, for example, echo "Hello World". If you switch back to your ALPHA terminal and press the key (⌃ Control + H) to open cenk1cenk2's original widget, you won't see the command from the BETA terminal.

However, if you examine your $HISTFILE, the command will appear there (tail $HISTFILE), assuming you've configured the appropriate settings to share your history across shells [^1].

The issue lies in this line: $(fc -rl 1 | …). It only reads the current shell session history, so regardless of the option you set, it will never read the global history. This is where fc -pa $HISTFILE comes in. It pushes all the elements from your $HISTFILE onto a stack and uses this history instead of your session history. This way, we can see events from other terminal sessions, for example, from our BETA terminal. When the current function ends, this history list will be automatically removed.

The real problem arises when you don't use fc -pa $HISTFILE. If you select some commands to delete, you'll notice that your command from the BETA terminal is no longer listed in your $HISTFILE. This is due to how the widget from marlonrichert/zsh-hist works[^2].

The deletion is done by adding all commands to be removed to the HISTORY_IGNORE. The current shell history list is then written to the history file using fc -W. As a result, all commands assigned to HISTORY_IGNORE are excluded from the history file. However, since this method only uses the current shell history, it fails to capture the command from our BETA shell, unless we used fc -pa $HISTFILE.

You can try this in your shell. The session history will contain the echo 5 command, but it won't be found in your $HISTFILE.

HISTORY_IGNORE=(echo 5)
echo 5
# 5

history -1
# 5985  echo 5

tail -3 $HISTFILE
# HISTORY_IGNORE=(echo 5)
# history -1
# tail -3 $HISTFILE

[^1]: zsh: 16 Options History [^2]: marlonrichert/zsh-hist/functions/hist

LangLangBart avatar Dec 29 '23 22:12 LangLangBart

Adding entries to the HISTORY_IGNORE was the key missing idea to make it work.

The following allows you to delete a selected entry from your history file using the ⌃ Control + D key. The list updates immediately afterward.

[!IMPORTANT] Ensure your environment variable 'HISTFILE' is assigned and exported, e.g. export HISTFILE="${ZDOTDIR:-$HOME}/.zsh_history"

export FZF_CTRL_R_OPTS="$(
	cat <<'FZF_FTW'
--bind "ctrl-d:execute-silent(zsh -ic 'fc -pa $HISTFILE; for i in {+1}; do ignore+=( \"${(b)history[$i]}\" );done;HISTORY_IGNORE=\"(${(j:|:)ignore})\";fc -W')+reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) print $0 }'"
--bind "start:reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) print $0 }'"
--header 'enter select · ^d remove'
--height 70%
--preview-window 'hidden:down:border-top:wrap:<70(hidden)'
--prompt ' Global History > '
--with-nth 2..
FZF_FTW
)"
Colored version

This version does the same as above, but colorize the lines with bat.

# The awk command removes duplicates, and aligns numbers from left to right.
# This alignment is necessary for the 'bat' function to colorize the output correctly while
# maintaining proper field index expression.
export FZF_CTRL_R_OPTS="$(
	cat <<'FZF_FTW'
	--ansi
--bind "ctrl-d:execute-silent(zsh -ic 'fc -pa $HISTFILE; for i in {+1}; do ignore+=( \"${(b)history[$i]}\" );done;
	HISTORY_IGNORE=\"(${(j:|:)ignore})\";fc -W')+reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) {printf \"%-10s\", $1; $1=\"\"; print $0} }' |
	bat --color=always --plain --language sh"
--bind "start:reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) {printf \"%-10s\", $1; $1=\"\"; print $0} }' |
	bat --color=always --plain --language sh"
--header 'enter select · ^d remove'
--height 70%
--preview-window 'hidden:down:border-top:wrap:<70(hidden)'
--preview 'bat --color=always --plain --language sh <<<{2..}'
--prompt ' Global History > '
--with-nth 2..
FZF_FTW
)"

The reason you can only select one is because the +m flag, which disables multi-select, is added after all other fzf environment variables within the fzf-history-widget.

https://github.com/junegunn/fzf/blob/8d20f3d5c4e2645e623ed43487501dfe76a86b98/shell/key-bindings.zsh#L102-L102


UPDATE: ~~It's amazing that this actually worked.~~

[!CAUTION] Assigning values to the FZF_CTRL_R_OPTS environment variable can solve the deletion part, but not always the correct selection. This is because the retrieved number for a selection can diverge, leading to the incorrect history entry being returned. The optimal solution is to create a dedicated widget for deletion (refer to cenk1cenk2's widget above) using marlonrichert's zsh-hist plugin, or mimic the deletion process with a function based on the plugin from marlonrichert. ~~Meanwhile, the current fzf-history-widget should remain unchanged.~~

deletion function
hist_delete_fzf() {
  local +h HISTORY_IGNORE=
  local -a ignore
  fc -pa "$HISTFILE"
  selection=$(fc -rl 1 |
  		awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++)  print $0}' |
  		fzf --bind 'enter:become:echo {+f1}')
  if [ -n "$selection" ]; then
  	while IFS= read -r line; do ignore+=("${(b)history[$line]}"); done < "$selection"
  	HISTORY_IGNORE="(${(j:|:)ignore})"
  	# Write history excluding lines that match `$HISTORY_IGNORE` and read new history.
  	fc -W && fc -p "$HISTFILE"
  else
  	echo "nothing deleted from history"
  fi
}

UPDATE2: A complete version: https://github.com/junegunn/fzf/discussions/3629

LangLangBart avatar Dec 30 '23 01:12 LangLangBart

This thread has suddenly become a work of art. You all are absolute legends! Especially @LangLangBart, whose beautifully formatted comments are a joy to read.

Anyway, one semi-involved followup question: My knowledge of zsh syntax and function isn't very deep. What exactly is happening in these complex keybindings (copied from an earlier message)?

--bind "ctrl-d:execute-silent(zsh -ic 'fc -pa $HISTFILE; for i in {+1}; do ignore+=( \"${(b)history[$i]}\" );done;HISTORY_IGNORE=\"(${(j:|:)ignore})\";fc -W')+reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) print $0 }'"
--bind "start:reload:fc -pa $HISTFILE; fc -rl 1 |
	awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, \"\", cmd); if (!seen[cmd]++) print $0 }'"

If anyone would be kind enough to explain clause by clause what is happening in each, that would be immensely helpful in helping others customize these keybindings to their taste.

Many thanks in advance.

cohml avatar Jan 05 '24 17:01 cohml

What exactly is happening in these complex keybindings …

term description
fc -pa $HISTFILE pushes all the elements from your $HISTFILE onto a stack to use this history
for i in {+1} loops over the selected lines
{+1} the + is a fzf placeholder expression flag providing a space-separated list of the selected lines
{+1} the 1 provides the first field, in this case, the event numbers, e.g., 7205, 7204,…
ignore+=( ${(b)history[$i]} uses the event number on the associative history[^1] array to get the command, you may need to load the zsh/parameter[^5] module zmodload zsh/parameter[^6]
ignore+=( ${(b)history[$i]} the (b) is a parameter flag[^2] used to backslash special chars
${(j:|:)ignore} (j:<string>:) joins every element in the array together using a string as a separator
(${(j:|:)ignore}) wraps everything into parentheses as required by the HISTORY_IGNORE[^3] for multiple patterns
fc -W writes the history, excluding all commands assigned to HISTORY_IGNORE

[!TIP] Quick ways to find the meaning of something in zsh include the official website[^4], or the man pages.

for i in $manpath; do find $i -name 'zsh*' | xargs grep -l HISTORY_IGNORE; done
# /usr/share/man/man1/zshparam.1
man zshparam
# …

[^1]: zsh: 22 The zsh/parameter Module - history [^2]: zsh: 14 Parameter-Expansion-Flags [^3]: zsh: 15 Parameters Used By The Shell - HISTORY_IGNORE [^4]: zsh: documentation [^5]: zsh: 22.20 zsh/parameter [^6]: zsh: 17 Shell Builtin Commands - zmodload

LangLangBart avatar Jan 06 '24 02:01 LangLangBart

Is there any bash version of this?

konosubakonoakua avatar Mar 27 '24 09:03 konosubakonoakua

Made this into a ZSH plugin for anyone to use https://github.com/p1r473/zsh-hist-delete-fzf/

I encourage someone to try and do this with zsh-hist as I cant figure it out

p1r473 avatar May 15 '24 03:05 p1r473