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
BashBytes of text, UTF-8 by default on every modern Linux distro. Next command parses lines itself with `grep` / `awk` / `sed`.
cmd.exeBytes 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
BashNormal lexical scope. `for x in *.log; do count=$((count+1)); done` accumulates as written.
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!`).
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.exeAwkward: `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
BashShell expands `*` / `?` / `[abc]` before the command sees them. `shopt -s nullglob` for empty-on-no-match; `shopt -s globstar` for `**`.
cmd.exeGlobbing 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
BashLinux / macOS / BSD / WSL / Git Bash / Cygwin. Effectively everywhere except a fresh Windows install without WSL.
cmd.exeWindows-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.exedoskey ll=dir /a $* - awkPattern scanning and processing language for structured text.Bash
awk '{print $1}' filecmd.exefor /f "tokens=1" %a in (file) do @echo %a - catPrint file contents to standard output.Bash
cat file.txtcmd.exetype file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.Bash
chmod 755 filecmd.exeicacls file /grant Users:(RX) - cpCopy files and directories.Bash
cp source destcmd.execopy source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.Bash
curl https://example.comcmd.execurl https://example.com - cutExtract sections (fields or characters) from each line.Bash
cut -d',' -f1 file.csvcmd.exefor /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.exeecho hello world - exportSet an environment variable and mark it for export to child processes.Bash
export NAME=valuecmd.exeset NAME=value - findLocate files by name, size, time, or other attributes.Bash
find . -name "*.log"cmd.exedir /s /b *.log - grepSearch file contents for a pattern.Bash
grep -r "pattern" .cmd.exefindstr /S /I "pattern" * - headOutput the first lines of a file.Bash
head -n 10 filecmd.exepowershell -Command "Get-Content file -TotalCount 10" - historyShow previously run commands from the shell history.Bash
historycmd.exedoskey /history - killSend a signal to a process (typically to terminate it).Bash
kill <pid>cmd.exetaskkill /pid <pid> - lsList directory contents.Bash
ls -lacmd.exedir /a - mkdirCreate a new directory.Bash
mkdir dircmd.exemkdir dir - mvMove or rename files and directories.Bash
mv source destcmd.exemove source dest - pingSend ICMP echo requests to test reachability and round-trip latency.Bash
ping example.comcmd.exeping example.com - psList a snapshot of currently running processes.Bash
ps auxcmd.exetasklist - rmDelete files and directories.Bash
rm filecmd.exedel file - sedStream editor for filtering and transforming text.Bash
sed 's/old/new/g' filecmd.exepowershell -Command "(Get-Content file) -replace 'old','new'" - sortSort lines of text.Bash
sort filecmd.exesort file - sshOpen a secure shell on a remote host or run a remote command.Bash
ssh user@hostcmd.exessh user@host - tailOutput the last lines of a file.Bash
tail -n 10 filecmd.exepowershell -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.exetar -czf archive.tar.gz dir - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.Bash
topcmd.exetasklist - touchCreate an empty file or update its modification time if it exists.Bash
touch file.txtcmd.exetype nul > file.txt - wcCount lines, words, or bytes.Bash
wc -l filecmd.exefind /c /v "" file - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.Bash
wget https://example.com/file.zipcmd.execurl -O https://example.com/file.zip - whichLocate an executable in PATH and print its full path.Bash
which python3cmd.exewhere python3 - xargsBuild and execute command lines from standard input.Bash
find . -name '*.tmp' | xargs rmcmd.exefor /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.exetar -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.