Been there, done that
Shell history and browser history, I could do without one but not the other! I have no shame in admitting I’m very reliant on my shell history for getting things done on the command line. Ctrl+R, augmented via fzf, has become one of many secondary brains of mine.
But there’s one curious thing about it! Shell history has no practical way of
filtering results besides string matching. That’s where
fzf comes in, it’s an amazing fuzzy finder.
The nicest part about it is that you can use it as a backend to complete just
about anything! Seriously, just pipe stuff to fzf
and you have a fast, fuzzy
completion UI for anything line-oriented.
Yet, I’d like yet another layer: semantic “bookmarking” of history entries, so that I can memorize an important command for later. What is particularly important, I want to be able to “tag” it with something concise, instead of having to rely on a fuzzy match of a part of the command to find it.
So, for instance, git commit --no-verify --amend
could be saved as git verify
amend
or something like that. To this end, I came up with a couple of commands,
bt
, short for been there, and dt
, short for done that. What they do
precisely is the following:
bt
prompts for a command in your history and asks to save it with another namedt
then recalls a command from this named history
So, for instance, if you end up writing git commit --no-verify --amend
often,
or something like that, you can write that once, then write bt
, pick that
command, and then enter git commit noverify
or some other short hand, then
pick it handily using dt
.
Using fzf’s preview capabilities, it shows the command to be executed and the description of the command in a separate preview window, so you always know what’s going to happen.
All possible with a couple of short bash functions! bt
stores data in a
simple tabulated list in your home directory, so it’s nothing
complicated. Giving an argument to bt just saves that command directly, without
prompting. You can have bt and dt too, just dump this into your .bash_profile
:
bt() {
if [[ -z "$1" ]]; then
CMD=$(history 10 | fzf --header="Choose a command to save" --no-sort --tac | cut -d' ' -f 5-)
else
CMD=$1
fi
[[ -z "$CMD" ]] && echo "Aborted" && return 2
echo "Saving \"${CMD}\"..."
read -p "Name of command: " NAME
[[ -z "$NAME" ]] && echo "No name given, aborting" && return 1
LONG=$(read -p "Longer description (optional): ")
if [[ ! -d "${HOME}/.config/btdt" ]]; then
mkdir -p "${HOME}/.config/btdt"
fi
DATA="${BTDT_DATA:-${HOME}/.config/btdt/data}"
if [[ ! -f $DATA ]]; then
touch $DATA
fi
result="${CMD}\t${NAME}\t${LONG}\n"
printf "$result" >> $DATA
}
dt() {
local data="${BTDT_DATA:-${HOME}/.config/btdt/data}"
local cmd=$(cat $data | fzf -d'\t' --with-nth 2 --preview='echo -e "\033[1m"{1}"\033[0m""\n\n"{3}' | cut -f1)
echo "${cmd}"
eval "${cmd}"
}
The cool thing about fzf’s --with-nth
argument is that it can display one
thing in the preview and return another as the result. --with-nth 2
tells it
to use the second element that it’s filtering, so applying that to
foo\tbar\tbaz
would return bar
, but the preview elements {1}
and {3}
will contain foo
and baz
respectively, and will be shown to the user in the
preview.
I tried if something like this already existed, but I couldn’t find anything. That’s weird, this can’t be a truly original idea, can it? I think there are graphical tools for this purpose, but this is about 20 lines of bash, so it’s relatively straightforward.
I’m not the most experienced bash programmer, so I don’t know if eval
is the
right way to “evaluate something out of a pipe”, but hey, it seems to work! It
only works with bash, and you’ll need fzf, and I have no idea if it works with
other shells, so suggestions are
welcome.