Skip to content
shellmap

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.exe

    Windows only — on every install since NT 3.1 (1993). Not present in WSL's Linux user space; `cmd.exe` is host-only.

    Fish

    Linux / 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.exe

    Bytes 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.

    Fish

    Bytes 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.exe

    Variables expand at **parse time**. `set X=%X% foo` inside a `for` body does not accumulate unless you enable delayed expansion (`setlocal enabledelayedexpansion` + `!X!`).

    Fish

    Normal 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.exe

    Awkward: `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.exe

    No native arrays. Parse text with `for /f` or use space-separated strings and hope filenames have no spaces.

    Fish

    Every 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.exe

    Tab cycles file/dir matches in the CWD. No autosuggestions, no syntax highlighting, no fuzzy match. History scroll via up-arrow only.

    Fish

    Autosuggestions 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 $*
    Fish
    alias ll='ls -la'
  • awkPattern scanning and processing language for structured text.
    cmd.exe
    for /f "tokens=1" %a in (file) do @echo %a
    Fish
    awk '{print $1}' file
  • catPrint file contents to standard output.
    cmd.exe
    type file.txt
    Fish
    cat file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    cmd.exe
    icacls file /grant Users:(RX)
    Fish
    chmod 755 file
  • cpCopy files and directories.
    cmd.exe
    copy source dest
    Fish
    cp source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    cmd.exe
    curl https://example.com
    Fish
    curl https://example.com
  • cutExtract sections (fields or characters) from each line.
    cmd.exe
    for /f "tokens=1 delims=," %a in (file.csv) do @echo %a
    Fish
    cut -d',' -f1 file.csv
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    cmd.exe
    echo hello world
    Fish
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    cmd.exe
    set NAME=value
    Fish
    set -gx NAME value
  • findLocate files by name, size, time, or other attributes.
    cmd.exe
    dir /s /b *.log
    Fish
    find . -name "*.log"
  • grepSearch file contents for a pattern.
    cmd.exe
    findstr /S /I "pattern" *
    Fish
    grep -r "pattern" .
  • headOutput the first lines of a file.
    cmd.exe
    powershell -Command "Get-Content file -TotalCount 10"
    Fish
    head -n 10 file
  • historyShow previously run commands from the shell history.
    cmd.exe
    doskey /history
    Fish
    history
  • killSend a signal to a process (typically to terminate it).
    cmd.exe
    taskkill /pid <pid>
    Fish
    kill <pid>
  • lsList directory contents.
    cmd.exe
    dir /a
    Fish
    ls -la
  • mkdirCreate a new directory.
    cmd.exe
    mkdir dir
    Fish
    mkdir dir
  • mvMove or rename files and directories.
    cmd.exe
    move source dest
    Fish
    mv source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    cmd.exe
    ping example.com
    Fish
    ping example.com
  • psList a snapshot of currently running processes.
    cmd.exe
    tasklist
    Fish
    ps aux
  • rmDelete files and directories.
    cmd.exe
    del file
    Fish
    rm file
  • sedStream editor for filtering and transforming text.
    cmd.exe
    powershell -Command "(Get-Content file) -replace 'old','new'"
    Fish
    sed 's/old/new/g' file
  • sortSort lines of text.
    cmd.exe
    sort file
    Fish
    sort file
  • sshOpen a secure shell on a remote host or run a remote command.
    cmd.exe
    ssh user@host
    Fish
    ssh user@host
  • tailOutput the last lines of a file.
    cmd.exe
    powershell -Command "Get-Content file -Tail 10"
    Fish
    tail -n 10 file
  • tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).
    cmd.exe
    tar -czf archive.tar.gz dir
    Fish
    tar -czf archive.tar.gz dir/
  • topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.
    cmd.exe
    tasklist
    Fish
    top
  • touchCreate an empty file or update its modification time if it exists.
    cmd.exe
    type nul > file.txt
    Fish
    touch file.txt
  • wcCount lines, words, or bytes.
    cmd.exe
    find /c /v "" file
    Fish
    wc -l file
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    cmd.exe
    curl -O https://example.com/file.zip
    Fish
    wget https://example.com/file.zip
  • whichLocate an executable in PATH and print its full path.
    cmd.exe
    where python3
    Fish
    which python3
  • xargsBuild and execute command lines from standard input.
    cmd.exe
    for /f "delims=" %f in ('dir /s /b *.tmp') do del "%f"
    Fish
    find . -name '*.tmp' | xargs rm
  • zipPackage and compress files into a ZIP archive.
    cmd.exe
    tar -a -cf archive.zip dir
    Fish
    zip -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 `;`).

Full references

Other comparisons