cmd.exe vs Fish
Two shells that share less than any other pair on shellmap: a 1993 Windows text-only batch host vs a modern non-POSIX Unix prompt-first shell.
Summary
cmd.exe is Windows' legacy text shell — DOS-derived, shipped on every NT release since 1993, scripting in `.bat` / `.cmd` batch files. Pipes carry bytes, variables are strings, and the language has barely changed in 25 years.
Fish is a deliberately non-POSIX Unix shell — autosuggestions, syntax highlighting, sane defaults, and a small, cleaner scripting language. No native Windows build (use WSL); default on no major OS but a popular opt-in interactive choice on Linux/macOS.
They share no lineage, no OS, and no script compatibility. The realistic compare scenario is a Windows-laptop developer who runs `cmd.exe` host-side and `fish` inside WSL — what changes between those two contexts.
Syntax & semantic differences
Host OS / availability
cmd.exeWindows only — on every install since NT 3.1 (1993). Not present in WSL's Linux user space; `cmd.exe` is host-only.
FishLinux / macOS / BSD. No native Windows build; reach Windows via WSL. Not the default on any major distro — usually `apt install fish` / `brew install fish`.
Pipe contents
cmd.exeBytes of text. Encoding is the **OEM codepage** (cp437 / cp850 on US Windows) unless you `chcp 65001` to switch to UTF-8 — a switch with known limitations in legacy console apps.
FishBytes of text, **UTF-8** by default on every modern system. Non-ASCII filenames flow through pipes without re-encoding.
Piping cmd output into a WSL fish session via `clip.exe` or a shared file is where encoding bugs surface — set `chcp 65001` before redirecting non-ASCII text.
Variable assignment
cmd.exe`set NAME=value` (no spaces around `=`; trailing spaces silently become part of the value). `set "NAME=value"` is the safe quoted form.
Fish`set NAME value` — verb form, whitespace-separated. `set -x NAME value` exports it. `NAME=value` only works as an inline env override: `NAME=value cmd`.
cmd's `set FOO=bar baz` stores `bar baz` as one string; fish's `set FOO bar baz` stores the list `[bar, baz]` — two elements, not one.
Reading variables
cmd.exe`%NAME%` for normal expansion. Delayed expansion `!NAME!` is only available after `setlocal enabledelayedexpansion`.
Fish`$name` works; `${name}` does **not** — fish has no brace expansion of variables. Inside `"..."` interpolates; single quotes do not.
Variable scope inside loops
cmd.exeVariables expand at **parse time**. `set X=%X% foo` inside a `for` body does not accumulate unless you enable delayed expansion (`setlocal enabledelayedexpansion` + `!X!`).
FishNormal lexical scope — a variable set inside a `for` is visible afterwards. No parse-time / runtime distinction to remember.
Conditional
cmd.exe`if exist file (...) else (...)`. Operators are keywords: `exist`, `defined`, `equ`, `lss`, `gtr`. Whitespace and parenthesis placement is strict.
Fish`if test -f file; ...; end`. Use `test` (also aliased as `[`). Terminator is `end`, not `fi`.
Loops
cmd.exe`for %f in (*.log) do @echo %f` interactively, **but** `for %%f in (*.log) do @echo %%f` inside a `.bat` file. The doubled `%%` rule is the #1 batch-scripting gotcha.
Fish`for f in *.log; echo $f; end`. Single `$f` everywhere — no parse-vs-runtime variable doubling.
Command substitution
cmd.exeAwkward: `for /f "delims=" %i in ('cmd') do set RESULT=%i`. There is no clean inline form.
Fish`(cmd)` — bare parentheses, no `$` prefix. `$(cmd)` is **not** valid in fish; backticks are not supported either.
Arrays / lists
cmd.exeNo native arrays. Parse text with `for /f` or use space-separated strings and hope filenames have no spaces.
FishEvery variable is implicitly a list, 1-indexed. `set arr a b c; echo $arr[1]` prints `a`. `count $arr` for length.
Functions / subroutines
cmd.exe`:label` definitions called with `call :label arg1 arg2`. Arguments are `%1`, `%2`. `%~1` strips surrounding quotes. `goto :eof` returns. Awkward by design.
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.
Arithmetic
cmd.exe`set /a x=1+2` — integer-only. Hex with `0x`, octal with a leading `0`. No floating-point.
Fish`math 1 + 2` or `math "1.5 + 2.5"` — supports floating-point. The `math` built-in does the heavy lifting.
Comments
cmd.exe`REM single-line` or `:: pseudo-label` (faster, but **fails inside `(...)` blocks** — silent parse errors).
Fish`# single-line`. No multi-line comment syntax — start every line with `#`.
Interactive UX
cmd.exeTab cycles file/dir matches in the CWD. No autosuggestions, no syntax highlighting, no fuzzy match. History scroll via up-arrow only.
FishAutosuggestions from history (grey ghost-text completes on `→`), inline syntax highlighting, context-aware completions with descriptions, Alt-↑ / Alt-↓ for argument history.
Side-by-side commands
The 32 most-compared commands in cmd.exe and Fish. See all cmd.exe commands · See all Fish commands.
- aliasCreate a shortcut name for a longer command line.cmd.exe
doskey ll=dir /a $*Fishalias ll='ls -la' - awkPattern scanning and processing language for structured text.cmd.exe
for /f "tokens=1" %a in (file) do @echo %aFishawk '{print $1}' file - catPrint file contents to standard output.cmd.exe
type file.txtFishcat file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.cmd.exe
icacls file /grant Users:(RX)Fishchmod 755 file - cpCopy files and directories.cmd.exe
copy source destFishcp source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.cmd.exe
curl https://example.comFishcurl https://example.com - cutExtract sections (fields or characters) from each line.cmd.exe
for /f "tokens=1 delims=," %a in (file.csv) do @echo %aFishcut -d',' -f1 file.csv - echoPrint arguments to standard output, separated by spaces, followed by a newline.cmd.exe
echo hello worldFishecho "hello world" - exportSet an environment variable and mark it for export to child processes.cmd.exe
set NAME=valueFishset -gx NAME value - findLocate files by name, size, time, or other attributes.cmd.exe
dir /s /b *.logFishfind . -name "*.log" - grepSearch file contents for a pattern.cmd.exe
findstr /S /I "pattern" *Fishgrep -r "pattern" . - headOutput the first lines of a file.cmd.exe
powershell -Command "Get-Content file -TotalCount 10"Fishhead -n 10 file - historyShow previously run commands from the shell history.cmd.exe
doskey /historyFishhistory - killSend a signal to a process (typically to terminate it).cmd.exe
taskkill /pid <pid>Fishkill <pid> - lsList directory contents.cmd.exe
dir /aFishls -la - mkdirCreate a new directory.cmd.exe
mkdir dirFishmkdir dir - mvMove or rename files and directories.cmd.exe
move source destFishmv source dest - pingSend ICMP echo requests to test reachability and round-trip latency.cmd.exe
ping example.comFishping example.com - psList a snapshot of currently running processes.cmd.exe
tasklistFishps aux - rmDelete files and directories.cmd.exe
del fileFishrm file - sedStream editor for filtering and transforming text.cmd.exe
powershell -Command "(Get-Content file) -replace 'old','new'"Fishsed 's/old/new/g' file - sortSort lines of text.cmd.exe
sort fileFishsort file - sshOpen a secure shell on a remote host or run a remote command.cmd.exe
ssh user@hostFishssh user@host - tailOutput the last lines of a file.cmd.exe
powershell -Command "Get-Content file -Tail 10"Fishtail -n 10 file - tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).cmd.exe
tar -czf archive.tar.gz dirFishtar -czf archive.tar.gz dir/ - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.cmd.exe
tasklistFishtop - touchCreate an empty file or update its modification time if it exists.cmd.exe
type nul > file.txtFishtouch file.txt - wcCount lines, words, or bytes.cmd.exe
find /c /v "" fileFishwc -l file - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.cmd.exe
curl -O https://example.com/file.zipFishwget https://example.com/file.zip - whichLocate an executable in PATH and print its full path.cmd.exe
where python3Fishwhich python3 - xargsBuild and execute command lines from standard input.cmd.exe
for /f "delims=" %f in ('dir /s /b *.tmp') do del "%f"Fishfind . -name '*.tmp' | xargs rm - zipPackage and compress files into a ZIP archive.cmd.exe
tar -a -cf archive.zip dirFishzip -r archive.zip dir/
Gotchas when porting between them
- cmd is host-only on Windows — you cannot run cmd inside WSL, and you cannot run fish from a `.bat` file. Cross-shell scripting between them happens via `wsl <fish-cmd>` or `cmd.exe /c <cmd-batch>` boundaries.
- Path separators: cmd uses `\` and `;` between PATH entries; fish uses `/` and `:`. From WSL, translate with `wslpath -w /home/user` or `wslpath -u "C:\Users\x"`.
- cmd `&&` runs the next command on success, `||` on failure, and `&` runs unconditionally. Fish has `&&` / `||` since 3.0 (2019); older fish docs still show `; and` / `; or`, both of which remain valid and idiomatic for scripts.
- cmd `set` with no args prints **every environment variable** — a wall of text. Fish `set` with no args prints every shell variable (read-only and non-exported included); `set --show NAME` is the focused alternative.
- Escape characters differ in both directions: cmd uses `^` outside quotes to escape `&|<>`; fish uses `\`. A cmd one-liner with `^&` pasted into a fish prompt becomes literal `^&` (which fish does not interpret as a separator anyway — use `;`).