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
FishBytes of text, UTF-8 by default. The next command parses lines itself.
PowerShellTyped .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
FishDeliberately non-POSIX. Most bash scripts fail to parse — fish is its own language.
PowerShellNo 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
FishEvery variable is implicitly a list, **1-indexed**. `set arr a b c; echo $arr[1]` prints `a`. `count $arr` for length.
PowerShellArrays 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
FishNo heredoc syntax. Use `printf "line 1\nline 2\n"` or `string join \n a b c | cat`.
PowerShellHere-strings: `@"`(newline)`line 1`(newline)`line 2`(newline)`"@` (interpolating) or `@'...'@` (literal). The closing `"@` / `'@` must be at column 1.
Cross-platform reach
FishLinux / macOS / BSD. No native Windows build — use WSL to run fish on a Windows machine.
PowerShellWindows 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'PowerShellSet-Alias ll Get-ChildItem - awkPattern scanning and processing language for structured text.Fish
awk '{print $1}' filePowerShellGet-Content file | ForEach-Object { ($_ -split '\s+')[0] } - catPrint file contents to standard output.Fish
cat file.txtPowerShellGet-Content file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.Fish
chmod 755 filePowerShellicacls file /grant Users:(RX) - cpCopy files and directories.Fish
cp source destPowerShellCopy-Item source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.Fish
curl https://example.comPowerShellInvoke-WebRequest https://example.com - cutExtract sections (fields or characters) from each line.Fish
cut -d',' -f1 file.csvPowerShellGet-Content file.csv | ForEach-Object { ($_ -split ',')[0] } - echoPrint arguments to standard output, separated by spaces, followed by a newline.Fish
echo "hello world"PowerShellecho "hello world" - exportSet an environment variable and mark it for export to child processes.Fish
set -gx NAME valuePowerShell$env:NAME = "value" - findLocate files by name, size, time, or other attributes.Fish
find . -name "*.log"PowerShellGet-ChildItem -Recurse -Filter *.log - grepSearch file contents for a pattern.Fish
grep -r "pattern" .PowerShellSelect-String -Pattern "pattern" -Path *.txt - headOutput the first lines of a file.Fish
head -n 10 filePowerShellGet-Content file -TotalCount 10 - historyShow previously run commands from the shell history.Fish
historyPowerShellGet-History - killSend a signal to a process (typically to terminate it).Fish
kill <pid>PowerShellStop-Process -Id <pid> - lsList directory contents.Fish
ls -laPowerShellGet-ChildItem -Force - mkdirCreate a new directory.Fish
mkdir dirPowerShellNew-Item -ItemType Directory -Path dir - mvMove or rename files and directories.Fish
mv source destPowerShellMove-Item source dest - pingSend ICMP echo requests to test reachability and round-trip latency.Fish
ping example.comPowerShellTest-Connection example.com - psList a snapshot of currently running processes.Fish
ps auxPowerShellGet-Process - rmDelete files and directories.Fish
rm filePowerShellRemove-Item file - sedStream editor for filtering and transforming text.Fish
sed 's/old/new/g' filePowerShell(Get-Content file) -replace 'old', 'new' - sortSort lines of text.Fish
sort filePowerShellGet-Content file | Sort-Object - sshOpen a secure shell on a remote host or run a remote command.Fish
ssh user@hostPowerShellssh user@host - tailOutput the last lines of a file.Fish
tail -n 10 filePowerShellGet-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/PowerShelltar -czf archive.tar.gz dir - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.Fish
topPowerShellGet-Process | Sort-Object CPU -Descending | Select-Object -First 20 - touchCreate an empty file or update its modification time if it exists.Fish
touch file.txtPowerShellNew-Item file.txt - wcCount lines, words, or bytes.Fish
wc -l filePowerShell(Get-Content file).Count - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.Fish
wget https://example.com/file.zipPowerShellInvoke-WebRequest https://example.com/file.zip -OutFile file.zip - whichLocate an executable in PATH and print its full path.Fish
which python3PowerShellGet-Command python3 - xargsBuild and execute command lines from standard input.Fish
find . -name '*.tmp' | xargs rmPowerShellGet-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName } - zipPackage and compress files into a ZIP archive.Fish
zip -r archive.zip dir/PowerShellCompress-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.