_______._ _ _ _.___/ | | | | | __-___ _____ _____ ____-___/ __ .__/_ ._/__(_ ..) /... | ..../ .| ) ./_ ./ ..(/ \_ .\ .../ .\_ / __./\_ \ ang3lo/ex0dus ( ) |../ ^xhb ..|.| /___\ .. .) Oo. \_____|_/_________|_|________/_/ .oO : | - -----andrzej-_ ____\_____|----_|_____________'-lichnerowicz-------- - : | . p e r s o n a l ! | #1 | wEBPAGE . . _ - --------- | - -- - | ------- - _ . -:-\-\-------------------|- - - -|-----------------------------------\-\-: | _|___________|_ | | : : |
|//_)-> 2 0 2 5 . 0 2 . 1 6 <--------------------------------------------(_\|
| |
| |
| |
# Solving AoC in Fish
## Prerequisites
## Installation

This time to follow along, it's enough to have fish shell installed. If you’ve never tried fish, it’s a friendly interactive shell that breaks away from many of the legacy conventions in Bash, often making things more intuitive.

Here’s how you can install fish on macOS and set it as your default shell:

❯ brew install fish
❯ chsh -s /opt/homebrew/bin/fish

It's most likely looking very similar on Linux, just replace with your package manager of choice.

## Fish for Bash users

Fish is a shell similar to Bash, but it has a few important differences. One of the first things you’ll notice is that it doesn’t use or for configuration. Instead, fish stores its settings in files.

In some ways, fish feels like a built-in version of oh-my-zsh—everything is integrated and can be managed with g. That command opens a web-based configuration tool, which allows you to enable or disable features, install plugins, and manage your prompt. Although I don’t use it heavily, I appreciate how it can help beginners learn about fish’s features. The tab, in particular, is handy for browsing all shell functions and their source code.

Screenshot of fish_config web ui

Screenshot by author.

fish_config in action

Unlike Bash, fish doesn’t use export to set environment variables. Instead, it uses the set command with different flags to define scope:

  • creates a variable limited to the current block (local).
  • creates an environment variable in the current shell.
  • creates a global variable.

For example:

for i in (seq 1 10)
set -x y $i
set -l x $i
echo $x
end
echo $x
echo $y
1
2
3
4
5
5

Notice that x is only valid inside the loop, while y persists. To erase a variable, you can use .

Another major difference is that fish does not perform word splitting, so if you set a variable to a string with spaces, it won’t be split into an array when you use it:

> foo="bar baz"
> printf '"%s"\n' $foo
"bar"
"baz"
set foo "bar baz"
printf '"%s"\n' $foo
"bar baz"

All variables in fish are arrays (often referred to as lists). For example, your is already a list of paths:

❯ echo $PATH
/usr/bin /bin /usr/sbin /sbin
❯ count $PATH
4

No colons in sight! Fish arrays are also 1-based:

❯ echo $PATH[0]
fish: array indices start at 1, not 0.
echo $PATH[0]

You might also notice that set doesn’t use an equals sign:

❯ set var=value
set: var=value: invalid variable name. See `help identifiers`
set: Did you mean `set var value`?
(Type 'help set' for related documentation)
❯ set var = value
❯ echo $var
= value

Appending values is straightforward:

❯ set foo foo
❯ echo $foo
foo
❯ set -a foo bar
❯ echo $foo
foo bar

Fish supports both normal wildcards and recursive wildcards . It also supports variable substitution, but lacks certain features like (for interpreting escape sequences). In fish, to process escape sequences, simply avoid quoting:

echo 1\n2
1
2

Fish doesn’t support Bash’s substring manipulations like , , or . Instead, there’s a dedicated string builtin for text operations:

❯ set foo "foo foo bar bar"
❯ string replace foo '' $foo
bar bar

It’s not quite as convenient as Bash’s built-in expansions, but it’s consistent and more explicit. Another Bash feature missing in fish is event designators for recalling commands (like or ). Personally, I don’t miss them much, but here’s an example of how you could replicate for :

It's not as convenient as bash functions, that's truth. The other thing I am kind of missing, although i wasn't using it much, are event designators and recalling last command:

function sudo
if test "$argv" = !!
eval command sudo $history[1]
else
command sudo $argv
end
end

This lets you do something like:

% kill -9 168
bash: kill: (168) - Operation not permitted
% sudo !!

but in fish. You can even see how fish’s approach is arguably type safe, avoiding the pitfalls of Bash’s sometimes unpredictable expansions:

% echo "bash is great!!"
echo "bash is greatkill -9 168"
bash is greatkill -9 168

Similarly, fish handles unset variables differently. Instead of something like echo , you use explicit checks:

if not set -q nofoo
echo "hello"
else
echo $nofoo
end

You could go further and write a helper function:

% echo ${nofoo:?Required.}
function require_var
# usage: require_var VAR ERRMSG
set varname $argv[1]
set errormsg $argv[2]
if not set -q $varname
echo "Error: $varname: $errormsg" >&2
exit 1
end
eval echo '$'$varname
end
# Then:
echo (require_var nofoo "Required.")

I was never a heavy Bash user, but my coworkers often rely on set in scripts. Fish doesn’t have these flags, opting for more explicit error handling, somewhat reminiscent of Rust. Instead of (exit on error), you can chain commands with and and or:

some_command arg1 arg2; or exit
another_command; or return

Instead of (treat unset variables as an error), you use :

if not set -q somevar
echo "ERROR: 'somevar' is required." >&2
exit 1
end

Instead of (print commands), fish uses . And rather than , fish offers , an array containing the exit status of each command in a pipe:

a | b | c
echo $pipestatus
# e.g., might be `1 0 0` if `a` failed but `b` and `c` succeeded.

These differences might sound verbose, but they often make fish more predictable and easier to debug. You can still create any shortcuts or helpers you miss from Bash.

Regarding , fish separates return status from return values:

function foo
if test $argv[1] = 1
return 1
else
echo "test"
end
end
❯ foo 1
❯ echo $status
1
> foo 2
test
❯ echo $status
0

One downside I’ve encountered is with certain tooling. At work, we have utilities that output lines for , like . They don’t directly support fish. I tried wrapping them in functions that convert statements into fish-compatible , but that became tedious. So, at work, I just use for those commands.

## Reading input

Fish feels like a proper REPL at times. It does syntax highlighting of your commands and automatically indents within loops or conditional blocks:

for line in $lines
echo $line
|

Typing end properly closes the loop. It’s a small but pleasant quality-of-life feature when you’re tinkering or prototyping.

for line in $lines
echo $line
end
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
# The Puzzle
Now for the [puzzle](https://adventofcode.com/2024/day/6). The first part seems straightforward if you’re writing in a typical programming language, but doing it in fish turned out not to be so bad. Fish loops are less cryptic than Bash, and its syntax highlighting and indentation help with clarity. Below is a snippet of a guard-patrol simulation, limited to 10 iterations just to test the movement:
```fish
function solve_part1
set x $guardX
set y $guardY
set dir $guardDir
set visitedPositions "$x,$y"
for i in (seq 10)
if not in_bounds $x $y
break
end
switch $dir
case 0
set dx 0
set dy -1
case 1
set dx 1
set dy 0
case 2
set dx 0
set dy 1
case 3
set dx -1
set dy 0
end
set nx (math $x + $dx)
set ny (math $y + $dy)
if in_bounds $nx $ny
if is_obstacle $nx $ny
set dir (math "($dir + 1) % 4")
else
set x $nx
set y $ny
set pos "$x,$y"
if not contains -- $pos $visitedPositions
set visitedPositions $visitedPositions $pos
end
end
end
end
echo (count $visitedPositions)
end

The helper functions -- and -- are straightforward:

function in_bounds
set x $argv[1]
set y $argv[2]
if test $x -ge 0 -a $x -lt $WIDTH -a $y -ge 0 -a $y -lt $HEIGHT
return 0
end
return 1
end
function get_cell
set x $argv[1]
set y $argv[2]
set idx (math "$y * $WIDTH + $x")
echo $grid[$idx]
end
function is_obstacle
set cell (get_cell $argv[1] $argv[2])
if test "$cell" = "#"
return 0
end
return 1
end

After running the function:

....#.....
....^....#
..........
..#.......
.......#..
..........
.#........
........#.
#.........
......#...
....>.....
.........#
..........
..#.......
.......#..
..........
.#........
........#.
#.........
......#...
....#>....
.........#
..........
..#.......
.......#..
..........
.#........
........#.
#.........
......#...

Oof. I realized the robot was driving over obstacles and turning only afterward. The culprit was that I forgot to make variables in helper functions local. In fish, you must use to restrict a variable’s scope:

function get_cell
set -l x $argv[1]
set -l y $argv[2]
set idx (math "$y * $WIDTH + $x")
echo $grid[$idx]
end

There was also a subtle bug with 1-based arrays. For example, if we treat as 1-based, we need to adjust in_bounds checks accordingly:

if test $x -ge 1 -a $x -le $WIDTH -a $y -ge 0 -a $y -lt $HEIGHT

it does feel a little weird that is 0-based because, and is 1-based because that’s how fish arrays work. But, the output is correct:

...#.....
.........#
..........
..#.......
.......#..
..........
.#........
........#.
#.........
......#v..
41

The next step was to find all places where adding an extra obstacle would force the guard into an infinite loop. I refactored into a generic , passing an optional obstacle as a parameter. Here’s a snippet of how I handle that in :

function is_obstacle
set -l x $argv[1]
set -l y $argv[2]
if set -q argv[3] argv[4]
if test $x -eq $argv[3] -a $y -eq $argv[4]
return 0
end
end
set -l cell (get_cell $x $y)
if test "$cell" = "#"
return 0
end
return 1
end

To detect loops, I check for both position and direction. Just revisiting the same position isn’t enough, because the guard may pass through the same spot from different directions. So I created a second list:

function run_guard_simulation
if set -q argv[1] argv[2]
set obstacleX $argv[1]
set obstacleY $argv[2]
end
set x $guardX
set y $guardY
set dir $guardDir
set visitedPositions "$x,$y,$dir"
while true
#for i in (seq 10)
if not in_bounds $x $y
break
end
#print_grid $x $y $dir $obstacleX $obstacleY
switch $dir
case 0
set dx 0
set dy -1
case 1
set dx 1
set dy 0
case 2
set dx 0
set dy 1
case 3
set dx -1
set dy 0
end
set nx (math $x + $dx)
set ny (math $y + $dy)
if in_bounds $nx $ny
if is_obstacle $nx $ny $obstacleX $obstacleY
set dir (math "($dir + 1) % 4")
else
set x $nx
set y $ny
set pos "$x,$y,$dir"
if not contains -- $pos $visitedPositions
set visitedPositions $visitedPositions $pos
else
return 1
end
end
else
break
end
end
if not set -q obstacleX obstacleY
echo (count $visitedPositions)
end
end
❯ ./day06.fish example.txt
Part1 Result: 46
Part2 Result: 6

well, the part 2 answer is correct, but what happened to part 1? Oh. I see. Now I'm counting some of the positions twice, that used to be counted only once. I could do another variable like :

set pos "$x,$y"
if not contains -- $pos $visitedPositions
set visitedPositions $visitedPositions $pos
end
set posDir "$x,$y,$dir"
if not contains -- $posDir $visitedPosDir
set visitedPosDir $visitedPosDir $posDir
else
return 1
end

That way, if the guard tries to occupy the same (x, y, direction) tuple, we know it’s in a loop. The final output for Part 1 and Part 2 ended up correct, but performance was not stellar:

❯ ./day06.fish example.txt
Part1 Result: 41
Part2 Result: 6
________________________________________________________
Executed in 15.93 secs fish external
usr time 14.48 secs 0.06 millis 14.48 secs
sys time 1.37 secs 1.38 millis 1.37 secs

Running it on took X... I nearly gave up, but the debug output of the arena gave me enough confidence to let the script finish. It’s not my proudest moment in terms of performance, but it works, and I learned a lot about fish in the process. Given this was part of an Advent of Code puzzle, I was mostly happy it produced the right answer. Sometimes, brute force is just fine for smaller inputs or personal experimentation.

Anyway, that’s my little dive into using fish for something fairly non-trivial. It’s not always the best fit for large scripts, but the interactive experience is great, and the design choices (like explicit scoping) can help avoid subtle bugs. Hopefully this helps if you’re curious about fish or looking to move away from Bash’s idiosyncrasies. Happy fish-ing!

| |
| |
| |
\__ --> andrzej.lichnerowicz.pl <-- __/ // \\ // ------------------------ ---------------------- \\ '~~~~~~~~~~~~~~~~~~~~~~~~~// ------ ------- \~~~~~~~~~~~~~~~~~~~~~~` '~~~~~~~// \~~~~~~~` // ---------- \ '~~~~~~~~~~~~~~~`