<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Hanlho. Today I Learned - fish</title>
    <subtitle>A collection of concise write-ups on small things I learn day to day.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://til.hanlho.com/topics/fish/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://til.hanlho.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-29T00:00:00+00:00</updated>
    <id>https://til.hanlho.com/topics/fish/atom.xml</id>
    <entry xml:lang="en">
        <title>Fish Shell Completion For Custom Commands</title>
        <published>2026-09-29T00:00:00+00:00</published>
        <updated>2026-09-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Hans L&#x27;Hoest
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://til.hanlho.com/fish/fish-shell-completion-for-custom-commands/"/>
        <id>https://til.hanlho.com/fish/fish-shell-completion-for-custom-commands/</id>
        
        <content type="html" xml:base="https://til.hanlho.com/fish/fish-shell-completion-for-custom-commands/">&lt;p&gt;Any custom CLI tool can get tab completion if the tool supports getting its CLI arguments from itself.&lt;&#x2F;p&gt;
&lt;p&gt;Example: a small profile wrapper (&lt;code&gt;agentic &amp;lt;profile&amp;gt;&lt;&#x2F;code&gt;) that reads its profiles from &lt;code&gt;~&#x2F;.config&#x2F;agentic&#x2F;profiles.yaml&lt;&#x2F;code&gt;. (I created it to start agents with specific models, settings or system prompts.)&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fish&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-fish &quot;&gt;&lt;code class=&quot;language-fish&quot; data-lang=&quot;fish&quot;&gt;&lt;span style=&quot;color:#65737e;&quot;&gt;# ~&#x2F;.config&#x2F;fish&#x2F;completions&#x2F;agentic.fish
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;complete -c &lt;&#x2F;span&gt;&lt;span&gt;agentic &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;-f -a &lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;(agentic --list-profiles)&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;complete -c &amp;lt;command&amp;gt;&lt;&#x2F;code&gt; registers a completion rule for that command.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;&#x2F;code&gt; disables file completion, so TAB never falls back to completing filenames.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;-a &quot;(...)&quot;&lt;&#x2F;code&gt; is a command substitution: fish runs it on every TAB press and uses the output lines as candidates.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Three things make the rest automatic:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fish autoloads completions.&lt;&#x2F;strong&gt; Any file at &lt;code&gt;~&#x2F;.config&#x2F;fish&#x2F;completions&#x2F;&amp;lt;command&amp;gt;.fish&lt;&#x2F;code&gt; is sourced lazily the first time completion for &lt;code&gt;&amp;lt;command&amp;gt;&lt;&#x2F;code&gt; is requested.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The TAB-as-description convention.&lt;&#x2F;strong&gt; When a candidate contains a TAB character, fish renders the part before the tab as the completion and the part after it as the grey description on the right. So the wrapper prints &lt;code&gt;name&amp;lt;TAB&amp;gt;description&lt;&#x2F;code&gt; (&lt;code&gt;printf &#x27;%s\t%s\n&#x27;&lt;&#x2F;code&gt;) and the shell never parses the config itself  (the separator has to be a real tab byte).&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Single source of truth.&lt;&#x2F;strong&gt; The completion knows nothing about the config format, it asks the tool. Every TAB re-runs the command, so a profile added to the config appears immediately with its description; no shell restart, no re-registration. Profiles without a description yield the bare name.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Verify without pressing TAB:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;fish&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-fish &quot;&gt;&lt;code class=&quot;language-fish&quot; data-lang=&quot;fish&quot;&gt;&lt;span style=&quot;background-color:#bf616a;color:#2b303b;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span&gt; fish &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;-c &lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#a3be8c;&quot;&gt;complete -C&amp;quot;agentic &amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;fast	&lt;&#x2F;span&gt;&lt;span&gt;Pi with a fast model
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;private &lt;&#x2F;span&gt;&lt;span&gt;Vibe with mistral and local models
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
</feed>
