Skip to content
shellmap

Fish vs PowerShell

Two opposite design philosophies: fish optimizes the interactive prompt; PowerShell optimizes typed-object scripting.

Summary

Fish is a deliberately non-POSIX Unix shell — designed prompt-first, with autosuggestions, syntax highlighting, sane defaults, and a smaller, cleaner scripting language. Linux/macOS native, no Windows build (use WSL).

PowerShell is Microsoft's object shell — pipes carry typed .NET objects, scripts (`.ps1`) are a real programming language, default on Windows since Win7. PowerShell 7+ (`pwsh`) runs on Linux / macOS too.

They share nothing structurally — different OS roots, different paradigms, different syntax. Compare them when you split time between a Linux/macOS box where you've chosen fish and a Windows machine where PowerShell is default, or when you run both in parallel via WSL.

Syntax & semantic differences

  • Pipe contents

    Fish

    Bytes of text, UTF-8 by default. The next command parses lines itself.

    PowerShell

    Typed .NET objects. The next cmdlet reads properties directly — no parsing needed.

    Fish's `ls -l | awk '{print $5}'` becomes PowerShell's `Get-ChildItem | Select-Object -ExpandProperty Length` — different operations entirely because the pipeline carries different things.

  • POSIX compatibility

    Fish

    Deliberately non-POSIX. Most bash scripts fail to parse — fish is its own language.

    PowerShell

    No POSIX compatibility either. Bash / zsh / fish scripts must be rewritten — different operators, different control flow.

  • Variable assignment

    Fish

    `set name value` — verb form, whitespace-separated. `set -x name value` to export. `name=value cmd` is an inline env override only.

    PowerShell

    `$Name = "value"` — C-style. Spaces around `=` are allowed. Variables hold any object, not just strings.

    Fish `set -x` is **export**, not "trace mode" as in bash. PowerShell's equivalent for tracing is `Set-PSDebug -Trace 1`.

  • Reading variables

    Fish

    `$name`. `${name}` is **not** valid — no brace expansion. For defaults: `set -q name; or set name default`.

    PowerShell

    `$Name`. `${Name}` works and is required when the name contains special characters. For defaults: `if ($null -eq $Name) { $Name = 'default' }`.

  • Arrays / lists

    Fish

    Every variable is implicitly a list, **1-indexed**. `set arr a b c; echo $arr[1]` prints `a`. `count $arr` for length.

    PowerShell

    Arrays are explicit `@('a','b','c')`, **0-indexed**. `$arr[0]` prints `a`. `$arr[-1]` for the last element. `.Count` for length.

    1 vs 0 indexing is the most common porting bug. Always re-test loop bounds after porting.

  • Conditional

    Fish

    `if test -f file; ...; end`. Use `test` (also aliased as `[`). Terminator is `end`, not `fi`.

    PowerShell

    `if (Test-Path file) { ... }`. Operators are `-eq`, `-lt`, `-match`, `-like`. Never `==`, `<`, or `=~`.

  • Loops

    Fish

    `for f in *.log; echo $f; end` — terminator is `end`. Glob expansion happens shell-side before the loop runs.

    PowerShell

    `foreach ($f in Get-ChildItem *.log) { Write-Output $f.Name }`. `ForEach-Object` is the streaming cmdlet form.

  • Command substitution

    Fish

    `(cmd)` — bare parentheses. `$(cmd)` is **not valid syntax**; backticks are not supported.

    PowerShell

    `$(expr)` for strings inside `"..."`; `@(cmd)` to force an array; `(cmd)` returns the raw object.

    Parenthesis usage is **opposite** between the two — fish uses `(cmd)` for substitution; PowerShell uses `(cmd)` for grouping and `$(cmd)` for substitution. Easy to mix up when porting.

  • Functions

    Fish

    `function fn; echo $argv[1]; end`. Args are in the list `$argv`. Save with `funcsave fn` so the function autoloads from `~/.config/fish/functions/<name>.fish` next session.

    PowerShell

    `function Fn { param($Name) Write-Output $Name }`. Verb-noun naming convention (linter enforces it). `param()` supports types and default values.

  • Aliases / shortcuts

    Fish

    `abbr -a name expansion` — expands at the prompt so you see the real command before running. `alias name='cmd'` exists but is just a function wrapper.

    PowerShell

    `Set-Alias ll Get-ChildItem` — maps a name to a cmdlet only, no parameters. For "alias with args" you write a function.

  • Arithmetic

    Fish

    `math 1 + 2` or `math "1.5 + 2.5"`. Floating-point supported. The `math` built-in does the work.

    PowerShell

    `1 + 2` is native; works for `[int]`, `[double]`, `[decimal]`. Type matters: `"1" + 2` is `"12"` (string concat), not `3`.

  • Heredocs

    Fish

    No heredoc syntax. Use `printf "line 1\nline 2\n"` or `string join \n a b c | cat`.

    PowerShell

    Here-strings: `@"`(newline)`line 1`(newline)`line 2`(newline)`"@` (interpolating) or `@'...'@` (literal). The closing `"@` / `'@` must be at column 1.

  • Cross-platform reach

    Fish

    Linux / macOS / BSD. No native Windows build — use WSL to run fish on a Windows machine.

    PowerShell

    Windows PowerShell (5.1) is Windows-only. PowerShell 7+ (`pwsh`) is cross-platform — same scripts run on Windows, macOS, Linux.

Side-by-side commands

The 32 most-compared commands in Fish and PowerShell. See all Fish commands · See all PowerShell commands.

  • aliasCreate a shortcut name for a longer command line.
    Fish
    alias ll='ls -la'
    PowerShell
    Set-Alias ll Get-ChildItem
  • awkPattern scanning and processing language for structured text.
    Fish
    awk '{print $1}' file
    PowerShell
    Get-Content file | ForEach-Object { ($_ -split '\s+')[0] }
  • catPrint file contents to standard output.
    Fish
    cat file.txt
    PowerShell
    Get-Content file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    Fish
    chmod 755 file
    PowerShell
    icacls file /grant Users:(RX)
  • cpCopy files and directories.
    Fish
    cp source dest
    PowerShell
    Copy-Item source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    Fish
    curl https://example.com
    PowerShell
    Invoke-WebRequest https://example.com
  • cutExtract sections (fields or characters) from each line.
    Fish
    cut -d',' -f1 file.csv
    PowerShell
    Get-Content file.csv | ForEach-Object { ($_ -split ',')[0] }
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    Fish
    echo "hello world"
    PowerShell
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    Fish
    set -gx NAME value
    PowerShell
    $env:NAME = "value"
  • findLocate files by name, size, time, or other attributes.
    Fish
    find . -name "*.log"
    PowerShell
    Get-ChildItem -Recurse -Filter *.log
  • grepSearch file contents for a pattern.
    Fish
    grep -r "pattern" .
    PowerShell
    Select-String -Pattern "pattern" -Path *.txt
  • headOutput the first lines of a file.
    Fish
    head -n 10 file
    PowerShell
    Get-Content file -TotalCount 10
  • historyShow previously run commands from the shell history.
    Fish
    history
    PowerShell
    Get-History
  • killSend a signal to a process (typically to terminate it).
    Fish
    kill <pid>
    PowerShell
    Stop-Process -Id <pid>
  • lsList directory contents.
    Fish
    ls -la
    PowerShell
    Get-ChildItem -Force
  • mkdirCreate a new directory.
    Fish
    mkdir dir
    PowerShell
    New-Item -ItemType Directory -Path dir
  • mvMove or rename files and directories.
    Fish
    mv source dest
    PowerShell
    Move-Item source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    Fish
    ping example.com
    PowerShell
    Test-Connection example.com
  • psList a snapshot of currently running processes.
    Fish
    ps aux
    PowerShell
    Get-Process
  • rmDelete files and directories.
    Fish
    rm file
    PowerShell
    Remove-Item file
  • sedStream editor for filtering and transforming text.
    Fish
    sed 's/old/new/g' file
    PowerShell
    (Get-Content file) -replace 'old', 'new'
  • sortSort lines of text.
    Fish
    sort file
    PowerShell
    Get-Content file | Sort-Object
  • sshOpen a secure shell on a remote host or run a remote command.
    Fish
    ssh user@host
    PowerShell
    ssh user@host
  • tailOutput the last lines of a file.
    Fish
    tail -n 10 file
    PowerShell
    Get-Content file -Tail 10
  • tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).
    Fish
    tar -czf archive.tar.gz dir/
    PowerShell
    tar -czf archive.tar.gz dir
  • topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.
    Fish
    top
    PowerShell
    Get-Process | Sort-Object CPU -Descending | Select-Object -First 20
  • touchCreate an empty file or update its modification time if it exists.
    Fish
    touch file.txt
    PowerShell
    New-Item file.txt
  • wcCount lines, words, or bytes.
    Fish
    wc -l file
    PowerShell
    (Get-Content file).Count
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    Fish
    wget https://example.com/file.zip
    PowerShell
    Invoke-WebRequest https://example.com/file.zip -OutFile file.zip
  • whichLocate an executable in PATH and print its full path.
    Fish
    which python3
    PowerShell
    Get-Command python3
  • xargsBuild and execute command lines from standard input.
    Fish
    find . -name '*.tmp' | xargs rm
    PowerShell
    Get-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName }
  • zipPackage and compress files into a ZIP archive.
    Fish
    zip -r archive.zip dir/
    PowerShell
    Compress-Archive -Path dir -DestinationPath archive.zip

Gotchas when porting between them

  • Parenthesis usage is opposite: fish `(cmd)` is command substitution; PowerShell `(cmd)` is grouping. Fish `$(cmd)` is a syntax error; PowerShell `$(expr)` is the subexpression operator only valid inside `"..."`.
  • Default text encoding differs: fish writes UTF-8 on every system; Windows PowerShell 5.1 historically wrote UTF-16 LE with BOM for `>` redirect. **pwsh 6+** defaults to UTF-8 (no BOM). Cross-shell file output between the two on PS 5.1 is the #1 source of "why is this file binary garbage" bugs.
  • pwsh 7 on Linux / macOS ships aliases `ls`, `cat`, `cp`, `rm` — they're cmdlets that **reject Unix flags**. Fish users muscle-memorizing `ls -la` get an error in pwsh; use `gci -Force`.
  • Fish does not honor `~/.bashrc` or `~/.profile`. Likewise a `pwsh` session does not read shell rc files — environment variables go in `$PROFILE` (`~/.config/powershell/Microsoft.PowerShell_profile.ps1` on Unix; `Documents\PowerShell\Microsoft.PowerShell_profile.ps1` on Windows).
  • Fish `set -x VAR value` is **export**, not "trace mode" as in bash. PowerShell's nearest equivalent for tracing is `Set-PSDebug -Trace 1` (per-session) — porting bash's `set -x` literally to either shell silently does the wrong thing.

Full references

Other comparisons