I wrote an fzf
extension that completes flags for the current command from
your history. This makes it easy to build invocations with lots of flags, or to
remember the spelling of a rarely used flag.
You can find the code here .
This post assumes you already know about fzf and have it installed. If you don’t, a lot of this might be confusing. It is incredibly useful, and is as valuable to my workflows as
vim
andtmux
.
In the default
fzf
configuration, <c-r>
searches your history. It is a huge improvement over the default reverse history
search.
Note that in this post
<c-r>
means hitting<ctrl>
andr
at the same time.
At work we have a script that has dozens of flags. I’ve used probably 20-30 of them at different times. On a single invocation I’ll need to pick and choose a subset of those flags. Maybe building up something like this:
$ cmd-with-logs-of-flags \
--use_magic \
--foo \
--bar \
--num_servers=100
Because it’s a custom command, I can’t benefit from the zsh
tab completion.
And because I haven’t yet memorized the flags, every time I wanted to run it I
had to open up a new tmux
pane and less
the cmd-with-logs-of-flags
source
file to see what was a valid flag. Gross.
At first I suffered along using <c-x> <c-e>
. That will open your editor with
your current command. You can edit to your heart’s content. Save, quit, and the
command will be updated. Slightly less gross, as now you don’t have to retype
the whole command if you mess up the shell syntax.
To make things even easier, I wrote a zsh
plugin that uses fzf
to complete
previously used flags from your history. As you can see in the gif above, now
all I have to do is type cmd-with-logs-of-flags
, hit <c-q>
, then I can
complete flags to my heart’s content.
This has been a huge time saver. Not only is cmd-with-logs-of-flags
less
annoying to use, but it’s also been useful for flags that I tend for forget.
When writing this article I was using hugo server
. I used <c-r>
to get me
started, but then I needed the flag to build draft posts, not just published
posts. I remembered it was something like --build-drafts
. <c-q>
showed me
that in fact it was --buildDrafts
.
The general idea is:
Look at the command you’ve already entered. Save the first token, which is
the command, as match_prefix
.
local match_prefix=$1
Look at your history:
fc -rl 1
Use sed
to clean up the history lines:
1234* ls -al
. You can use
fc -rl 1 | less
to see what these commands look like._1234*_
part (note I replaced
spaces with underscores to make it easier to see what it’s doing).\
or \_
. This only becomes a problem where you’ve typed multiline
commands, where \
is the line continuation character. This is needed for
a later step in the process, where the slashes confuse a while
in the
shell. (Or at least I think that’s what is happening. Without stripping
those slashes, the flag doesn’t make it back to the prompt.)sed -E -e 's/^[[:space:]]*[0-9]*\*?[[:space:]]*//' -e 's/\\+[[:space:]]*$//' | \
Pass the commands to ripgrep .
match_prefix
.awk
, you could no doubt skip this step and just use
awk
to do the matching. Keeping it as-is makes it easier for me to
reason about.rg "^${match_prefix}" --color=never --no-line-number
Finally, pass the commands to gawk
.
gawk
instead of normal
awk
.--foo
or -x
.--num_servers=100
to suggest both --num_servers
and
--num_servers=100
.-x foo
. We want that to produce both -x
and -x foo
.-x foo
is a flag (-x
)
and an argument (foo
). With -v --num_servers=100
, we don’t want to
think to think that --num_servers=100
is a value for the -v
flag.The rest of the file is integrating with fzf
. For the most part here I just
copied the setup code from other fzf
commands. There are a couple things worth
nothing, though.
First, <c-q>
normally already does something in the shell. It is the
counterpart to the <c-s>
command. If you don’t already know what that does,
odds are that at some point you’ve accidentally hit <c-s>
, then wondered why
your terminal froze. Normally you use <c-q>
to unfreeze your prompt. This is a
useless feature in modern shells, as far as I can tell, so I’ve disabled <c-s>
by putting this in my zshrc
:
stty -ixon
That frees up <c-q>
so that, as far as I know, it’s not bound to anything.
Second, we need to operate on the text that has been entered in the command. If
I type hugo serve <c-q>
, we need to be able to see that hugo
is the first
thing we typed.
In zsh
functions, that is normally saved in the BUFFER
variable. I took
this approach at first, but it broke for multiline commands. In this scenario,
for example, ${BUFFER}
wouldn’t be populated correctly:
$ hugo \
server <c-q>
On multiline commands, zsh
puts
the
first part of the command in PREBUFFER
, and it sets CONTEXT=cont
to let us
know that the part we care about will actually be in the ${PREBUFFER}
variable. We use that to know where to look:
local buffer_with_start_of_cmd=${BUFFER}
if [[ $CONTEXT == "cont" ]]; then
buffer_with_start_of_cmd=${PREBUFFER}
fi
You’ll need zsh
. Make sure gawk
and rg
are on your path.
Clone the
repo
and source it. I
have this in my zshrc
:
# Make sure you clone this first on new installations:
# git clone https://github.com/srsudar/fzf-complete-flags ~/.zsh/fzf-complete-flags
if [ ! -f ~/.zsh/fzf-complete-flags/fzf-complete-flags.zsh ]; then
echo "fzf-complete-flags.zsh not found--did you clone the repo?" >&2
else
source ~/.zsh/fzf-complete-flags/fzf-complete-flags.zsh
fi