How To Fish Subcommands
2020-06-14
I’m currently working on a somewhat minimalistic Fish Shell implementation of jrnl, a popular CLI journal tool written in Python. My Fish version is still a work in progress but there’s one insight I’d like to share: how to correctly implement subcommands in Fish so that CLI are options are passed through to subcommands.
My first attempt looked something like this. At the bottom of this snippet (which you can paste into a file and run!) I’m defining the “main” export, a function called foo
. This function has a single subcommand, to which it passes its arguments. That function, called __hello
, defines a single option, -i
(or --info
). Additionally, there’s some shared functionality implemented in __shared
. And to make matters a bit more complicated, __shared
also accepts an option.
#!/usr/bin/env fish
function __shared
set -l options (fish_opt -s t -l tag)
argparse $options -- $argv
echo $_flag_t
echo $argv
end
function __hello
set -l options (fish_opt -s i -l info)
argparse $options -- $argv
echo $_flag_i
__shared $argv
end
function foo -a cmd
switch $cmd
case hello
__hello $argv
end
end
Running this with source foo.fish; foo hello -t -i
results in an error:
__hello: Unknown option “-t”
foo.fish (line 12):
argparse $options -- $argv
^
in function '__hello' with arguments 'hello -t -i'
called on line 22 of file foo.fish
in function 'foo' with arguments 'hello -t -i'
__shared: Unknown option “-i”
foo.fish (line 5):
argparse $options -- $argv
^ in function '__shared' with arguments 'hello -t -i'
called on line 16 of file foo.fish
in function '__hello' with arguments 'hello -t -i'
called on line 22 of file foo.fish
in function 'foo' with arguments 'hello -t -i'
Line 12 refers to the argparse
call in __hello
. As the error message implies, the option -t
is indeed unknown to the options I’ve specified in __hello
. Luckily Fish added an --ignore-unknown
option in 3.1b1, which solves this issue. Before that release you had to redirect stderr to /dev/null
which wasn’t ideal. By changing that one line to argparse -i $options -- $argv
the script works again, hooray! Now let’s see what happens if you pass positional parameters to __shared
. Change the call to __shared
to __shared foo bar $argv
and run the script again:
$ source foo.fish; foo hello -t -i
-i
-t
foo
bar
hello
No errors, but the name of the subcommand shows up in the arguments list, which is not what I want. Luckily that’s easy to fix. Simply use set -e argv[1]
to remove the first element (the subcommand name) from the original argument list. Note that I’m not using a dollar sign here which is intentional:
case hello
set -e argv[1]
__hello $argv ```
Let's recap what happens to `$argv`, when the script is called with `foo param -t -i`, as the arguments flow through the different function calls.
- `param -t -i`
- Remove first element `param` -> `-t -i`
- Any option that `argparse` can succesfully parse is removed, in this case `-i`, but we're adding `foo bar` -> `foo bar -t`
- Parse `-t` option -> `foo bar`
The final `$argv` at the very end of `__shared` is therefore just `foo bar`, which is exactly what I need.
`argparse` and `fish_opt` are some of the coolest helpers that Fish makes available to you as a script author and evertime I write POSIX sh or Bash scripts I miss their utility. It's therefore absolutely worth it to check out the [documentation](https://fishshell.com/docs/current/cmds/argparse.html)!
Happy Fish Scripting!