Skip to content
shellmap

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

    PowerShell

    Typed .NET objects with properties and methods. `Get-Process | Where-Object CPU -gt 100` filters by an actual numeric property.

    cmd.exe

    Bytes 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

    PowerShell

    Normal lexical scope. A variable set inside a `foreach` is visible after the loop.

    cmd.exe

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

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

    Subroutines via `:label` and `call :label arg1 arg2`. Arguments are `%1`, `%2`, ... `%~1` strips surrounding quotes. `goto :eof` returns. Awkward by design.

  • Cross-platform reach

    PowerShell

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

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

    None. `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-ChildItem
    cmd.exe
    doskey ll=dir /a $*
  • awkPattern scanning and processing language for structured text.
    PowerShell
    Get-Content file | ForEach-Object { ($_ -split '\s+')[0] }
    cmd.exe
    for /f "tokens=1" %a in (file) do @echo %a
  • catPrint file contents to standard output.
    PowerShell
    Get-Content file.txt
    cmd.exe
    type file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    PowerShell
    icacls file /grant Users:(RX)
    cmd.exe
    icacls file /grant Users:(RX)
  • cpCopy files and directories.
    PowerShell
    Copy-Item source dest
    cmd.exe
    copy source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    PowerShell
    Invoke-WebRequest https://example.com
    cmd.exe
    curl https://example.com
  • cutExtract sections (fields or characters) from each line.
    PowerShell
    Get-Content file.csv | ForEach-Object { ($_ -split ',')[0] }
    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.
    PowerShell
    echo "hello world"
    cmd.exe
    echo hello world
  • exportSet an environment variable and mark it for export to child processes.
    PowerShell
    $env:NAME = "value"
    cmd.exe
    set NAME=value
  • findLocate files by name, size, time, or other attributes.
    PowerShell
    Get-ChildItem -Recurse -Filter *.log
    cmd.exe
    dir /s /b *.log
  • grepSearch file contents for a pattern.
    PowerShell
    Select-String -Pattern "pattern" -Path *.txt
    cmd.exe
    findstr /S /I "pattern" *
  • headOutput the first lines of a file.
    PowerShell
    Get-Content file -TotalCount 10
    cmd.exe
    powershell -Command "Get-Content file -TotalCount 10"
  • historyShow previously run commands from the shell history.
    PowerShell
    Get-History
    cmd.exe
    doskey /history
  • killSend a signal to a process (typically to terminate it).
    PowerShell
    Stop-Process -Id <pid>
    cmd.exe
    taskkill /pid <pid>
  • lsList directory contents.
    PowerShell
    Get-ChildItem -Force
    cmd.exe
    dir /a
  • mkdirCreate a new directory.
    PowerShell
    New-Item -ItemType Directory -Path dir
    cmd.exe
    mkdir dir
  • mvMove or rename files and directories.
    PowerShell
    Move-Item source dest
    cmd.exe
    move source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    PowerShell
    Test-Connection example.com
    cmd.exe
    ping example.com
  • psList a snapshot of currently running processes.
    PowerShell
    Get-Process
    cmd.exe
    tasklist
  • rmDelete files and directories.
    PowerShell
    Remove-Item file
    cmd.exe
    del file
  • sedStream editor for filtering and transforming text.
    PowerShell
    (Get-Content file) -replace 'old', 'new'
    cmd.exe
    powershell -Command "(Get-Content file) -replace 'old','new'"
  • sortSort lines of text.
    PowerShell
    Get-Content file | Sort-Object
    cmd.exe
    sort file
  • sshOpen a secure shell on a remote host or run a remote command.
    PowerShell
    ssh user@host
    cmd.exe
    ssh user@host
  • tailOutput the last lines of a file.
    PowerShell
    Get-Content file -Tail 10
    cmd.exe
    powershell -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 dir
    cmd.exe
    tar -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 20
    cmd.exe
    tasklist
  • touchCreate an empty file or update its modification time if it exists.
    PowerShell
    New-Item file.txt
    cmd.exe
    type nul > file.txt
  • wcCount lines, words, or bytes.
    PowerShell
    (Get-Content file).Count
    cmd.exe
    find /c /v "" file
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    PowerShell
    Invoke-WebRequest https://example.com/file.zip -OutFile file.zip
    cmd.exe
    curl -O https://example.com/file.zip
  • whichLocate an executable in PATH and print its full path.
    PowerShell
    Get-Command python3
    cmd.exe
    where python3
  • xargsBuild and execute command lines from standard input.
    PowerShell
    Get-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName }
    cmd.exe
    for /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.zip
    cmd.exe
    tar -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.

Full references

Other comparisons