Hacker News new | past | comments | ask | show | jobs | submit login
Improving Shell Workflows with Fzf (jambor.dev)
241 points by todsacerdoti on March 30, 2021 | hide | past | favorite | 60 comments



I use fzf in this very way and could have written this article if I weren’t lazy lol. However, I can add two other use cases:

1. I use i3wm at home (haven’t yet made the jump into Sway/Wayland), and don’t want to change my screen resolution by opening an app, clicking with my mouse, and then waiting. So I use fzf for quick resolution changes using xrandr:

  cr() {
    xrandr | sed -En “s/^[ ]+([0-9]{3,4}x[0-9]{3,4}0.\*/\1/p” | fzf | xargs -I{} xrandr —output HDMI-0 —mode {}
  }
2. At work I switch between multiple Kubernetes contexts during the day. To make the switch fast without typing a sentence, I will use this helper function:

  kc () {
     kubectl config get-contexts | tail -n +2 | fzf | cut -c 2- | awk ‘{print $1}’ | xargs kubectl config use-context
  }
I found this to be better than tab-completion because with tab completion I only get the name of the context, and not the cluster/user/namespace fields that make it easier to select the right one.

Use the above at your own risk, as they are fast hack-ups and could be far more clear (my sed skills are prone to error and overly verbose).

—Edited for HN formatting


I would highly recommend looking at something like kubectx [0] to manage switching between kubernetes contexts! I've been using this for a while now and have it aliased to kx. It supports tab completion, as well as fzf integration if you have it installed [1]. There's also kubens in there which does the same for namespaces!

[0]: https://github.com/ahmetb/kubectx

[1]: https://github.com/ahmetb/kubectx#interactive-mode


Just a heads up that the quotes used in the kc function appear to be "smart quotes" and will likely throw an error if copied verbatim into your shell config


Thanks for the heads up! For some reason HN keeps mangling the post so I am not going to try to edit it again.


For something similar to your xrandr example in i3m, I prefer to use rofi, which gives you an interactive menu in the center of the screen:

Link to Rofi: https://github.com/davatorium/rofi

Examples of how I use rofi:https://adityam.github.io/linux-blog/post/rofi-selectors/


Those are very creative use cases for fzf, I had never considered doing something like that.

My usage of it probably counts as quite basic. That said, I use it constantly. I just love the feeling of real time interaction it gives - it is well and truly part of my muscle memory now.

I use the vim plugin too, which is also brilliant and makes navigating larger projects very quick and easy. I use these two extra bindings all the time:

  nnoremap <silent> <C-Space> yiw:Rg <C-r>"<CR>
  vnoremap <silent> <C-Space> y:Rg <C-r>"<CR>
They're very simple, but when combined with the ability to add search items to the quickfix list it makes for a pretty powerful workflow.

The first one in particular gives a great approximation of symbol search that 'just works' anywhere.


Yeah, the ability to fake a "show all references to this identifier" via integrated ':Rg' is the greatest feature of FZF I've encountered yet, and makes for a passable substitute of one language-server must-haves.


I must have bindings that are overriding what that should do, can you explain what <C-space> y and yiw should normally do?


Hey, sorry that I didn't really try to explain the bindings. Climb_stealth said it all really but here are some extra bits.

The first one is a normal mode binding (the n in nnoremap). yiw copies the word you are currently on (without surrounding spaces), and then <C-r>" pastes that into the :Rg command. The :Rg command is part of the fzf vim plugin and it is a wrapper around ripgrep (see the link from climb_stealth), which gives you fuzzy search over the results of a ripgrep search.

There is more detail on the <C-r>" command here:

https://vim.fandom.com/wiki/Paste_registers_in_search_or_col....

It's basically just how you paste the contents of registers into commands.

The second one is almost exactly the same except that it is a visual mode mapping (the v in vnoremap). If you highlight something with visual mode, then use that command, it will search for the highlighted text using ripgrep.

<C-Space> is just the key chord that the command is bound to. It is a pretty arbitrary personal choice, I find it a very quick and easy chord combination but you can put whatever you want in there. For example, <C-S> would activate the command when you pressed the chord 'control + s'.

There is quite a good list of keys that are unused by vim here:

https://vim.fandom.com/wiki/Unused_keys

You can pick whatever you want but I prefer not to override any of the default bindings.

Hope that helps!


Very helpful! Thank you for the great detail!


'yiw' is yank inner word. It basically copies the current word from wherever you are in that word. The pure yank 'y' I presume would just work if you already have something highlighted. It looks like these shortcuts take the current word or current selection and pass it to Rg [0] to be searched for.

I don't have the plugin but it sounds super useful and I'm going to give them a try.

[0] Rg being ripgrep, see https://github.com/BurntSushi/ripgrep


Thank you!


Junegunn, the fzf author, makes great vim plugins too. Both the user experience and the developer experience (the code of these plugins) feel just right.

Fzf is better integrated in vim than in emacs. So I use dmenu on X/wofi on wayland instead, which are basically GUI fzf replacement.


Both fzf and dmenu are "selector menu" utilities, in turn, for terminal and X, but for dmenu, fuzzy search is not its main thing. It has a fuzzy patch though...


Emacs has older alternatives that integrate and work well like Helm, ido, etc. Also fzf's author is a vimmer, so understandable.


I love fzf and tend to use it whenever repeatitive, non-static behavior is in play.

Replacing Ctrl-R with fzf is the most basic and most useful usage.

Basically every git command involving history, branches etc have fzf wrappers in my setup.

My latest, least common usage is having built a wrapper around KeepassX and keepassx-cli to access my secrets from the CLI with fzf to fuzzy search entries. Saves me a lot of time every day.

Thanks junegunn for theamazimg tool!


My FZF opts:

    export FZF_DEFAULT_OPTS='--reverse --border --exact --height=50%'
    export FZF_ALT_C_COMMAND='fd --type directory'
    export FZF_CTRL_T_COMMAND="mdfind -onlyin . -name ."
alt-c/esc-c (esc on Mac, alt everywhere else) lets me cd to any directory, ctrl-t finds files via Apple's spotlight.

The esc-c command is so freaking useful.


Alternatively, you can add fzf to the <TAB> menu and have it available anywhere that you have tab-completion: https://github.com/Aloxaf/fzf-tab

(I use this and it works great.)


This is wonderful and works better than what I expected!


I mostly use it to open files in vim [0]. The good thing about these aliases is that they keep the command in the shell history. So you open a file with the alias and it will leave a full 'vim path-to-file' in the history.

  # Open any file under current dir
  vf() {
    files="$(fzf --query="$1" --multi --select-1 --exit-0)"
    if test -n "$files"; then
        echo "${EDITOR:-vim} \"$files\""
        print -s "${EDITOR:-vim} \"$files\""
        ${EDITOR:-vim} "$files"
    fi
  }
  
  # Open from current modified files in git repository
  vfc() {
    files="$(git ls-files -m | fzf --multi --select-1 --exit-0)"
    if test -n "$files"; then
        echo "${EDITOR:-vim} \"$files\""
        print -s "${EDITOR:-vim} \"$files\""
        ${EDITOR:-vim} "$files"
    fi
  }


I also have a "create feature branch from JIRA issue" (it was in the list of ideas in this post I wrote for the engineering blog at work [1] about autocompleting EC2 instances, although I was already using it). The JIRA API is a bit too slow for fast response, so what I do is populate a text file via a cron job that requests from the API regularly, and use the text in the file for the completion.

    [1]: https://engineering.hybridtheory.com/aws/devops/2018/08/15/fzf-autocompletions/


Forgot to add that at some point I had a similar autocompleter to the one in the blog above, that would connect to N chosen machines (using the _multiple_ fzf setting), pane them in a tmux session automatically and thus allow you to send the same command to all of them. I only used it twice, but was kind of fun to write.


It sure would be cool if this post had a screenshot/screencast of what it looks like to use in practice!


If anyone is interested in a rust version (probably the main benefit being you can use it as a rust library) there is a similar tool called skim[1]

It's actively developed and seems to have a non-overlapping set of features with fzf other than the basics (they both have features the other doesn't, and skim copies most of the core ui from fzf)

[1] https://github.com/lotabout/skim


Last I heard, skim's fuzzy search was not "as good as" fzf (I don't know in what measure?)...

And there were talks of how skim could not utilize concurrency as efficiently as fzf and that has something to do with how Rust handles concurrency and how Go does it...

These were just the result of me skimming (ha!) over internet posts so I'd be curious if anyone could elaborate on them...


When I tried skim the list didn't update as "smoothly" as fzf, in response to changing input. No language bias here! (I personally use Rust but not Go) I'd be interested to know more.


Fzf can be a tremendous time-sink because its credible promise to improve your quality of life in the terminal.

One of my most-frequently-used pry customizations is a keybinding that opens the current backtrace in fzf, with the preview window showing the selected location, allowing me to quickly walk up or down the stack, with tons of context. `enter` takes me to that frame, and `ctrl-v` opens that file location in vim.


I’d love to hear more about this - is it a gdb integration?


The vim integration piece uses the same concepts as the gdb integration. The rest uses some (very ugly) local scripts, cobbled together in 10-minute snatches of time over years. Hopefully, the functionality of the pieces not included here can be inferred from the names.

https://gist.github.com/bcgraham/f88e7e500e1b933bed1c7077f86...

I don't have the time now to clean it up, unfortunately. Before you judge too harshly, let him without sin cast the first stone.


Fzf is great, but unfortunately it doesn't have line wrap (there's an open bug basically marked as WONTFIX). So if you frequently work with long commands and want to use fzf to search for older commands with Ctrl-R, things will be quite complicated :-(


There's an easy workaround. I use a keybinding such that when searching through commands with Ctrl-R I can press ? to display the the full wrapped command in a preview window at the bottom of the screen.

My Ctrl-R config is at https://gist.github.com/brbsix/5e4f18833133b3accac78e3331292... but the line below is the relevant bit:

    # View full command in preview window (?)
    export FZF_CTRL_R_OPTS="${FZF_CTRL_R_OPTS:+$FZF_CTRL_R_OPTS }--preview 'echo {}' --preview-window down:5:hidden:wrap --bind '?:toggle-preview'"


Couldn't you "one time" process your history file to remove the line wraps (and make it appear that you ran a bunch of very long commands)?


You're misreading what I'm saying, it's the other way around. fzf doesn't support line wrapping very long lines.

I have really long commands in my shell history and fzf just truncates them, which makes it pretty useless in many cases, since those commands are similar or identical up to 80-100-120 characters.

So in many situations I kind of have to guess which command has the parameter that I want at the end.

It's not that my shell history has lines with line wrap in them, it's that fzf can't show long commands on multiple lines so that I can see the entire command...


As a hack, you could do pre process to hide some of the common prefix, pass it into FZF, and then post process to add back the prefix, before finally running it.

Basically, something like:

    history | strip-prefix | fzf | add-prefix | bash
I'm not sure if it would be useful, but I think it's worth a try (and would be a fun project).


Ah that makes more sense and seems like a serious limitation


I think the first example should have been done with direnv rather than having a manual step, but I supposed to each their own. The other examples are all pretty neat.

I recently set up fasd, which I recommend, but I tied it together with fzf, which makes it really nice.

fasd: https://github.com/clvv/fasd

tie it with fzf: https://github.com/CGamesPlay/dotfiles/blob/master/files/.co...


Here's a way to search and watch Youtube without ads, using fzf, youtube-dl, mpv, and jq:

    mpv ytdl://$(youtube-dl -j 'ytsearch30:spider pig' | jq '{id,title,uploader}' --compact-output | fzf | jq -r .id)


My only constant usage pattern for this is integration with 10000 last unique lines of Redis stored history across all the systems I use. Push every unique command into a single Redis across all the systems, Ctrl-R to pull it via fzf. It really improves command line performance.


Can you tell us more about your setup? Sounds quite cool


I use bash. I'm sure zsh has something similar as hooks.

This is my setup.

PROMPT_COMMAND variable is set to a function that "history 1", pulling the last command entered. That command is piped into a command line tool that pushes a single line into a Redis list. The list is trimmed to no more than 10000 entries.

Ctrl-R is bound to a function that uses a tool to connect to Redis, fetches the list, finds uniques, and pushes them as the input to fzf. The selected command becomes a shell variable BUFFER. Finally, READLINE_LINE=${BUFFER} and READLINE_POINT=${#READLINE_LINE}, thus inserting the ${BUFFER} into the command line and moving the cursor to the end of it.

This allows me to edit the pulled command.

All systems/vms point to the same redis running on a server and using the same list key, thus allowing for a shared command line history. If the server is unreachable, the hooked functions use shell's history instead of the redis server to fetch last set commands( fewer lines, obviously ). In additional to that failure to connect to redis server switches makes the shell prompt two lines where the first line says '[Command history server unreachable/shell server cli tools not found]' and the second line becomes a standard prompt.

I have been running this for 2 years with zero issues. Initially I thought doing connect->push->disconnect will be too slow, but I'm not noticing it much, even over the Wifi. I've been thinking of converting this service into a container to add an archive of when, where and what was typed in addition to the current setup.


vaguely related I also have a redis shell prompt hack to allow changing an env var in all your shell processes:

  __set_env_vars_from_redis () {
      eval $(redis-cli --raw hgetall env |\
             awk '{i = (NR - 1) % 2; args[i] = $1; if(i == 1) { printf("export %s=%s\n", args[0], args[1]) }}')
  }
If you call that function in your shell prompt command, then you can do stuff like

  redis-cli hset env BAT_THEME 'Dracula'


fzf is also great as a live preview window for a command:

    echo '' | fzf --print-query --preview-window wrap --preview 'cat test.json | jql {q}'
Here shown with jql[0] to write json queries and see the live output.

[0]: https://github.com/cube2222/jql


You might like to also check out https://github.com/akavel/up when in need for some more complex filtering with live preview


After fiddling with Emacs, I have a persistent thought that the shell would benefit considerably from more featureful completion in the style of Helm/Ivy, instead of just a list with an occasional ‘preview’. The shell is a programming environment, after all, with all the commands and executables being the library—so we need to finally treat it as such.

For starters, I want the completion to show the arguments order for a command, and one-line description for each option. Perhaps my completion config is just incomplete, but somehow I haven't seen any howtos of this scale yet.


As the shell developer cannot know in advance which commands and programs you'll use, it becomes a problem of providing a good way for developers to provide the shell a way to inspect and complete.

Bash has a very good way of doing that. Actually Debian based distributions have very good completion scripts.

git, apt and other very used programs have good completion.

Is up to the developer.


Figure I'd link my git aliases here, that make heavy use of fzf. The goal is generally to never have to type a filename (eg. for git add) or a commit hash (eg. for cherry-pick).

Here's a link to my 'cp' alias that lets me choose a branch, then a commit to cherry pick into my current branch:

https://github.com/kbd/setup/blob/e23b3e8e2363284c3c766c0be2...


I have to read through yours which indeed look nice from a quick scan, but if your goal is firstly to save typing file paths, I presume you instead considered just having a shell mapping to do that instead of needing to instrument aliases for each command? Here's mine, which I get by hitting ctrl-s anywhere in any command line: https://github.com/Julian/dotfiles/blob/main/.config/zsh/com...


You can get similar behavior out of the box with fzf by typing `git add **[tab]`. The difference with what I do in my aliases is that I take the choices of files to add from the output of git status.


Ah, interesting -- as in fzf wants you to integrate it with your shell's tab completion by default?


Yeah https://github.com/junegunn/fzf#files-and-directories

Though I just remembered fzf maps ctrl+t by default as well (I never use it...)

https://github.com/junegunn/fzf#key-bindings-for-command-lin...


I have, after some extreme madness, slightly adapted (and I dare say improved) the github checkout one. My use case is to check out PRs that have had my review requested, so I changed the JQ command to do that (which you may or may not want), but the real masterstroke was adding in previewing the body of the PR with mdv. Key to doing that was switching to null delimited lines.

    gh api 'repos/:owner/:repo/pulls' |\
    jq -c --raw-output -j '[ .[] | select( .requested_reviewers | select(.[].login | contains("your-github-handle-here"))) ] | .[] | "#\(.number)\u0009\(.title)\u0009\(.body)\u0000"' |\
    fzf --read0 --delimiter='\t' --with-nth=1,2 --preview='echo {3} | mdv -' --preview-window=up:80% |\
    sed -n '1s/#\([0-9]\+\).*/\1/p' |\
    xargs gh pr checkout
I did my damnedest to use the unit separator character 0x1F instead of tab in between fields but I could not figure out how to get FZF to like it. It makes me so sad that those characters with absolutely primo spots in the ASCII table and that ACTUALLY MEAN what we use tabs (or commas, or pipes) and linebreaks for don't get any love. Ah well.


These are great.

I'll throw in a fifth, which is I've slowly been cobbling together a simple fuzzy CLI music player (though I use fzy rather than fzf but same idea).

Code is here: https://github.com/Julian/dotfiles/tree/main/.config/zsh/fun...

Look at say `artist` or `play` or `shuffle` which are the music ones.


I concur.

I saw this tool to find pytest cases after I implemented my own simple fzf shell function that uses bat for preview. https://github.com/dbaty/testfinder

I also implemented simple bash functions to:

+ Select a CMake target to build

Extract all add_executables defined in the CMakeLists of my project directory and pipe the fzf selection to cmake to build

+ Find and sort (by mtime) all compiled executables placed in the cmake build directory

It can be hard to remember the path of the executable produced by cmake, so i find all executable files in the cmake build dir, ls -alh, sort them by mtime (you want to execute the most recently compiled binary) and pipe to fzf

Any time I pipe something to grep, I think if this could become an fzf-based command. Highly recommend people do the same with their workflows.


I just switched my time tracking to a fzf-based solution named tmt [1], also a great use case for fzf!

[1] https://github.com/sitaramc/notes/blob/master/tmt


If you use vim, I created this plugin to manage branches using fzf https://github.com/stsewd/fzf-checkout.vim


Nice, I love fzf! It’s an amazing recursive search replacement, but my favorite use was a script I wrote for changing git branches, where they were sorted by most recently changed so that the branch you’re probably looking for is only a few lines away, but you can go back pretty far too if you want. It feels like tig but for changing branches, it’s awesome.


Some excellent scripts, I like the one with `--multi` to delete git branches.

My most powerful bash alias has become this one [1], where `subl` is the editor of my choice. I no longer search files, I find them. Really, fzf is almost indistinguishable from magic and has become crucial to my workflow.

[1] alias fzubl='fzf -m | xargs subl'


If you use fzf-vim, pressing `alt+a` in Ag or Rg mode will mark all of your matches, pressing enter will them open them all in a quickfix list.

Amazing trick, I use it frequently.


This is great. I created something vaguely similar a while back, but probably does not allow for as much complexity. https://github.com/seiferteric/clamp


Very useful thanks!

Question, I find your blog very clean, I imagine the blog is written using a static site generator? If yes which one?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: