The Fish Cookbook
From Shell to Plate: Savor the Zest of Fish
๐ฆ
Welcome to The Fish Cookbook, your ultimate guide to unlocking the full potential of shell scripting with the delightful Fish. With its user-friendliness and convenient features, Fish stands out as a true gem among shells.
Master programming challenges with our curated collection of Fish recipes. Glide through your code with ease, whether you're a seasoned pro or a shell scripting newbie.
This cookbook is licensed under CC BY-NC-SA 4.0, ensuring knowledge and inspiration flow while respecting the rights of others.
What's on the Menu?
- Setting Sail
- Foundations
- Decoding Prompts: What Are These Wiggly Characters?
- Where Am I in Fish? Discovering Your Current Path
- Searching and Running Commands in Fish: How Do I Do That?
- Did My Command Succeed in Fish?
- What's the Fish Shebang All About? Making Scripts Executable
- How Do I Debug My Fish Scripts Interactively? Navigating with Breakpoints
- Where to find Fish's equivalent of
.bash_profile
or.bashrc
?
- All About Variables
- How Do I Set Variables in Fish? A Quick Guide to Variable Scopes
- How Do I Export a Variable in Fish?
- How Do I List All Environment Variables in Fish?
- How Do I Set the
$PATH
Persistently in Fish? - How Do I Remove a Path from the
$PATH
in Fish? - How Do I Remove a Path Persistently from the
$PATH
in Fish? - How Do I Check if a Path Exists in the
$PATH
in Fish?
- All About Functions
- How Do I Whip Up a Function in Fish?
- How Do I Craft a "Private" Function in Fish?
- Should Function Names and File Names Be a Perfect Match?
- Can I Fit Multiple Functions in a Single File?
- How Do I Reveal a Function's Definition in Fish?
- Exploring Functions, Builtins, and Commands in Fish
- Displaying the Fish Function Roster
- Verifying a Function's Existence in Fish
- All About Arguments
- All About Aliases
- IO
- Concurrency: Making Your Fish Swim Faster
- Glossary
- Contributing
Setting Sail
Ahoy there, matey! Prepare to embark on a grand adventure with our trusty mate, Fish. In this section, we'll guide you through the step-by-step setup process to ensure you're ready to navigate the command line seas like a true captain. Let's get your shipshape and ready for the voyage ahead!
How the Shell do I Install Fish?
If you're ready to dive into the wonderful world of Fish, here's how you can get your hands wet. You can either follow the official website for instructions, or stick around and sail through the directions provided here for your OS.
For macOS with Homebrew:
brew update && brew install fish
For Debian:
wget http://download.opensuse.org/repositories/shells:fish:release:2/Debian_8.0/Release.key
apt-key add - < Release.key
echo 'deb http://download.opensuse.org/repositories/shells:/fish:/release:/2/Debian_8.0/ /' >> /etc/apt/sources.list.d/fish.list
apt-get update
apt-get install fish
For Ubuntu:
sudo apt-add-repository ppa:fish-shell/release-2
sudo apt-get update
sudo apt-get install fish
For CentOS:
cd /etc/yum.repos.d/
wget http://download.opensuse.org/repositories/shells:fish:release:2/CentOS_7/shells:fish:release:2.repo
yum install fish
For Fedora:
dnf install fish
For Arch Linux:
pacman -S fish
For Gentoo:
emerge fish
For FreeBSD:
pkg install fish
For NetBSD:
pkgin install fish
For OpenBSD:
pkg_add fish
For the brave who want to build from source:
sudo apt-get -y install git gettext automake autoconf ncurses-dev build-essential libncurses5-dev
git clone -q --depth 1 https://github.com/fish-shell/fish-shell
cd fish-shell
autoreconf && ./configure
make && sudo make install
With Fish installed, a thrilling journey lies ahead. Gear up to explore the command line, unearthing Fish's core treasures and secrets. Here's to abundant and joyous fishing ventures!
Making Fish Your Default Shell, Ahoy!
Once you've installed Fish and it's safely located in your $PATH
, for example, at /usr/local/bin, you can make it your default shell for a smooth sailing command line experience.
echo /usr/local/bin/fish | sudo tee -a /etc/shells
chsh -s /usr/local/bin/fish
Discovering Fish's Installation Location
Curious about where Fish is installed? Utilize the handy which
command.
which fish
/usr/local/bin/fish
Where to Seek Help
If you find yourself in need of assistance or have burning questions about Fish, fret not. There's a treasure trove of resources awaiting you:
- Official Repository โ Explore the official Fish repository on GitHub, where you can find the source code and contribute to the project.
- Tutorial โ Set sail with the Fish tutorial, a step-by-step guide to help you master the shell's features and functionality.
- Documentation โ Dive into the official documentation for Fish, where you'll discover a wealth of information and guidance.
r/fishshell
โ Join the community on Reddit, where fellow Fish enthusiasts gather to share their knowledge and adventures.- Gitter Channel โ Hop aboard the Gitter Channel, a chat platform where you can engage with other Fish users and seek live assistance.
- StackOverflow
#fish
โ Cast your questions into the StackOverflow sea, specifically in the#fish
tag, to receive guidance from the wider programming community. - awsm.fish โ Discover a curated collection of prompts, plugins, and other hidden treasures to enhance your Fish journey.
๐
Foundations
Decoding Prompts: What Are These Wiggly Characters?
The prompt is where you type commands and interact with Fish. Read more about the UNIX prompt here.
You might encounter something like this:
jb@mbp ~/C/cookbook>
The tilde ~
represents the home directory in a compact form, instead of /users/jb/home
, /Users/jb
, and so on.
The @
symbol is an arbitrary character chosen to separate the username jb
from the computer's name mbp
. You can learn to tailor your prompt to display only what's relevant to you.
The forward slash /
serves as the path delimiter. At a glance, you can see that the current directory is inside the home directory, under Code/cookbook
. The abbreviated path displays just C
.
To customize the length of the abbreviated path, use:
set fish_prompt_pwd_dir_length NUMBER
Or, if you prefer not to have abbreviated paths:
set fish_prompt_pwd_dir_length 0
Then, your prompt will look like this:
jb@mbp ~/Code/cookbook>
The greater-than symbol >
signifies the end of the prompt.
Not a fan of these conventions? Feel free to craft your own prompt to suit your preferences.
Where Am I in Fish? Discovering Your Current Path
To find your current location in Fish, simply refer to the read-only environment variable $PWD
.
echo $PWD
/Users/jb/Code/cookbook
Alternatively, you can use the pwd
builtin to determine the current directory.
pwd
/Users/jb/Code/cookbook
In Fish, both $PWD
and pwd
always resolve symbolic links. This means that if you're inside a directory that's a symbolic reference to another, you'll still get the path to the actual directory.
Interactively, typing pwd
is more convenient. For scripting, $PWD
is less expensive in terms of function calls.
Dazzle me!
echo "The current working directory is "(pwd)
# Versus
echo "The current working directory is $PWD"
Searching and Running Commands in Fish: How Do I Do That?
To run a command, type the command's name and press return.
ls
Alternatively, begin typing the command you're looking for and press tab. Fish will use the built-in pager, allowing you to browse and select the command interactively.
Fish determines which commands are available by examining the $PATH
environment variable. This variable contains a list of paths, and every binary file inside any of those paths can be run by their name.
Display your $PATH
contents with:
printf "%s\n" $PATH
/usr/local/bin
/usr/bin
/bin
To list every command in your system and display them in columns, use:
ls $PATH | column
If the list is truncated, try:
ls $PATH | column | less
Navigate the list using k
and j
to move down/up, and press q
to exit.
The $PATH
variable is created at the beginning of the Fish process during environment initialization. You can modify, prepend, or append this variable yourself, for example, in ~/.config/fish/config.fish
.
In addition to the type
, builtin
, and functions
built-ins mentioned earlier, *nix systems often include one or more shell-agnostic alternatives such as which
, apropos
, whatis
, and others.
These commands have overlapping functionality but also possess unique features. Consult your system's manpage for more information.
Did My Command Succeed in Fish?
Every command returns an exit code to indicate whether it succeeded or not. An exit code of 0 means success, while any other value signifies failure. Different commands use various integers to represent possible errors.
In Fish, you can check the exit code of any command using the read-only variable $status
.
my_command
echo $status
When working with pipes, Fish provides a helpful variable called $pipestatus
that allows you to check the exit codes of all commands in a pipeline.
Suppose you have a pipeline like this:
command1 | command2 | command3
To find the exit codes of all commands in the pipeline, use $pipestatus
:
echo $pipestatus
This will print an array of exit codes, one for each command in the pipeline. For example, if command1
succeeded, command2
failed with an exit code of 2, and command3
failed with an exit code of 1, the output would look like this:
0 2 1
With $pipestatus
, you can conveniently keep an eye on the success and failure of commands in your pipelines.
What's the Fish Shebang All About? Making Scripts Executable
The shebang is a special comment that instructs the shell to run a script using a specific program, such as node
or python
.
To run a script with fish
by default, add a shebang to the first line of your file:
#!/usr/bin/env fish
Give me the scoop!
#!/usr/bin/env fish
echo "Check this out, ma'! I'm a Fish on wheels ๐ผ"
Save that to a file and mark it as executable.
chmod +x my_script
The system above allows you to run the script directly by using its path:
./my_script
instead of:
fish my_script
How Do I Debug My Fish Scripts Interactively? Navigating with Breakpoints
To interactively debug your Fish scripts, use the breakpoint
builtin. Simply drop it anywhere in your script where you want to pause execution and launch an interactive debugging prompt.
function some_function
set --local files (ls ~)
breakpoint # when this is executed, an interactive prompt will be launched and you will be able to inspect $files
# ... rest of function is not executed until you exit from debugger
end
With breakpoint
, you can take a closer look at your script's execution, inspect variables, and navigate through the script.
.bash_profile
or .bashrc
?
Where to find Fish's equivalent of In Fish shell, your configuration is stored in the config.fish
file located at ~/.config/fish/config.fish
. This file serves as Fish's equivalent to .bash_profile
or .bashrc
in Bash. It allows you to customize and configure your Fish shell environment by adding functions, environment variables, and other personalized settings.
All About Variables
How Do I Set Variables in Fish? A Quick Guide to Variable Scopes
To set variables in Fish, use the set
builtin.
set foo 42
The set
builtin accepts the following flags to explicitly declare the scope of the variable:
-l
,--local
: available only to the innermost block-g
,--global
: available outside blocks and by other functions-U
,--universal
: shared between all fish sessions and persisted across restarts of the shell-x
,--export
: available to any child process spawned in the current session
If no scope modifier is used, the variable will be local to the current function; otherwise, it will be global.
If the variable has already been defined, the previous scope will be used.
Local Variables
The variable foo
will not be available outside of the if
block.
if true
set -l foo 42
end
echo "foo=$foo" # foo=
Global Variables
The variable foo
will be available outside the if
block.
if true
set -g foo 42
end
echo "foo=$foo" # foo=42
Universal Variables
The variable foo
will be preserved and available to future shell sessions.
set -U foo 42
fish
echo "foo=$foo" # foo=42
Exported Variables
The variable foo
will be local and exported, therefore available to the fish
child process created inside the if
block.
if true
set -lx foo 42
fish -c 'echo "foo=$foo"' # foo=42
end
The variable foo
will be global, but since it's not exported, it won't be available to the fish
child process.
set -g foo 42
fish -c 'echo "foo=$foo"' # foo=
The variable GPG_AGENT_INFO
will be universal and exported, therefore preserved across future shell sessions and child processes.
set -Ux GPG_AGENT_INFO /Users/jb/.gnupg/S.gpg-agent:12345:2
Now you're set to sail through the variable seas of Fish!
How Do I Export a Variable in Fish?
To export a variable in Fish, use the set
builtin along with the scope modifier -x
or --export
.
set -x foo 42
fish -c 'echo "foo=$foo"' # foo=42
By using the -x
or --export
option, you're ensuring that the variable is available to any child process spawned in the current session.
How Do I List All Environment Variables in Fish?
To list all environment variables in Fish, use the set
builtin without any modifier flags.
set
If you prefer to see only the variable names without the values, employ the --names
option:
set --names
And for a full view of each variable without truncating long lines, go with --long
:
set --long
$PATH
Persistently in Fish?
How Do I Set the To persistently add a path to your $PATH
in Fish, make use of the $fish_user_paths
variable.
set -U fish_user_paths $fish_user_paths my_path
If you're using Fish 3.2.1 or later, you can opt for the built-in fish_add_path
function instead.
fish_add_path my_path
See
$PATH
in the Fish tutorial for juicy information.
$PATH
in Fish?
How Do I Remove a Path from the To remove a path from the $PATH
in Fish, use the set
builtin with the -e
or --erase
flag, combined with the contains
builtin to find the index of the path you'd like to remove.
if set -l index (contains -i $my_path $PATH)
set -e PATH[$index]
end
$PATH
in Fish?
How Do I Remove a Path Persistently from the To remove a path persistently from the $PATH
in Fish, use the set
builtin with the -e
or --erase
flag, combined with the contains
builtin to find the index of the path you'd like to remove from the $fish_user_paths
variable.
if set -l index (contains -i $my_path $fish_user_paths)
set -e -U fish_user_paths[$index]
end
$PATH
in Fish?
How Do I Check if a Path Exists in the To check if a path exists in the $PATH
in Fish, use the contains
builtin. This handy tool helps you verify whether your desired path is swimming along with the other paths in your $PATH
variable.
if contains $my_path $PATH
# $my_path is in $PATH
end
All About Functions
How Do I Whip Up a Function in Fish?
Crafting a function in Fish is a piece of cake! Just use the function
builtin to start creating your shell masterpiece.
function mkdirp
mkdir -p $argv
end
To make sure this function stays fresh in future Fish sessions, save it to ~/.config/fish/functions/mkdirp.fish. A neat way to accomplish this is by using the funcsave
function.
funcsave mkdirp
Alternatively, you can use the functions
builtin to write the function definition to a file, like a secret recipe for your custom shell commands.
functions mkdirp > ~/.config/fish/functions/mkdirp.fish
How Do I Craft a "Private" Function in Fish?
While Fish doesn't have built-in private functions, you can still be sneaky! Use a custom namespace to prefix any function you want to treat as private.
function _prefix_my_function
end
You can also simulate private scope using functions -e
.
Show me the secret sauce!
function foo
function _foo
echo Foo
functions -e _foo # Erase _foo
end
_foo
end
This workaround helps you keep some Fish functions under wraps, like a top-secret ingredient in your shell recipe.
Should Function Names and File Names Be a Perfect Match?
Absolutely! Fish's lazy-loading / autoloading mechanism depends on this match made in heaven.
If you have a file ~/.config/fish/functions/foo.fish with a valid function definition bar
:
- In a new shell, trying to run
bar
produces an unknown-command error. - Typing
foo
will highlight as a valid command, but produce an unknown-command error. - Trying to run
bar
again now works like a charm.
Show me the magic!
Save bar
to ~/.config/fish/functions/foo.fish.
function bar
echo Bar
end
functions bar > ~/.config/fish/functions/foo.fish
Create a new shell session.
fish
Try to run bar
, then foo
, then bar
again.
bar
# fish: Unknown command 'bar'
foo
# fish: Unknown command 'foo'
bar
# Bar
Can I Fit Multiple Functions in a Single File?
Yes, indeed! You can define as many functions as you want in a single file. Just remember that Fish doesn't have private functions, so every function in the file will end up in the global scope when the file is loaded.
Since functions are eagerly loaded, defining multiple functions in a single file might not be as efficient as the one-function-per-file approach, which leverages Fish's autoloading capabilities. So, while it's possible to have multiple functions in one file, spreading them across separate files might give you a performance boost and a more organized structure.
How Do I Reveal a Function's Definition in Fish?
If you're certain that the command you're looking for is a function, you can use the functions
builtin to display its definition:
functions my_function
On the other hand, if you're not sure whether the command is a function, a builtin, or a system command, you can rely on the type
command to reveal its true nature:
type my_function
my_function is a function with definition
# ... function definition ...
Using type
, you can easily identify what kind of command you're dealing with and get the information you need.
Exploring Functions, Builtins, and Commands in Fish
In Fish, commands, functions, and builtins each serve different purposes and are defined in different ways:
-
System commands are executable scripts, binaries, or symbolic links to binaries that are located in directories listed in your
$PATH
variable. When a command runs, it operates as a child process and can only access environment variables that have been exported. Example:fish
. -
Functions are user-defined commands that can be created within Fish. Some functions are included as part of the Fish distribution and serve as predefined helpers. Examples include
alias
,type
, andnextd
. -
Builtins are commands that have been compiled directly into the Fish executable. Like functions, builtins can access the environment, but they do not spawn a separate child process. This allows them to execute more quickly and with lower overhead. Examples of builtins include
functions
andset
.
These different types of commands help make Fish a versatile and powerful shell, allowing for a wide range of customization and utility.
Displaying the Fish Function Roster
To list all the functions defined in Fish, employ the functions
builtin without any arguments. Note that hidden functionsโthose with names starting with an underscoreโwon't be displayed. To reveal everything, including hidden functions, use functions -a
or functions --all
.
For a different approach, launch the Fish web-based configuration and head to the /functions tab by running:
fish_config functions
Verifying a Function's Existence in Fish
The type
function is your go-to tool for checking whether a function exists in Fish. It provides information about commands, builtins, or functions.
if not type --quiet "$command_name"
exit 1
end
For builtins, use builtin --names
.
if not contains -- "$command_name" (builtin --names)
exit 1
end
For functions, employ functions --query
.
if not functions --query "$command_name"
exit 1
end
For other commands, utilize command --search
.
if not command --search "$command_name" > /dev/null
exit 1
end
In Fish >= 2.5, it's even easier:
if not command --search --quiet "$command_name"
exit 1
end
All About Arguments
Accessing Function Arguments in Fish
To access the arguments passed to a function in Fish, simply use the $argv
variable.
function Foo
printf "%s\n" $argv
end
Foo foo bar baz
foo
bar
baz
Accessing Script Arguments in Fish
To access the arguments passed to a script in Fish, use $argv
.
fish ./my_script foo bar baz
foo
bar
baz
Example: my_script
#!/usr/bin/env fish
printf "%s\n" $argv
Parsing Command Line Arguments in Fish
To parse command line arguments in Fish, you can use a for
loop.
for option in $argv
switch "$option"
case -f --foo
case -b --bar
case \*
printf "error: Unknown option %s\n" $option
end
end
For a more powerful parsing solution, you can explore Fish's official argparse
builtin. Another option is to consider using getopts
, which is a different approach to CLI parsing.
All About Aliases
The Art of Defining Aliases in Fish
Fish offers the alias
function to create a convenient alias:
alias rimraf "rm -rf"
This creates a function
like:
function rimraf --wraps='rm -rf' --description 'alias rimraf rm -rf'
rm -rf $argv;
end
Be aware that aliases created with alias
won't be available in new shell sessions. To make them persistent, use:
alias -s ...
This saves the alias to ~/.config/fish/functions/[alias-name].fish
config.fish
The Problem with Aliases in Defining aliases inside ~/.config/fish/config.fish
slows down your shell start since each alias/function is eagerly loaded.
To persist aliases across shell sessions, use alias -s
, which creates a function
and saves it to ~/.config/fish/functions
. This leverages Fish function lazy-loading / autoloading mechanism.
IO
Reading a File with Fish
To gracefully read a file line by line in Fish, employ the read
builtin.
while read -la line
echo $line
end < my_file
Reading from stdin and Redirecting Like a Pro in Fish
To read from stdin in Fish, the read
builtin is your best friend.
read --prompt "echo 'Name: ' " -l name
Name: Flipper
echo $name
Flipper
To read from an arbitrary input stream, combine read
with the while
builtin.
while read -la line
echo $line
end
Mastering Redirection in Fish
To redirect stderr to $my_file
:
my_command 2> $my_file
To redirect stdout to $my_file
:
my_command > $my_file
To redirect stdout to stderr:
my_command >&2
To redirect stderr to stdout:
my_command 2>&1
Concurrency: Making Your Fish Swim Faster
Run Commands in the Background with Fish
To run a command in the background in Fish, simply use &
.
sleep 10 &
Checking Background Jobs in Fish
To see if background jobs are running in Fish, use the jobs
builtin.
if jobs > /dev/null
echo Busy
end
Synchronize Background Tasks in Fish Like a Pro
Fish 3.0.0
introduced a dedicated wait
builtin for easy synchronization of background tasks. But if you're curious about how it was done before wait
existed, here's a throwback recipe for you.
First, to check if tasks are running in the background, parse the output from the jobs
builtin.
Parse by Job ID
function get_jobs
jobs $argv | command awk -v FS=\t '
/[0-9]+\t/{
jobs[++nJobs] = $1
}
END {
for (i in jobs) {
print(jobs[i])
}
exit nJobs == 0
}
'
end
Parse by Group ID
function get_jobs
jobs -g | command awk 'NR > 0 { print; i++ } END { exit i == 0 }'
end
Next, block the foreground until all background jobs are finished.
function wait
while true
set -l has_jobs
set -l all_jobs (get_jobs)
or break
for j in $argv
if contains -- $j $all_jobs
set -e has_jobs
break
end
end
if set -q has_jobs
break
end
end
end
Here's an example of our code in action.
set -l urls "https://"{google,twitter,youtube,facebook,github}".com"
for url in $urls
fish -c "curl -Lw \"$url: %{time_total}s\n\" -o /dev/null -s $url" &
end
wait (get_jobs)
Glossary
Here is a concise glossary of key terms and concepts mentioned in the Fish Shell Cookbook:
-
Fish: Fish, short for "Friendly Interactive Shell," is a command-line shell for Unix-like operating systems. It is known for its user-friendly features, syntax highlighting, auto-suggestions, and powerful scripting capabilities.
-
Fish Installation: Installing Fish involves obtaining the Fish shell and setting it up on your system. You can install Fish using package managers or by building it from source.
-
Default Shell: The default shell is the shell that is automatically launched when you open a terminal or log in to your system. Setting Fish as the default shell ensures that Fish is the shell you interact with by default.
-
Fish Configuration: The Fish configuration involves customizing the behavior and appearance of the Fish shell. Configuration options can be set in the
config.fish
file, which is loaded when Fish starts. -
Variables: Variables in Fish are used to store and manipulate data. They can hold strings, numbers, or other types of values. Variables can be defined, assigned values, and accessed in Fish scripts and interactive sessions.
-
Functions: Functions in Fish allow you to group a series of commands together and give them a name. Functions can be defined with the
function
keyword and called by their name. -
IO (Input/Output): IO refers to the input and output operations performed by the shell. Fish provides various commands and techniques for reading from files, redirecting output, and handling input streams.
-
Concurrency: Concurrency in Fish involves running commands or tasks simultaneously. Fish allows you to run commands in the background, check the status of background jobs, synchronize tasks, and wait for background processes to complete.
These terms provide a high-level understanding of the key concepts covered in the Fish Cookbook. Refer to the respective sections in this document for more detailed information and practical examples.
Contributing
Ahoy there! We're excited to have you on board for the Fish Cookbook adventure! Your contributions can make this Cookbook a valuable resource for the Fish Shell community. So, let's dive in and follow the guidelines below.
You can contribute to the Cookbook in a few ways:
-
Adding new recipes: If you have a useful Fish Shell recipe or a clever solution to a pesky problem, you can share your knowledge by adding a new recipe to the repository. Just make sure your recipe follows the established structure and provides clear instructions. Let's make this Cookbook even more diverse and flavorful!
-
Enhancing existing recipes: If you come across a recipe that could use some sprucing up, you can lend a hand and make it even better. Improve the content, add more information, fix any errors, or fine-tune the code. Together, we'll make those recipes shine!
-
Keeping the Cookbook current: The Fish shell is continuously evolving, with new features and updates being released regularly. Help us ensure the Cookbook stays relevant by identifying outdated content, highlighting new features or changes, and proposing updates to the existing recipes. By keeping the Cookbook in sync with the latest Fish shell developments, we'll maintain a valuable and current resource for everyone to enjoy.
To join our crew and contribute to the Cookbook, follow these guidelines:
- Set your sails and fork the repository. Then, create a new branch for your contributionsโa safe space for your changes.
- Work your magic in the new branch. Ensure that the content is accurate, clear, and follows the agreed-upon structure. We're all on the same ship here!
- If you're adding a new recipe, choose a catchy and informative titleโone that grabs attention!
- Commit your changes and write a descriptive commit message to let us know what your changes are all about. Be as detailed as a well-crafted map!
- Push your branch to your forked repository, like a captain charting new waters.
- Open a pull request against the main repository. Share the story behind your changes and why they're valuable. The more we know, the better we can appreciate your work!
Once your pull request is submitted, our trusty crew of maintainers will review it. They may provide feedback or request some tweaks before we hoist the flag and merge your contribution.
You have our heartfelt gratitude for lending a hand in enhancing the Fish Cookbook. We value your efforts, and together, we'll create a fantastic resource for the Fish community. Ahoy!