I need help to remove entries from history
- [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.zshbut 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 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?
thank you for joining @cohml Unfortunately I didn't come up without a working solution yet.
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!
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
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.
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
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.
The dedicated widget by
cenk1cenk2, which makes use of the plugin frommarlonrichert, works well and might be the best solution yet for removing entries from the history file usingfzfinzsh.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 $HISTFILEtocenk1cenk2'swidget. Otherwise you may loose commands from other terminal shells that have already been added to your$HISTFILEfile.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
setting
setopt share_historyin.zshrcshould 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
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_OPTSenvironment 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 tocenk1cenk2'swidget above) usingmarlonrichert's zsh-histplugin, or mimic the deletion process with a function based on the plugin frommarlonrichert. ~~Meanwhile, the currentfzf-history-widgetshould 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
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.
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
zshinclude the official website[^4], or themanpages.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
Is there any bash version of this?
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