guide_to_fish_completions

A guide for fish shell completions
Fish shell is great and if you are reading this is because you already know that. So Iām assuming that you are familiar with all the fish shell greatness.
Completions, right thatās the topic. Fish completions donāt come out of the box for most of the tools so you either find them yourself by searching for some open source completions done by someone, or you create your own. Thereās also a third option which is you ignore all of that and live your life without completions, but letās skip this option.
A bit of context, recently I have started doing a lot of work around aws using terraform, to help us manage access to our developers we use okta. To run commands against our infrastructure we would run something like:
The challenge of not having any completions is that firstly my memory sucks so I need to constantly do reverse searches secondly, I donāt remember all of the names of the profiles we have, as we have multiple profiles for each environment, a lot of environments and last but not least we also lose aws-cli completions. Now writing completions can be hard, see for yourself the git completions provided by fish. Thatās because although they seem simple there are tons of edge cases that can generate false positives and false negatives. In this guide we wonāt try to solve every single possible scenario instead, we will provide completions for the happy paths which make 99% of the use cases.
Letās start with the most basic scenario when we type aws-okta and press tab we should be getting completions for the available aws-okta commands. So how do we start with completions? We just create a file in our fish completions folder, the default is ~/.config/fish/completions/. If you donāt have a completions directory, create one. We then create a file inside that folder with the name of the command, in this case, aws-okta.fish.
If we run aws-okta ā help we get this:
Letās just add completions for these 5 commands, and for the purpose of this guide we will ignore the flags. On our ~/.config/fish/completions/aws-okta.fish we will add the following:
On the first line, we create a list of our commands, on the second line we just provide completions for our aws-okta command. If you have never seen the complete command before, donāt panic, you can find the full list of the available options here.
Letās breakdown the options we are passing to our complete command. The -f or --no-files mean that we donāt want to provide any filenames of the current directory, as options for the autocomplete. The -c or -- command is to specify the command we want to provide completions for. So far pretty simple, letās skip the -n for now and letās go to the -a or -- arguments. This option represents the arguments we want to pass to our completion and in this case, we pass our list of commands $okta_commands. The -n or ā- condition flag is something that allows us to enable and disable completions based on the condition. The condition can be any shell command but it must return 0 if we want to apply completions. We can be super creative with it and do whatever we want, in our very simple example we use an existing helper function from fish. So this is where things start to get a bit complicated and if you get this part you get completions sorted out for life.
With this -n ānot __fish_seen_subcommand_from $okta_commandsā what we are saying is, if any of the commands that are specified in our list $okta_commands have not been seen, then we want to provide completions. Confused? Donāt worry this example is going to make things clear, nothing like a good example. If we didnāt specify that condition and had something like: complete -f -c aws-okta -a ā$okta_commandsā if we typed our aws-okta command and press tab, we would get our list$okta_commands displayed. The problem is that after we get our first command completed, we end up with something like this: aws-okta add. If we press tab again what do we get? The same list again. This is problematic because we want to add more completions and if we donāt create conditions for our completions, then our options will just stack on top of each other and we will be also providing wrong completions. Our condition -n ānot __fish_seen_subcommand_from $okta_commandsā is not bullet proof. If we type aws-okta yolo and then press tab we will get our list and thatās because our condition itās only doing one thing, it checks if any of the $okta_commands has been typed, if not then those are the options we want to provide.
We are almost over with the first part. Letās add descriptions for all of our $okta_commands
So now we have exactly the same completions as before, except now each command has a beautiful description. As you can see the only difference between each line is the argument and the description.
Completions without description
Completions with description
Great so we did our first step, now we are able to provide completions for the first part of our command with meaningful descriptions.
Side note. The best way to approach completions is by starting small and then slowly iterate over and fix edge cases. In the next section, we will be learning how to fix some of the edge cases and using some of the common practices.
Adding dynamic and nested completions
We were a bit lazy and just decided to provide our list as an option. But we can be a bit more elegant. This means extra work but it also makes completions more useful.
The next step is to provide completions to any of the okta_commands, for the sake of simplicity letās just focus on the exec command. But before we progress if you want to follow along. You can copy some of the commands and work on your local machine. You donāt need to actual install aws-okta you can create a dummy function, by just creating a file in ~/.config/fish/functions/aws-okta.fish. If the directory functions doesnāt exist, create it, and paste the following:
This is a dummy function that just echoes its arguments, but itās enough to play around with our completions.
Still with me? Hopefully everything has been making sense. Getting back to the initial command:
We now want to provide auto completions to the exec command. These completions are going to be generated dynamically, donāt worry itās simpler than it sounds. j To give you some context, aws-okta reads your aws .config file, you then provide the name of profile you want to use.
Hereās what a file looks like:
So if you would like to login into prod you would need to type aws-okta exec okta-prod
So ideally what we would like is to type aws-okta exec press tab and get okta-dev, okta-staging and okta-prod as options for completions. So how would we do that? Well, you do remember on our first example when we provided a list to our -a flag and it magically populated our completions with the options. We just have to the same. We can either just hardcore the profiles as list or we can read them from the file. Letās do it the hard way and get those profiles from the file. Without getting in too much detail about the __fish_okta_complete_profiles function, it greps all the lines that start with [ but not the first one [okta] and then using awk we extract the name of the profiles. Behold the final result:
Letās break line 12. Our condition to provide completions is: if we have seen the exec command typed and if we havenāt seen any of the existing profiles in our file also typed, then we want to provide the profiles as completion. Again these conditions that we have been using are pretty basic, but they will suffice for 99% of the cases and sometimes 99% is more than enough (At least is way better than 0%). Just a small note, in our condition and in the -a flag we are calling our function, thatās why we need to wrap it with ().
Simple, right? We are almost finished, just two more steps. Oh and by the way if you want to test this bit on your computer feel free to copy the okta profiles, create a file in ~/.aws/config and just paste the profiles above. Or if you prefer creating a file in another directory remember to also update our function with the correct path.
The next completion is quite simple and its solely existence is due to my laziness, thatās right the next completion just adds the --.
By now you should have started seeing a pattern in our conditions, we want to provide ā- if we have seen any of the profiles that exist on our file and we havenāt seen a ā- typed, then we provide that option. Because this our only completion for this specific condition then fish will insert -- automatically. Remember when I said these completions work for 99% the cases, if by any chances you decide to type aws-okta okta-prod and then press tab can you guess what would happen? I will give you little hint.
Our condition for the -- says that if we have typed a profile that exists in the file and if thereās no -- typed then we provide -- as completion. Itās not that bad, but! if we look on our previous completions which have a description, their condition is: if we havenāt typed any of the $okta_commands then we provide the $okta_commands which is also a valid condition, so as you may have guessed our completions will be all the $okta_commands plus the --.
We are almost finished, we just need the last bit which to be honest is the most important one. One of the drawbacks of using aws-okta is that I lose my autocompletions for terraform and aws-cli. Because we run those after the okta exec if I press tab, nothing happens. Letās fix that! Our last bit is if we type aws or terraform after the -- we want to get the original completions for those commands, and everything after aws s3 should also show completions. In fact we should show completions for any arbitrary command that we run, either be it aws, terraform or even a custom function with completions created by us. Letās break this into two parts, firstly we need to get everything after --. Secondly, regardless of what we have typed we just need to get completions for it.
So without further ado, our final version:
Thereās a lot to digest, letās start with our condition:
This is basically saying if -- is present in the entire command the user has typed, then we want to do some completions. Note that we pass -- twice, thatās because we donāt want contains to think that we are trying to pass an option.
As for the completion itself, we are providing the options for whatever is after --, for that we have created the following function:
Inside this function what we are actually doing is creating a variable $tokens, which is a list with all the tokens inserted by the user until the cursor. So if our cursor is at the end of the line, then our tokens are basically the entire command except for the character under our cursor which is what (commandline -opc) does. Then (commandline -ct) gets the character under our cursor. Now we have our entire command tokenised. Thatās pretty much useless since we wanted everything after --, not the entire command. So we use the command contains, but this time with -i flag which returns the index of the thing we are trying to find, the --. Since we know whatās the index, we just need to delete everything in our list of $tokens until the index. Thatās what this command set -e tokens[1..$index] does, deletes everything from the first token until the index of --. Phew!! all this work just to get the bit after --. Now that we have that part, things get pretty easy as we just need to run completions on our list of$tokens using the -C flag, complete -Cā$tokensā (Note thereās no space between the flag -Cand the quotes). With all of this we are basically saying to fish, āHey! can we get completions for anything that we type after the --?ā. In fact you can try this on your shell by doing complete -C"git checkout " (note thereās a space after checkout).
Conclusion
I hope that you found this post useful and that it answered some of the questions you had about fish completions. I tried to be as thorough as I could, so even if you donāt have any experience with fish and fish scripting you could follow along.
I would like to thank all the fish community and the contributors, especially faho for answering all of my questions related to fish scripting. You for reading all of this, I really hope that you found it useful, if not Iām sorry that I have wasted your time. Last but not least, my brother who showed me fish 6 years ago, thanks puto!
Last updated