PowerShell vs cmd.exe
Both ship on Windows, but they're not the same tool — one pipes objects, one pipes text, and the syntax has nothing in common.
Summary
cmd.exe is the legacy Windows shell — a direct descendant of MS-DOS COMMAND.COM, present on every Windows install since NT 3.1 (1993). Its scripting language (.bat / .cmd batch files) is text-only and minimal.
PowerShell is Microsoft's modern shell — released 2006, default since Windows 7. Pipes carry typed .NET objects, scripts (.ps1) are a real programming language, and most operations are verb-noun cmdlets (`Get-ChildItem`, `Set-Content`) rather than tiny external `.exe`s.
New work: PowerShell unless you have a specific reason. cmd survives mainly for legacy `.bat` files, simple one-liners in restricted environments, and the muscle memory of admins who grew up on it.
Syntax & semantic differences
Pipe contents
PowerShellTyped .NET objects with properties and methods. `Get-Process | Where-Object CPU -gt 100` filters by an actual numeric property.
cmd.exeBytes of text. The next command parses lines itself with `for /f` or `findstr`. No structured access.
Variables — assignment & read
PowerShell`$Name = "value"` to assign; `$Name` to read. Variables hold any object, not just strings.
cmd.exe`set NAME=value` to assign (no spaces around `=`); `%NAME%` to read. String-only.
cmd's `set NAME=value with spaces` includes the trailing whitespace silently — `set "NAME=value"` is the safe form. PowerShell has no such trap.
Variable scope inside loops
PowerShellNormal lexical scope. A variable set inside a `foreach` is visible after the loop.
cmd.exeVariables expand at **parse time** unless you enable delayed expansion: `setlocal enabledelayedexpansion` and use `!VAR!` instead of `%VAR%` inside the loop body.
This is the #1 footgun in batch scripting — `set X=%X% foo` inside a `for` loop will not accumulate as written. Use `!X!=!X! foo`.
Conditionals
PowerShell`if (Test-Path file) { ... } else { ... }` — operators are `-eq`, `-lt`, `-match`, `-like` (not `==`/`<`/`=~`).
cmd.exe`if exist file (...) else (...)` — uses string-keyword operators (`exist`, `defined`, `==`, `equ`, `lss`, `gtr`). Whitespace and parenthesis placement is strict.
Loops
PowerShell`foreach ($f in Get-ChildItem *.log) { Write-Output $f.Name }` — pipeline-style.
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 famous for tripping people up.
Script file & execution
PowerShell`.ps1` files. Execution policy gates running them: `Set-ExecutionPolicy RemoteSigned` (per-user) is the usual fix. Run with `.\script.ps1` from PowerShell.
cmd.exe`.bat` or `.cmd` files. No execution-policy concept — double-clicking runs them. `.cmd` is the modern preference (consistent errorlevel behavior with some built-ins).
Command discovery
PowerShell`Get-Command name` (alias `gcm`) shows the cmdlet, function, alias, or external `.exe`. `Get-Help name -Examples` for usage.
cmd.exe`where name` shows external `.exe` matches in `%PATH%`. Built-ins (`dir`, `copy`, `set`) are not files and don't show up in `where`. `help name` or `name /?` for built-in help.
Comments
PowerShell`# single-line` and `<# block #>` multi-line.
cmd.exe`REM single-line` or `:: pseudo-label` (faster, but invalid inside `(...)` blocks — fails silently or breaks parsing).
String quoting
PowerShell`"double"` interpolates `$var` and `$()`. `'single'` is literal. Backtick `` ` `` is the escape character.
cmd.exeDouble quotes are mostly literal; cmd uses `^` as escape for special chars (`&`, `|`, `<`, `>`) outside quotes. Inside double quotes, escape doesn't apply.
Error handling
PowerShell`try { ... } catch { ... } finally { ... }`. Use `-ErrorAction Stop` to make non-terminating errors throw. `$LASTEXITCODE` for external `.exe` exit codes.
cmd.exe`if errorlevel 1 ...` — but only catches errors **at or above** the level you specify. `if %errorlevel% neq 0 ...` for exact equality. No try/catch.
Functions / subroutines
PowerShell`function My-Fn { param($Name) Write-Output $Name }` — named, typed params via `param()`.
cmd.exeSubroutines via `:label` and `call :label arg1 arg2`. Arguments are `%1`, `%2`, ... `%~1` strips surrounding quotes. `goto :eof` returns. Awkward by design.
Cross-platform reach
PowerShellWindows PowerShell (5.1) is Windows-only. PowerShell 7+ (`pwsh`) runs on Linux/macOS/Windows from the same codebase and is the recommended modern target.
cmd.exeWindows-only. Will never run anywhere else.
Unix-name commands
PowerShell`ls`, `cat`, `cp`, `mv`, `rm`, `ps`, `kill`, `pwd`, `man` exist as **aliases** for cmdlets. They reject Unix flags — `ls -la` errors because there's no `-l` parameter on `Get-ChildItem`.
cmd.exeNone. `ls` is "not recognized". `cd`, `dir`, `copy`, `move`, `del`, `tasklist`, `taskkill`, `cd` (prints CWD without args) are the native commands — different names, different flags.
Side-by-side commands
The 32 most-compared commands in PowerShell and cmd.exe. See all PowerShell commands · See all cmd.exe commands.
- aliasCreate a shortcut name for a longer command line.PowerShell
Set-Alias ll Get-ChildItemcmd.exedoskey ll=dir /a $* - awkPattern scanning and processing language for structured text.PowerShell
Get-Content file | ForEach-Object { ($_ -split '\s+')[0] }cmd.exefor /f "tokens=1" %a in (file) do @echo %a - catPrint file contents to standard output.PowerShell
Get-Content file.txtcmd.exetype file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.PowerShell
icacls file /grant Users:(RX)cmd.exeicacls file /grant Users:(RX) - cpCopy files and directories.PowerShell
Copy-Item source destcmd.execopy source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.PowerShell
Invoke-WebRequest https://example.comcmd.execurl https://example.com - cutExtract sections (fields or characters) from each line.PowerShell
Get-Content file.csv | ForEach-Object { ($_ -split ',')[0] }cmd.exefor /f "tokens=1 delims=," %a in (file.csv) do @echo %a - echoPrint arguments to standard output, separated by spaces, followed by a newline.PowerShell
echo "hello world"cmd.exeecho hello world - exportSet an environment variable and mark it for export to child processes.PowerShell
$env:NAME = "value"cmd.exeset NAME=value - findLocate files by name, size, time, or other attributes.PowerShell
Get-ChildItem -Recurse -Filter *.logcmd.exedir /s /b *.log - grepSearch file contents for a pattern.PowerShell
Select-String -Pattern "pattern" -Path *.txtcmd.exefindstr /S /I "pattern" * - headOutput the first lines of a file.PowerShell
Get-Content file -TotalCount 10cmd.exepowershell -Command "Get-Content file -TotalCount 10" - historyShow previously run commands from the shell history.PowerShell
Get-Historycmd.exedoskey /history - killSend a signal to a process (typically to terminate it).PowerShell
Stop-Process -Id <pid>cmd.exetaskkill /pid <pid> - lsList directory contents.PowerShell
Get-ChildItem -Forcecmd.exedir /a - mkdirCreate a new directory.PowerShell
New-Item -ItemType Directory -Path dircmd.exemkdir dir - mvMove or rename files and directories.PowerShell
Move-Item source destcmd.exemove source dest - pingSend ICMP echo requests to test reachability and round-trip latency.PowerShell
Test-Connection example.comcmd.exeping example.com - psList a snapshot of currently running processes.PowerShell
Get-Processcmd.exetasklist - rmDelete files and directories.PowerShell
Remove-Item filecmd.exedel file - sedStream editor for filtering and transforming text.PowerShell
(Get-Content file) -replace 'old', 'new'cmd.exepowershell -Command "(Get-Content file) -replace 'old','new'" - sortSort lines of text.PowerShell
Get-Content file | Sort-Objectcmd.exesort file - sshOpen a secure shell on a remote host or run a remote command.PowerShell
ssh user@hostcmd.exessh user@host - tailOutput the last lines of a file.PowerShell
Get-Content file -Tail 10cmd.exepowershell -Command "Get-Content file -Tail 10" - tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).PowerShell
tar -czf archive.tar.gz dircmd.exetar -czf archive.tar.gz dir - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.PowerShell
Get-Process | Sort-Object CPU -Descending | Select-Object -First 20cmd.exetasklist - touchCreate an empty file or update its modification time if it exists.PowerShell
New-Item file.txtcmd.exetype nul > file.txt - wcCount lines, words, or bytes.PowerShell
(Get-Content file).Countcmd.exefind /c /v "" file - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.PowerShell
Invoke-WebRequest https://example.com/file.zip -OutFile file.zipcmd.execurl -O https://example.com/file.zip - whichLocate an executable in PATH and print its full path.PowerShell
Get-Command python3cmd.exewhere python3 - xargsBuild and execute command lines from standard input.PowerShell
Get-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName }cmd.exefor /f "delims=" %f in ('dir /s /b *.tmp') do del "%f" - zipPackage and compress files into a ZIP archive.PowerShell
Compress-Archive -Path dir -DestinationPath archive.zipcmd.exetar -a -cf archive.zip dir
Gotchas when porting between them
- Don't mix shells in a single script. Calling `cmd /c "..."` from a `.ps1` to dodge a PowerShell quirk almost always introduces a quoting problem on the boundary — fix it in PowerShell instead.
- cmd's `set` with no arguments dumps every environment variable (huge wall of text). PowerShell's `Get-ChildItem Env:` is the equivalent and is filterable.
- PowerShell's `>` redirect writes **PowerShell's formatted output** (object-to-string conversion via the host's default formatter), not the raw bytes a `.exe` would emit. To capture the bytes a tool emits, run it from `cmd.exe` or use `Start-Process -RedirectStandardOutput`.
- cmd handles `&&` and `||` as command separators (run on success / failure) only on Windows 10+. Older Windows / pure cmd had no `&&` — the workaround was `cmd1 && cmd2` rewritten as `cmd1 & if not errorlevel 1 cmd2`.
- Long paths: cmd has historically had a 260-char `MAX_PATH` limit; PowerShell 5.1 inherits it, PowerShell 7+ does not when the per-user/system long-path setting is enabled (`LongPathsEnabled` registry). Watch out when porting scripts that walk deep trees.