[HN Gopher] Writing simple tab-completions for Bash and Zsh
       ___________________________________________________________________
        
       Writing simple tab-completions for Bash and Zsh
        
       Author : lihaoyi
       Score  : 209 points
       Date   : 2025-08-10 09:50 UTC (13 hours ago)
        
 (HTM) web link (mill-build.org)
 (TXT) w3m dump (mill-build.org)
        
       | lihaoyi wrote:
       | I wrote this, hope everyone finds it as interesting reading this
       | as I did figuring this out for the first time!
        
         | oezi wrote:
         | Thanks, for the interesting read.
        
         | tetha wrote:
         | It's a good first dive into zsh completion. The whole thing is
         | quite the large system to wrap ones head around it and I'm
         | still somewhat struggling.
         | 
         | But at work, I've been slowly adding auto completion to our
         | ansible wrapper scripts, like explanations which playbooks to
         | use when, smart `-l` completion based off a possibly selected
         | playbook (so, if the playbook is postgres.yml, it doesn't
         | suggest mariadb groups), tag autocompletion (with a few,
         | admittedly, hardcoded explanations how these tags should be
         | used) and such.
         | 
         | It's somewhat of a friday-afternoon struggle project, but it's
         | making the big ansible project pretty approachable to use.
        
           | imcritic wrote:
           | Could you share how you do it? Ansible playbooks are ran via
           | command `ansible-playbook` command and it surely has its own
           | tab auto completion script.
        
         | bbkane wrote:
         | Thanks for sharing! I hope to incorporate your bash completion
         | ideas into my CLIs (I've already got zsh completions).
         | 
         | Instead of sourcing the zsh completion script on every startup,
         | you can install it into somewhere on $fpath and zsh will
         | "compile" and cache the completions. This can really speed up
         | shell startup time, but of course is harder to set up. Users
         | have to understand $fpath to put the completions there.
         | 
         | I distribute my CLIs via Homebrew which can install completions
         | automatically.
        
       | oezi wrote:
       | Isn't there a standard flag which programs can implement to avoid
       | writing this bash script?
       | 
       | Ideally this could all be part of a library such as argparse for
       | typical cases, right?
        
         | vcdimension wrote:
         | In zsh you can use the _gnu_generic function for simple
         | completion of commands with a --help flag. Just put a line like
         | this somewhere in your startup file: compdef _gnu_generic <CMD>
        
           | cb321 wrote:
           | _gnu_generic is fantastic. I use it all the time. If your CLI
           | toolkit emits colorized help, but skips said colorization if
           | NO_COLOR[1] is set then you can prefix _gnu_generic with
           | NO_COLOR=1 as in https://github.com/c-blake/cligen/wiki/Zsh-
           | completion-for-cl... for the cligen Nim CLI toolkit.
           | 
           | A similar thing in Bash is `complete -F _longopt YourCmd`,
           | but these will not work with "multi-commands" that have "sub-
           | commands" as the article of this thread covers. Truth is, as
           | non-standard as GNU long opts already are, how subcommands
           | work is even more non-standard (are there even global
           | options? Or between each subcommand? Is it how Python does
           | it? Or Go? Or some one specific library or ..?)
           | 
           | [^1]: https://no-color.org/
        
         | mnahkies wrote:
         | I've wondered this as well - it would sure be nice if there was
         | a standard --completion or something that common argument
         | parsing libraries could automatically implement for us (much
         | like they often implement automatic help text)
        
         | duckerude wrote:
         | Rust has the clap_complete package for its most popular arg
         | parsing library: https://crates.io/crates/clap_complete
         | 
         | ripgrep exposes its (bespoke) shell completion and man page
         | generation through a --generate option: rg --generate=man, rg
         | --generate=complete-bash, etcetera. In xh (clap-based) we
         | provide the same but AFAIK we're the only one to copy that
         | interface.
         | 
         | Symfony (for PHP) provides some kind of runtime completion
         | generation but I don't know the details.
        
       | bravesoul2 wrote:
       | Do many people do bash on osx or zsh on Linux, and would this
       | make much of a difference?
        
         | homebrewer wrote:
         | I don't know about "use" -- luckily, there's no opt-out
         | telemetry -- but enough of "enthusiast distribution" users who
         | have also opted in (very biased sample) have explicitly
         | _installed_ zsh (not necessarily run it)
         | 
         | https://pkgstats.archlinux.de/compare/packages#packages=bash...
         | 
         | OTOH, it's only 4-7% on Debian (also opt-in):
         | 
         | https://qa.debian.org/popcon.php?package=zsh
        
           | bravesoul2 wrote:
           | I use zsh at work and bash at home. I am such an
           | unsophisticated user that I haven't noticed a real
           | difference! Other than I can install ohmyzsh on zsh.
        
             | bbkane wrote:
             | I repeat the different variations of the same command so
             | often that I get a large quality of life improvement by
             | making that easier- that's why I take the trouble to
             | install zsh.
        
           | yonatan8070 wrote:
           | Why does this type of statistic require an opt-in solution?
           | Can't the Arch mirrors just tally requests for each package
           | without identifying information like IP adddress?
        
       | vcdimension wrote:
       | Here's another tutorial for creating zsh completers using the
       | built-in functions: https://github.com/vapniks/zsh-
       | completions/blob/master/zsh-c...
        
       | wiseowise wrote:
       | Shell syntax is the exact reason why we've needed LLMs in the
       | first place.
        
         | camdroidw wrote:
         | You mean Unix shell syntax. Powershell has got this absolutely
         | right ,and only this (which is probably still a 50% of what a
         | shell is)
        
           | pastage wrote:
           | Can you expand on why it is good I have never read any good
           | evangelisation for it. I can not stand PowerShell the syntax
           | is backward, least important information first. It is also
           | very slow for me but I have understood that it is a pebkac
           | issue. So I am open to be corrected.
        
             | vips7L wrote:
             | I think they're talking about powershells programming
             | syntax which is much more sane than any Unix shell. There's
             | way less footguns and everything is typed. You're not
             | dealing with raw strings for everything.
             | 
             | If you don't like the Verb-Noun nonsense I'd encourage you
             | to look at the default aliases as they make everything a
             | lot less verbose. For example Where-Object is just "where"
             | or getchild-item is default aliased to ls and gci.
             | 
             | Id encourage you to look at NuShell as well since it is
             | mostly the same philosophy as PowerShell.
        
         | anthk wrote:
         | Perl solved that 20 years ago.
         | 
         | And `rc` under plan9/9front did a Unix shell better than the
         | classic Unix itself.
        
       | homebrewer wrote:
       | With fish, if the program you're interested in hasn't betrayed
       | the decades-old tradition of shipping man pages, it's often as
       | simple as running `fish_update_completions`.
       | 
       | It parses all man pages on your system and generates completion
       | files for you. By default, they go into
       | ~/.cache/fish/generated_completions/*
       | 
       | If the man page was written poorly/is missing, you can always
       | write your own completion (and hopefully send it upstream). fish
       | uses such a simple format that I don't think there's any need for
       | tutorials save the official doc:
       | 
       | https://fishshell.com/docs/current/completions.html
       | 
       | For example, here's an excerpt from curl                 complete
       | --command curl --short-option 'L' --long-option 'location'
       | --description 'Follow redirects'        complete --command curl
       | --short-option 'O' --long-option 'remote-name' --description
       | 'Write output to file named as remote file'
        
         | btreecat wrote:
         | When I screen share, people don't realize I'm not using zsh and
         | dozen plugins. It's just fish and it's beautiful out of the
         | box.
        
         | kekebo wrote:
         | Thank you for the comment. https://github.com/umlx5h/zsh-
         | manpage-completion-generator appears to adapt this to ZSH. Have
         | yet to try through
        
         | cb321 wrote:
         | For those programs that _have_ betrayed shipping man pages,
         | instead say relying only on a --help system, do you happen to
         | know if the fish shell has an analogue to Zsh `_gnu_generic`
         | and Bash `complete -F _longopt`? If not, do you have any
         | insight into why not /what it would take to make that happen?
        
           | nikita2206 wrote:
           | The OP mentions them in the last part of their comment, there
           | is `complete ...` commands for registering completions
        
             | cb321 wrote:
             | It sounds like you don't know how the Bash/Zsh ideas I
             | mentioned work. They run the command with --help, parse the
             | output, and from that generate the completions, wiring them
             | in to the completion system. That method is a zero config
             | solution (well, you might need a list of such "command-
             | names only" - no option names, which could change at any
             | time -- so maybe "minimal config"). The OP & you mention a
             | much heavier config solution which strikes me as against
             | the vibe of Fish in general which is, supposedly, out-of-
             | the-box niceness.
        
           | mr_mitm wrote:
           | At least for a subset of Python CLI programs, I wrote this:
           | https://github.com/AdrianVollmer/pycompgen
           | 
           | Still in an early stage, but it should work.
        
         | jnpnj wrote:
         | oh wow, it's parsing all 9461 man pages on my arch install, for
         | a cute total of 13MB
         | 
         | thanks a lot
        
         | sanewombat wrote:
         | It's surprising that on OpenSUSE `zypper search fish-
         | completion` returns more than 200 packages. Something is fishy
         | here.
        
           | OptionOfT wrote:
           | That is to get live data in your completions.
           | 
           | Say you have a widget that has 3 commands: list, start and
           | stop.
           | 
           | With one of those completion files widget stop <tab> will
           | show you a tab-able list of widgets which are running.
        
         | IgorPartola wrote:
         | man pages are so underrated. I mean every project nowadays has
         | README.md so I don't see why we can't just auto generate them
         | with or without an LLM helping. Also I wish programs would use
         | standardized generic arguments for help, config file, version,
         | background the task, PID file, log file, and log level.
        
         | yencabulator wrote:
         | I'll switch to fish after it stops expanding `car TAB` to
         | `blkdiscard` when I don't have `cargo` in path. Non-prefix
         | completion for commands is plain evil.
        
           | dingnuts wrote:
           | I bet this is configurable but I wanted to say that this is
           | totally personal preference; I have the exact opposite
           | opinion. Prefix only matching requires much more tab slapping
           | in my experience.
        
             | yencabulator wrote:
             | I believe you would lose that bet. I look every few years
             | and I don't see it
        
               | natebc wrote:
               | interestingly this also seems to have come up pretty
               | recently in their discussions.
               | 
               | https://github.com/fish-shell/fish-
               | shell/discussions/11670#d...
               | 
               | > I'm not sure that subsequence matching ever produces
               | results that people expect (I feel like we've discussed
               | this before but haven't had time to go digging)
               | 
               | No solution, but the discussion seems positive, from a
               | maintainer too.
        
               | thiht wrote:
               | I gave up on fish after a few weeks because of a similar
               | preference issue where the fish maintainers flat out
               | refused to make a bad default configurable:
               | https://github.com/fish-shell/fish-shell/issues/8618
               | 
               | > We don't really do config flags like this, as a
               | philosophical point.
               | 
               | > I would be against accepting such a PR. I do not
               | believe this should be changed.
               | 
               | I understand that they want to keep the list of configs
               | short and manageable, but it means that's not a tool for
               | me. I'm all for good, opinionated defaults but I want to
               | be able to make some changes if I want to.
               | 
               | They apparently reconsidered and implemented the change
               | since migrating fish to rust though.
        
           | wpm wrote:
           | I'll switch to fish when it comes preinstalled on all of the
           | computers I use so I can write scripts in it.
        
             | yencabulator wrote:
             | I already avoid bash scripting so I lose very little. Shell
             | scripting beyond throwaway one-liners is a problem not a
             | solution.
             | 
             | (Well that and all my machines come from the same NixOS
             | configs.)
        
               | nothrabannosir wrote:
               | Ha that's funny, considering nixos is 99% stdenv which is
               | one of the worst bash monstrosities in existence, and
               | drives people ever further into the swamp of bash. (Ever
               | tried to debug stdenv setup hooks? I still have water
               | damage from the tears.)
               | 
               | I have personally embraced the insanity but let's not kid
               | ourselves about nixos basically just being three bashes
               | in a trench coat.
               | 
               | Basically https://xkcd.com/224/ , but s/lisp/nix/
               | s/perl/bash/
        
       | camdroidw wrote:
       | Why doesn't someone (not me) just build a basic DSL and a
       | transpiler that does this?
        
         | cb321 wrote:
         | The answer to your question is that command-lines have a much
         | larger diversity of syntax ( _even to get help_!) than most
         | people realize. Folks have their 30..60 commands they run
         | frequently and don 't run into many or conveniently
         | forget/neglect older ones like `gcc` or `tar` or `dd`. Many
         | people (not saying _you_ specifically) do not even realize that
         | double-dash long options are a GNU extension never standardized
         | or that Python toolkits typically allow --my-opt for --my-
         | option abbreviations, just to name a couple of the dozen
         | variations (space or  '=', or ':' or '/' or any of the above or
         | etc., etc.). There are probably hundreds if not thousands of
         | syntax possibilities, but people often act like there is only
         | one.
         | 
         | As an example of diversity estimation that you can try at home,
         | a couple of times I have run every single command in my command
         | search PATH with --help </dev/null >/tmp/help.$c 2>&1 . Caution
         | - be careful if you do this! Have backups/checksums of
         | everything important and run as an unprivileged user. I always
         | have to kill off several processes that just hang doing
         | something or otherwise manually intervene. Anyway, this alone
         | suggests data collection of help text is not a trivial problem.
         | 
         | Beyond data collection, many commands did not/do not use CLI
         | toolkits at all. Their commands may have even less regular
         | syntax. Freeform help makes it harder to produce a regular help
         | syntax to convert into the interpreter needed by a completion
         | system. That said, as elsethread commented for some toolkits
         | the Zsh _gnu_generic works great! It essentially _IS_ the
         | "automagic" system you might want, just for a highly restricted
         | circumstance.
         | 
         | Any CLI toolkit itself does _have_ the data, by necessity. So,
         | if the CLI framework supports the 2 or 3 common shells there is
         | no need for a translator exactly. You just need a code
         | generator. There is a stab at an auto-generation framework from
         | said data for the Nim CLI toolkit, cligen, over at:
         | 
         | https://github.com/c-blake/cligen/blob/master/util/complgen....
         | 
         | but it only works for Zsh right now. Anyway, I don't think
         | perfect should be the enemy of the good or anything like that,
         | but you seemed to ask an earnest "why" question and these are
         | some of the complexities.
        
           | camdroidw wrote:
           | Quickly skimming your code it seems like that's what I was
           | asking for? basically you enter your auto completions without
           | knowing the bash syntax?
           | 
           | As for the 30..60 commands, I use the following "tricks"
           | 
           | 1) my ctrl-p is mapped in a way that it searches the prefix
           | into history. (On phone right now)
           | 
           | 2) long history which syncs with other machines in a machine-
           | name-suffixed file via syncthing
           | 
           | 3) justfile which are really a game changer. I used bare j to
           | tell me what commands are available (as opposed to running
           | the first command) and I basically know everything I can and
           | do do in that folder
           | 
           | 4)
        
         | bbkane wrote:
         | People do!
         | 
         | See: https://pixi.carapace.sh/ or
         | https://github.com/withfig/autocomplete
         | 
         | It's still a hard problem as lots of tools format --help
         | differently. One of the things I'm jealous in Poweshell is
         | their standardized completions
        
       | derriz wrote:
       | I feel that the ergonomics of bash completion took a hit as the
       | configurations got "smarter" and "helpfully" started blocking
       | file or directory name completion if it thinks it wouldn't be
       | appropriate to have a file name at the current cursor position.
       | Instead of blocking, the default should always be to fall back to
       | filename completion.
       | 
       | Sometimes I'm close to disabling/uninstalling all completion
       | scripts out of irritation as decades of muscle memory are
       | frustrated by this behavior.
       | 
       | It's like that bad/annoying UX with text fields where the UI is
       | constantly fighting against you in order prevent you from
       | producing "illegal" intermediate input - e.g. let me paste the
       | clipboard here goddammit - I know what I'm doing - I'll correct
       | it.
        
         | cryptoz wrote:
         | I have come to absolutely despise web form inputs with front
         | end email validators that are broken. Input field hints to type
         | your email, so you start typing. As soon as you type the first
         | letter it goes red and says "error!!! Invalid email!"
         | 
         | Unbelievably frustrating.
        
           | pastage wrote:
           | As a guy who is vicously guarding our company email valdation
           | I can tell you that it is a rite of passage for new frontend
           | hires to mess that up.
        
         | compressedgas wrote:
         | As it works as desired after running: complete -r; there is
         | something broken about the bash-completion script.
        
         | IshKebab wrote:
         | I agree that is annoying. It's waaay less confusing to complete
         | a filename and then get an error from the actual program than
         | it is for just ...nothing to happen so you get confused and
         | have to `ls` to check if the file actually exists and it does
         | and so you think tab completion is broken for some reason and
         | you copy & paste the filename and then _finally_ you get the
         | error that explains what is going on.
         | 
         | It should at least print a message like "file foo.exe exists
         | but it isn't executable".
        
           | kevindamm wrote:
           | If you get into this position what you can do is `ls <tab-
           | completed-path>` or other command to put the filename in the
           | previous command's argument, then you can access it via !$ or
           | !^ (or use !!:1 or your shell's notation for indexing an
           | argument that was already in the previous command).
           | 
           | It's not a fix but it'll save a little time sometimes.
        
             | minhm wrote:
             | An alternative way would be pressing M-. (assuming one is
             | using Readline for typing text in the shell, which is the
             | default for my bash shell).
        
         | loeg wrote:
         | There is one particular command I occasionally use that has
         | totally broken completion for files, so I've taken to just
         | using 'ls X Y Z' to get the right completion behavior and then
         | changing 'ls' to the right command as the last step.
        
         | JNRowe wrote:
         | There is the complete-filename function that only completes
         | filenames in bash, bound to M-/ by default. You can use that in
         | any place you want a filename where "complete"(the function
         | normally bound to tab) would do something you don't desire.
         | 
         | There are a collection of other non-context aware completion
         | functions that are bound by default too, useful for example
         | when you when you wish to complete hostnames in a for-loop.
         | 
         | zle has what is largely a significant superset of this, the
         | documentation is spread about between the zshzle and zshcomp*
         | manpages.
        
       | vbezhenar wrote:
       | Here's zsh snippet I've came up with for my own simple functions.
       | I'm using it as a base for other completions. In this example,
       | function `set-java-home zulu-21` sets JAVA_HOME to
       | `~/apps/java/zulu-21`. Here's `_set-java-home`:
       | #compdef set-java-home                  local -a
       | versions=(~/apps/java/*(:t))         _describe 'version' versions
       | 
       | So basically almost a one-liner (but couldn't do it really one-
       | liner, unfortunately).
        
       | medv wrote:
       | JSON fields autocomplete right in bash/zsh:
       | https://fx.wtf/install#autocomplete
        
         | imcritic wrote:
         | Thanks for linking this! This is a lightweight solution,
         | compared to ijq (interactive jq), but it still may come in
         | handy.
         | 
         | https://github.com/gpanders/ijq
        
       | xenophonf wrote:
       | I wish tcsh would get more love.
        
         | esafak wrote:
         | Why, it's a dinosaur? Have you tried nushell, murex, oil shell
         | or xonsh?
        
         | chasil wrote:
         | There is a famous paper on the perils of scripting in the csh.
         | It is unfortunate that Bill Joy was not able to write a formal
         | grammar or parser for his language. It was certainly a missed
         | opportunity, and tcsh cannot fix the design.
         | 
         | That being said, csh advocates definitely influenced everything
         | in the Bourne/POSIX family.
        
       | harimurti wrote:
       | I'm not familiar with `_gnu_generic`, but it sounds like a handy
       | shortcut for basic completions without writing a full script.
       | Does it work with commands that only have `--help` but no man
       | pages?
        
       | sebtron wrote:
       | Basic completion in ksh is as easy as defining an array. From
       | https://man.openbsd.org/ksh :
       | 
       | Custom completions may be configured by creating an array named
       | 'complete_command', optionally suffixed with an argument number
       | to complete only for a single argument. So defining an array
       | named 'complete_kill' provides possible completions for any
       | argument to the kill(1) command, but 'complete_kill_1' only
       | completes the first argument. For example, the following command
       | makes ksh offer a selection of signal names for the first
       | argument to kill(1):                   set -A complete_kill_1 --
       | -9 -HUP -INFO -KILL -TERM
        
         | chasil wrote:
         | Is this in the Korn & Bolsy ksh88 book?
         | 
         | Or is this ksh93 syntax that oksh back ported?
        
           | sebtron wrote:
           | I don't know actually. Are there currently used version of
           | ksh that are not derived from ksh93?
        
             | chasil wrote:
             | Very much so - pdksh was a bsd-licensed clone that is the
             | ancestor of mksh (Android's system shell) and OpenBSD's
             | oksh, which you previously mentioned.
             | 
             | David Korn thanked the pdksh authors and maintainers for
             | making it available in the years when true ksh was closed
             | (ksh88) or open but licensed with awkward terms (ksh93 for
             | several years).
             | 
             | David Korn interview (I asked one of the questions):
             | 
             | https://m.slashdot.org/story/16351
             | 
             | "First of all pdksh is a ksh88 clone; and I might add a
             | better clone than the MKS Korn Shell...
             | 
             | "I don't know the pdksh development team but I would like
             | to thank them for the service they have done in making a
             | version of ksh available while ksh was proprietary. I have
             | noticed remarkable improvements in pdksh in its ability to
             | mimic ksh88 functionality. I don't know what plans the
             | pdksh development team has now that ksh93 is available in
             | open source form, but I certainly would help them try to
             | maintain compatibility if they do continue pdksh
             | distribution. Otherwise, I would hope that they would pick
             | up the ksh93 source and help support and enhance it."
        
       | gosub100 wrote:
       | I did something similar to this for tab-completing server names
       | for use with ssh. I went a step further and allowed pattern
       | matching based on the server being qa/prod and on locale. so for
       | instance you could type `ssh co prod <tab>` and it would tab-
       | complete / suggest any servers that were production and located
       | in the Denver datacenter (co is the state abbrev for Colorado,
       | for non-US readers).
       | 
       | Unfortunately my work doesn't allow me to share code, but
       | essentially I remapped ssh to a bash script that maintains an
       | environment variable containing the args (you must do this
       | because each <tab> press is an independent invocation of the
       | script. Then you run into persistence problems, so I added a call
       | to compute elapsed seconds so that it flushes the state variable
       | after a 10s timeout).
       | 
       | The bash script then forwards the args to a python script that
       | reads a JSON file and figures out which params (such as 'co' or
       | 'qa') map to which hostnames. It also matches against partial
       | hostnames, so when you see this after tab
       | 
       | qa-server-co1 qa-server-co2 pr-server-co3
       | 
       | you only need to add '3' to the list of args to narrow it down to
       | 1 match, then hit <enter> to ssh to that host.
        
       | paradox460 wrote:
       | I've started using jdx's usage[1] for my clis. It integrates
       | neatly into clap, and can be used stand alone in scripts. It can
       | generate completions, argparse, manpages, and more
       | 
       | I'm still on the fence if replacing the argparse blocks in my
       | fish scripts is worth the hassle, but against things like old
       | school optparse, it's far better
       | 
       | [1]: https://usage.jdx.dev/
        
       ___________________________________________________________________
       (page generated 2025-08-10 23:00 UTC)