Skip to content
shellmap

Bash vs cmd.exe

Linux's default scripting shell vs Windows' legacy text shell — the foundational layer of the WSL porting conversation.

Summary

Bash is the default Linux shell — POSIX-extended, the assumed interpreter behind almost every `#!/usr/bin/env bash` shebang on Linux servers, CI runners, and Dockerfiles. UTF-8 text streams, full POSIX parameter expansion, integer arithmetic built in, arrays since bash 3.0.

cmd.exe is the legacy Windows shell — a direct descendant of MS-DOS COMMAND.COM, on every Windows install since NT 3.1 (1993). Pipes carry bytes in the **OEM codepage** (cp437/cp850 by default), variables are strings, scripts (`.bat` / `.cmd`) are a deliberately minimal language whose parse-time variable expansion has not changed in 25 years.

Practical comparison: a developer with a Linux/macOS bash background who needs to write or debug `.bat` files on a Windows machine, or who runs bash inside WSL alongside a host-side `cmd.exe`. Most of what carries over is the *idea* of pipes and stdout; almost no syntax does.

Syntax & semantic differences

  • Pipe contents

    Bash

    Bytes of text, UTF-8 by default on every modern Linux distro. Next command parses lines itself with `grep` / `awk` / `sed`.

    cmd.exe

    Bytes of text, but in the **OEM codepage** (cp437 / cp850 on US Windows) unless you `chcp 65001` to switch to UTF-8 — and even then, several legacy console apps misbehave.

    Piping non-ASCII text between cmd and a WSL bash session via a shared file or clipboard is the #1 source of "why is this file mojibake" bugs.

  • Variable assignment

    Bash

    `name=value` (no spaces around `=`). `export name=value` to put it in the child-process environment.

    cmd.exe

    `set NAME=value` (no spaces around `=` — trailing spaces silently become part of the value). `set "NAME=value"` is the safe quoted form. Always inherited by children.

    cmd's `set NAME=value with spaces` includes the trailing whitespace silently; bash's `NAME="value with spaces"` does not have this trap.

  • Reading variables

    Bash

    `$NAME` or `${NAME}`. Full POSIX parameter expansion: `${NAME:-default}`, `${NAME#prefix}`, `${NAME%suffix}`.

    cmd.exe

    `%NAME%` for normal expansion. Delayed expansion `!NAME!` only after `setlocal enabledelayedexpansion`. No defaults, no prefix/suffix stripping.

  • Variable scope inside loops

    Bash

    Normal lexical scope. `for x in *.log; do count=$((count+1)); done` accumulates as written.

    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!`).

    This is the #1 footgun in batch scripting — a working bash loop transcribed line-for-line into a `.bat` file will quietly fail to accumulate counters.

  • Conditional

    Bash

    `if [[ -f file ]]; then ...; fi`. `[[ ]]` is the bash test built-in; `[ ]` is the POSIX-portable alternative.

    cmd.exe

    `if exist file (...) else (...)`. Operators are keywords: `exist`, `defined`, `equ`, `lss`, `gtr`. Whitespace and parenthesis placement is strict.

  • Loops

    Bash

    `for f in *.log; do echo "$f"; done` — terminator is `done`.

    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 most-cited batch gotcha.

  • Functions / subroutines

    Bash

    `my_fn() { echo "$1"; }`. Args are `$1`, `$2`, `$@`. Functions live in shell memory until exit.

    cmd.exe

    `:label` definitions called with `call :label arg1 arg2`. Args are `%1`, `%2`. `%~1` strips surrounding quotes. `goto :eof` returns. Awkward by design.

  • Command substitution

    Bash

    `$(cmd)` (modern) or backticks (legacy). Output captured cleanly: `today=$(date +%F)`.

    cmd.exe

    Awkward: `for /f "delims=" %i in ('cmd') do set RESULT=%i` interactively, or `%%i` in a `.bat` file. There is no clean inline form.

  • Comments

    Bash

    `# single-line`. No multi-line comment syntax — start every line with `#`, or use the `: <<'EOF' ... EOF` no-op trick.

    cmd.exe

    `REM single-line` or `:: pseudo-label` (faster, but `::` **fails inside `(...)` blocks** — silent parse errors).

  • Globbing

    Bash

    Shell expands `*` / `?` / `[abc]` before the command sees them. `shopt -s nullglob` for empty-on-no-match; `shopt -s globstar` for `**`.

    cmd.exe

    Globbing is **per-command**: `dir *.log`, `del *.tmp` expand them; other commands receive the literal string. A no-match yields the literal pattern as the argument.

  • Path separators

    Bash

    `/` between path components, `:` between PATH entries. Filenames are case-sensitive on most Linux filesystems.

    cmd.exe

    `\` between components, `;` between PATH entries. Filenames are case-**insensitive**. From WSL, translate with `wslpath -w /home/user` or `wslpath -u "C:\Users\x"`.

  • Logical operators

    Bash

    `cmd1 && cmd2` runs on success, `cmd1 || cmd2` on failure, `cmd1 ; cmd2` runs unconditionally. Standard since forever.

    cmd.exe

    `cmd1 && cmd2` and `cmd1 || cmd2` exist on every modern Windows. `cmd1 & cmd2` is the unconditional separator. Escape with `^&`, `^|` outside quotes.

  • Error handling

    Bash

    `set -e` to abort on failure; check `$?` (last exit code) or use `||` short-circuit. `set -o pipefail` for pipelines.

    cmd.exe

    `if errorlevel 1 ...` catches errors **at or above** the given level. `if %errorlevel% neq 0 ...` for exact equality. No try/catch, no `pipefail`.

  • Cross-platform reach

    Bash

    Linux / macOS / BSD / WSL / Git Bash / Cygwin. Effectively everywhere except a fresh Windows install without WSL.

    cmd.exe

    Windows-only. Will never run anywhere else.

Side-by-side commands

The 32 most-compared commands in Bash and cmd.exe. See all Bash commands · See all cmd.exe commands.

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

Gotchas when porting between them

  • WSL is the porting bridge most developers actually use — `wsl bash -c '<command>'` runs bash from cmd; `cmd.exe /c <command>` runs cmd from inside WSL. Note `.bashrc` is **not** sourced by `-c` invocations unless you add `-l` (login).
  • Line endings: bash scripts edited on Windows with `git config core.autocrlf true` get CRLF endings and break under bash with `^M: command not found`. Use `core.autocrlf=input` or run `dos2unix` on the file. The reverse — LF endings in a `.bat` file — also misbehaves on older Windows.
  • Encoding: bash writes UTF-8; cmd writes the OEM codepage (cp437 / cp850). `chcp 65001` switches cmd to UTF-8 for the session, but `more`, `find`, `findstr`, and a handful of legacy tools still misbehave on non-ASCII input even after the switch.
  • Path expansion differs: `~` is the user home in bash, but **literal `~`** in cmd — use `%USERPROFILE%`. `%CD%` is cmd's current directory; bash's `$PWD` is the closest equivalent. `cd /` in bash goes to filesystem root; `cd /` in cmd just goes to the root of the current drive.
  • Quoting: bash double quotes interpolate `$var` / `$(cmd)`; single quotes are literal. cmd quoting is largely **literal** — `^` is the escape character outside quotes for `& | < >`, and **does not work inside double quotes**. A bash one-liner with `echo "It's $USER's $HOME"` pasted into cmd outputs the literal string.

Full references

Other comparisons