Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I need help to remove entries from history #3522

Open
5 of 10 tasks
amigthea opened this issue Dec 2, 2023 · 14 comments
Open
5 of 10 tasks

I need help to remove entries from history #3522

amigthea opened this issue Dec 2, 2023 · 14 comments

Comments

@amigthea
Copy link

amigthea commented Dec 2, 2023

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

Info

  • OS
    • Linux
    • Mac OS X
    • Windows
    • Etc.
  • Shell
    • bash
    • 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

@cohml
Copy link

cohml commented Dec 14, 2023

@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?

@amigthea
Copy link
Author

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

@cohml
Copy link

cohml commented Dec 15, 2023

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!

@LangLangBart
Copy link
Contributor

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 this1.

the results refresh

loading the history with fc -pa $HISTFILE; fc -rl 1 23 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 zshaddhistory4 hook or HISTORY_IGNORE 5 to manage your history, rather than trying to modify the history file directly with sed.

Footnotes

  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

@amigthea
Copy link
Author

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.

@cenk1cenk2
Copy link

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

@LangLangBart
Copy link
Contributor

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.

@amigthea
Copy link
Author

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

@LangLangBart
Copy link
Contributor

LangLangBart commented Dec 29, 2023

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 works2.

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

Footnotes

  1. zsh: 16 Options History

  2. marlonrichert/zsh-hist/functions/hist

@LangLangBart
Copy link
Contributor

LangLangBart commented Dec 30, 2023

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.

FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )


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: #3629

@cohml
Copy link

cohml commented Jan 5, 2024

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.

@LangLangBart
Copy link
Contributor

LangLangBart commented Jan 6, 2024

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 history1 array to get the command, you may need to load the zsh/parameter2 module zmodload zsh/parameter3
ignore+=( ${(b)history[$i]} the (b) is a parameter flag4 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_IGNORE5 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 website6, 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
# …

Footnotes

  1. zsh: 22 The zsh/parameter Module - history

  2. zsh: 22.20 zsh/parameter

  3. zsh: 17 Shell Builtin Commands - zmodload

  4. zsh: 14 Parameter-Expansion-Flags

  5. zsh: 15 Parameters Used By The Shell - HISTORY_IGNORE

  6. zsh: documentation

@konosubakonoakua
Copy link

Is there any bash version of this?

@p1r473
Copy link

p1r473 commented May 15, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants