fzf for Flag Completion


fzf for Flag Completion

tl;dr

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 and tmux.

fzf complete flags demo

Why

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> and r 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.

How

The general idea is:

  1. Look at the command you’ve already entered. Save the first token, which is the command, as match_prefix.

    local match_prefix=$1
    
  2. Look at your history:

    fc -rl 1
    
  3. Use sed to clean up the history lines:

    • A typical line here might look something like 1234* ls -al. You can use fc -rl 1 | less to see what these commands look like.
    • We use the first pattern below to strip the _1234*_ part (note I replaced spaces with underscores to make it easier to see what it’s doing).
    • The second command is less obvious. We are stripping any line ending in \ 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:]]*$//' | \
    
  4. Pass the commands to ripgrep .

    • We only want to match those history entries for the command we’ve already typed, which we saved in match_prefix.
    • If I was better at 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
    
  5. Finally, pass the commands to gawk.

    • To get portability across linux and OSX, I used gawk instead of normal awk.
    • The function starts here , and is too long to be worth reproducing.
    • The general idea is look for things that look like flags: --foo or -x.
    • When you find one, peek ahead and see if there is a value for that flag. We want --num_servers=100 to suggest both --num_servers and --num_servers=100.
    • Same thing for -x foo. We want that to produce both -x and -x foo.
    • We need to be somewhat smart about this, though. -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.
    • Once we have these options, we check them against a dictionary. We only output the option if it wasn’t in the dictionary. This keeps us from outputting duplicates.

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

Install It

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

comments powered by Disqus