Get the FREE Ultimate OpenClaw Setup Guide →

fish-shell-scripting

npx machina-cli add skill Pyroxin/opinionated-claude-skills/fish-shell-scripting --openclaw
Files (1)
SKILL.md
10.0 KB

Fish Shell Scripting

Related skills:

  • software-engineer - General scripting design principles
  • python-programmer - When shell complexity exceeds ~100 lines, consider Python

<core_philosophy> Fish is a user-friendly shell that prioritizes correctness over POSIX compatibility. Its lists-not-strings semantics eliminates entire classes of bugs common in bash/zsh. The limitation is portability, not capability.

Key insight: Fish handles sophisticated automation well. Choose Fish when you control the environment; choose bash when you don't. </core_philosophy>

Platform Requirements

<platform_requirements> Compatibility: All Fish scripts MUST work on both macOS and Fedora Linux. Handle platform differences via uname detection.

Quote paths with spaces: Always quote file paths even though Fish has no word splitting. Fish doesn't need quotes technically (no word splitting), but quoting anyway: (1) makes intent explicit for readability, (2) builds consistent habits when switching between shells, (3) ensures compatibility when passing to external commands that may interpret spaces.

Shebang: Use #!/usr/bin/env fish for portability across installation locations. </platform_requirements>

When to Use Fish vs Other Tools

<fish_vs_alternatives> Use Fish for:

  • Automation on systems where Fish is installed (dev environments, personal machines, containers you control)
  • Scripts benefiting from lists-not-strings semantics and strong scoping
  • Complex CLI tools requiring argument parsing (argparse + fish_opt)
  • Docker/container orchestration, dotfiles, local tooling

Use bash for:

  • POSIX compliance requirements or /bin/sh compatibility
  • CI/CD pipelines in uncontrolled environments
  • Distribution to users who may not have Fish

Use a programming language for:

  • Heavy data structure manipulation or API processing
  • When shell semantics aren't the primary concern
  • Scripts exceeding ~100 lines with complex logic </fish_vs_alternatives>

Critical Differences from POSIX Shells

<posix_differences> No word splitting: Variables don't split on spaces. set name "foo bar"; echo $name is one argument. This eliminates a major source of bugs in bash/zsh.

All variables are lists: A "string" is a one-element list. Indexing is 1-based: $PATH[1], $PATH[-1].

No VAR=value syntax: Use set command. set -gx VAR value (global exported), set -lx VAR value (local exported).

Command substitution splits on newlines only: set lines (cat file) creates one element per line. For space-splitting: string split " ". </posix_differences>

Scoping

<scoping_decision> Choosing a scope:

NeedScopeExample Use Case
Temporary within function/block-lLoop variables, intermediate results
Shared across session-gCurrent project settings, temporary overrides
Available to child processes-gx or -lxPATH, EDITOR, build flags
Persist across sessions (Fish UI only)-Ufish_color_*, key bindings, prompt config

Flag combinations:

  • -l (local): Dies when block ends
  • -g (global): Session-scoped, not inherited by child processes
  • -x (exported): Available to child processes (combine with -g or -l)
  • -U (universal): Persists across all sessions, survives reboots

Example: set -gx EDITOR vim (global + exported), set -lx DEBUG 1 (local + exported to children).

CRITICAL: Environment variables (PATH, EDITOR, etc.) should NEVER be universal. Universal scope is for Fish UI config only. </scoping_decision>

Universal Variable Anti-Pattern

<universal_variable_antipattern> NEVER use universal variables for PATH or environment variables. Universal scope is for Fish UI config only (colors, key bindings, prompt).

Why this matters: Universal variables persist to disk and are shared across all Fish sessions. If you append to PATH in config.fish using universal variables, it grows indefinitely on every shell start because:

  1. config.fish runs on every session
  2. Universal variable already contains previous value
  3. Append adds duplicate entries
  4. This compounds across reboots
# WRONG: Grows PATH indefinitely on every shell start
set -U fish_user_paths ~/bin $fish_user_paths

# RIGHT: Session-scoped (recalculated fresh each session)
set -gx PATH ~/bin $PATH

# RIGHT: Use fish_add_path once interactively (idempotent, uses universal internally)
fish_add_path ~/bin

Safe universal variable uses: fish_color_*, fish_key_bindings, fish_prompt, fish_greeting. </universal_variable_antipattern>

Cross-Platform Patterns

<cross_platform> OS detection: switch (uname); case Darwin; ...; case Linux; ...; end

Conditional PATH: test -d ~/.cargo/bin; and fish_add_path ~/.cargo/bin

Platform-specific utilities to watch for:

  • GNU vs BSD commands: sed, find, date, stat, readlink behave differently
  • Package managers: Homebrew (macOS) vs DNF (Fedora)
  • Paths: /usr/local/bin (macOS Homebrew) vs /usr/bin (Fedora) </cross_platform>

Critical Idioms

<critical_idioms> Wrapper functions require --wraps and $argv:

function ls --wraps=ls --description "ls with color"
    command ls --color=auto $argv
end
  • Without --wraps=ls: Completions break (Fish doesn't know what command to complete for)
  • Without $argv: Arguments are swallowed (user's flags/paths ignored)

Argument parsing: Use argparse + fish_opt for complex CLI tools. Check flags with set -q _flag_name.

Guard interactive code: Non-interactive shells (SSH, rsync) execute config.fish. Guard output:

if status is-interactive
    echo "Welcome!"
end

Without this guard: SSH file transfers, rsync, and scp can fail because unexpected output corrupts the protocol.

Use string builtin: Prefer string match, string split over external grep/cut. Benefits: faster execution (no fork), consistent cross-platform behavior, proper exit codes.

and/or for control flow: Chain commands with and/or, not just &&/||. Both work, but and/or are Fish's native idiom. </critical_idioms>

Organization

<organization_decisions>

Content TypeLocationWhy
Functions (reusable commands)~/.config/fish/functions/name.fishAutoloaded on first use, not at startup—keeps shell fast
Topical config (per-tool setup)~/.config/fish/conf.d/tool.fishAuto-sourced alphabetically, keeps concerns separated
Interactive-only setupconfig.fish with status is-interactiveGuards against breaking non-interactive use
Environment variablesconf.d/ with set -gxSession-scoped, recalculated fresh each session

Minimal config.fish: Keep it to ~15 lines of orchestration. Put actual config in conf.d/ files.

Autoload advantage: Functions in functions/ are loaded only when first called, not at startup. This keeps shell startup fast even with many custom functions. </organization_decisions>

Common Mistakes

<common_mistakes>

From Bash Users

<from_bash>

  • Using VAR=value syntax: Fish uses set VAR value. The VAR=value command pattern doesn't exist.
  • Expecting word splitting: set x "foo bar"; echo $x is ONE argument in Fish. This is a feature, not a bug.
  • Using export VAR=value: Fish uses set -gx VAR value (global exported).
  • Using $(...): Fish uses (...) for command substitution, not $(...).
  • Using &&/|| exclusively: Works in Fish, but and/or are the native idiom.
  • Using [[...]] tests: Fish uses test or [...], not bash's [[...]]. </from_bash>

From Zsh Users

<from_zsh>

  • Array indexing from 0: Fish arrays are 1-indexed like zsh, but syntax differs: $array[1] not $array[1] or ${array[1]}.
  • Expecting setopt/unsetopt: Fish configuration works differently—use set -U for persistent settings.
  • Using zsh-specific globbing: Fish globbing is simpler; complex patterns may need different approaches. </from_zsh>

From Any Shell Background

<from_any_shell>

  • Appending to universal variables in config.fish: Creates infinite PATH growth. Use set -gx or one-time fish_add_path.
  • Not guarding interactive code: Breaks SSH/rsync file transfers with protocol errors.
  • Forgetting $argv in wrapper functions: Silently swallows all user arguments.
  • Using (whoami) instead of $USER: Unnecessary subshell; environment variable is faster.
  • Using (hostname) instead of $hostname: Same issue—use the variable.
  • Using alias instead of abbr: Abbreviations expand visibly in history, making commands reproducible. Aliases hide what actually ran.
  • Treating universal variables like environment variables: Universal is for Fish UI config only (colors, bindings, prompt). </from_any_shell> </common_mistakes>

Resources

<resources> **Online Documentation:** - **Fish Documentation**: https://fishshell.com/docs/current/ - **Fish Tutorial**: https://fishshell.com/docs/current/tutorial.html - **Fish FAQ (common gotchas)**: https://fishshell.com/docs/current/faq.html - **Fish for Bash Users**: https://fishshell.com/docs/current/fish_for_bash_users.html

Local Documentation:

Typical locations (check these paths directly):

  • macOS (Homebrew Apple Silicon): /opt/homebrew/share/doc/fish/
  • macOS (Homebrew Intel): /usr/local/share/doc/fish/
  • Fedora/Linux: /usr/share/doc/fish/

Man pages:

  • macOS (Homebrew Apple Silicon): /opt/homebrew/share/man/man1/fish*.1
  • macOS (Homebrew Intel): /usr/local/share/man/man1/fish*.1
  • Fedora/Linux: /usr/share/man/man1/fish*.1

To find dynamically (from Fish shell):

ls "$(dirname (realpath (command -s fish)))/../share/doc/fish"
</resources>

Source

git clone https://github.com/Pyroxin/opinionated-claude-skills/blob/main/opinionated-fish-shell/skills/fish-shell-scripting/SKILL.mdView on GitHub

Overview

Master Fish shell scripting decision framework and critical idioms. Learn when to choose Fish over Bash, understand macOS and Fedora compatibility requirements, and apply Fish specific patterns that prevent bugs such as universal variable anti-patterns, wrapper functions, and interactive guards.

How This Skill Works

Fish uses lists rather than strings, no word splitting, and a distinct scoping model. Variables are created with set, indexing is 1 based, and there is no VAR=value syntax. Scripts should identify the platform with uname, use the fish shebang, and quote paths with spaces for readability and cross shell consistency.

When to Use It

  • Automation on systems where Fish is installed such as dev machines and controlled containers
  • Scripts that benefit from lists not strings and strong scoping for reliability
  • Docker container orchestration, dotfiles, and local tooling built with Fish
  • Scripts that require argument parsing using fish_opt or similar Fish tooling
  • POSIX compliant environments or CI/CD pipelines where Fish may not be available or required

Quick Start

  1. Step 1: Create a fish script with #!/usr/bin/env fish at the top and begin with set declarations
  2. Step 2: Use set for variables and test with uname for platform differences, quote paths with spaces
  3. Step 3: Run the script in macOS and Fedora, refine with wrappers and proper scoping

Best Practices

  • Use the portable shebang #!/usr/bin/env fish for cross installability
  • Always quote paths with spaces to preserve readability and compatibility
  • Declare variables with set instead of VARNAME=value style assignments
  • Choose the correct scope with -l local -g global -x exported and use -U sparingly for Fish UI config
  • Guard noninteractive runs with status --is-interactive or similar checks to avoid surprises

Example Use Cases

  • Wrap a command in a logging function: function log_and_run; echo log: $argv; command $argv; end
  • Export a PATH entry in a controlled environment: set -gx PATH /usr/local/bin $PATH
  • Platform aware config using uname: if test (uname) = Darwin; echo macOS; else; echo Linux; end
  • Wrapper for interactive prompting without accidentally running in noninteractive shells: if status --is-interactive; read -P 'Continue? (y/n) ' ans; end
  • Avoid universal variables by using local or global scope appropriately instead of set -U VAR value

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers