Fish is really a good shell that I’m using everyday since two years now. Unfortunately there is not much guides about how to program using it. That’s sad because fish has a lot of advantages over bash to write scripts: cleaner and easier to remember syntax, good variables escaping, etc.. So I decided to write a small tutorial on the subject.
By “programming” I mean making small program that can realize simple mathematical operations, apply algorithms, etc… Yes, as strange as it way seem even when reading the full fish documentation it’s still easier to make such basic things in C rather than fish or bash. That is, in my opinion, because that documentation (which is quite good indeed) mainly speaks about an interactive shell rather than about a programming language. This article aims to correct that point.
To start, install fish, create a small .fish file, put a
#! /usr/bin/fish on top of it, make it executable and start to read the guide.
set variable_name insert value here
set keyword takes as first argument the name of the variable. All the remaining arguments will constitute its value. If there are multiple ones the resulting variable will be a list.
To use that variable, use the
The dedicated command for mathematical operations in fish is
math "1+2" # 3
math concatenates all its arguments with a space between them before evaluating the complete expression. So
math 1 + 2 and
math 1+ 2 are all equivalent. But since many operators can also be used in the fish syntax (like <, >, |, *,…) it is usually easier to put everything in quotes.
math supports most C operators and outputs the result on stdout.
To play a little with mathematical operations and variables we will also need to use command substitution, which is realized in fish using parentheses:
set x 1 set y (math "$x*3") set y (math "$y+1") echo $y # 4
The if in fish is quite easy to remember compared to bash:
if some_command_returning_true_or_false echo "command returned true" else echo "command returned false" end
There is no specific delimiter to mark the beginning of command blocks except the carriage return. The end of the structure is marked with the
end keyword, like with all other command structures.
What is a little bit more tricky is the condition used for the if: it’s a command whose returning status should be 0 (for true) or any other value (for false). Since the
math keyword returns its value on stdout and not as a status, how to use it with
if? This is the trick: the returning status of
math will be false if the calculated value is equal to 0 and true for any other value. So the following code will work:
if math "1==1" echo "1 equals 1" else echo "1 is not equal to 1" end
Even if it works as expected, there is still one little problem. It will output this when executed:
1 1 equals 1
That is because the
math command will perform as it is supposed to and output its value on stdout. To avoid this, we can redirect its output to /dev/null:
if math "1==1" > /dev/null echo "1 equals 1" else echo "1 is not equal to 1" end
As a side note, the
test command can also be useful for expression evaluation in
if. It can perform checks on files and strings.
set x 0 while math "$x<10" > /dev/null echo $x set x (math "$x+1") end
Now that we explained the
while should be quite trivial isn’t it?
As explained before, when we assign multiple values to a variable it will create a list. The different elements of that list can be accessed using the
set myvar "apple" "banana" "apple pie" echo $myvar # apple echo $myvar # banana echo $myvar # apple pie echo $myvar[1..2] # apple banana
When passing a whole list as an argument to a command fish will expand the elements of the list as multiple arguments for the command. This makes fish infinitely superior to bash by making it, at least, somewhat possible to make scripts handling correctly spaces in the file names.
set myfiles "file 1.txt" "file 2.txt" rm $myfiles # yes, for the first time in the whole Unix history this will work as expected
Anyway, to get the size of a list you can use the
count $myvar # 3
Lists in fish are technically immutable, but you can construct a new list and assign the new value to the same variable:
set myvar $myvar tomato
To iterate on list elements you can also use the
for ... in structure:
for el in $myvar echo "Element value: $el" end #Element value: apple #Element value: banana #Element value: apple pie #Element value: tomato
Parsing command line arguments
When creating complex scripts using fish it can be useful to parse command line arguments. Most tutorials about this using bash will explain the usage of the
getopts command. That command is specific to bash so it is not usable in fish, fortunately we can use an alternative which is the Unix standard
getopt command (notice the only difference is the “s”).
Here is a complete example of usage of a command line arguments parsing program in fish. It seems long and boring but once we get used to it it becomes quite fast to write.
function help_exit echo "Usage: [options] arguments..." echo "Arguments:" echo "-a : Do something" echo "-d : Do something else" echo "-c stuff : Do someting with stuff" exit 1 end set args (getopt -s sh abc: $argv); or help_exit set args (fish -c "for el in $args; echo \$el; end") set i 1 while true switch $args[$i] case "-a" echo "argument a is specified" case "-b" echo "argument b is specified" case "-c" set i (math "$i + 1") echo "value of argument c is" $args[$i] case "--" break end set i (math "$i + 1") end set pargs if math "$i <" (count $args) > /dev/null set pargs $args[(math "$i + 1")..-1] end echo "positional arguments:" $pargs
The most interesting part of this example is the call to
getopt‘s first positional argument is the options specifier (here
abc:). That options specifier is a list of characters where each character is a possible option for our program (only short options with only one character are supported with this usage). If an option is followed by a colon (;) it means that option takes a parameter. The rest of the positional arguments for
getopt are the arguments to parse (in fish it’s the
The whole purspose of
getopt is to analyze Unix-style command line arguments and rewrite them in a more standardized format which is easy to parse with a simple
while. By using the
-s sh option of
getopt and using a little trick by invoking a new fish interpreter I create a nice list where each arguments are clearly indicated. As example, if I use these arguments:
-ac tmp.fs hello world
I will obtain these elements in
"-a" "-c" "tmp.fs" "--" "hello" "world"
getopt was smart enough separate the
-c arguments and determine that
tmp.fs was a parameter of
-c instead of a positional argument.
-- always indicate the beginning of the positional arguments.
getopt also return false if it detected an error during the parsing, in which case we generally exit the program after printing the help.
That’s all for now, if you have some ideas about additional stuff to explain in this guide don’t hesitate to post some comments. Just for fun, here is an implementation of 99 bottles of beer on the wall in fish:
#! /usr/bin/fish for quant in (seq 99 -1 1) if math "$quant > 1" > /dev/null echo "$quant bottles of beer on the wall, $quant bottles of beer." if math "$quant > 2" > /dev/null set suffix (math "$quant - 1") set suffix "$suffix bottles of beer on the wall." else set suffix "1 bottle of beer on the wall." end else if math "$quant == 1" > /dev/null echo "1 bottle of beer on the wall, 1 bottle of beer." set suffix "no more beer on the wall!" end echo "Take one down, pass it around, $suffix" echo "--" end