Machine-readable rules live in data/expansion.yaml
(schema: schema/expansion.schema.json).
Primary reference text is captured under audit/cmd-help/ (assoc-help.txt, attrib-help.txt, break-help.txt, call-help.txt, cd-help.txt, certutil-help.txt, choice-help.txt, clip-help.txt, cls-help.txt, cmd-help.txt, color-help.txt, comp-help.txt, copy-help.txt, date-help.txt, del-help.txt, dir-help.txt, doskey-help.txt, dpath-help.txt, echo-help.txt, endlocal-help.txt, exit-help.txt, fc-help.txt, find-help.txt, findstr-help.txt, for-help.txt, forfiles-help.txt, ftype-help.txt, goto-help.txt, help-help.txt, hostname-help.txt, icacls-help.txt, if-help.txt, keys-help.txt, label-help.txt, md-help.txt, mklink-help.txt, mode-help.txt, more-help.txt, move-help.txt, msg-help.txt, path-help.txt, pause-help.txt, popd-help.txt, print-help.txt, prompt-help.txt, pushd-help.txt, rd-help.txt, reg-help.txt, reg-query-help.txt, rem-help.txt, ren-help.txt, replace-help.txt, robocopy-help.txt, set-help.txt, setlocal-help.txt, setx-help.txt, shift-help.txt, sort-help.txt, start-help.txt, subst-help.txt, taskkill-help.txt, tasklist-help.txt, time-help.txt, timeout-help.txt, title-help.txt, tree-help.txt, type-help.txt, ver-help.txt, verify-help.txt, vol-help.txt, waitfor-help.txt, where-help.txt, whoami-help.txt, xcopy-help.txt, chcp-help.txt).
-
Percent-tilde (
%~) - requires Command Extensions; letter modifiers (order-independent, case-insensitive), path search (%~$ENV:n, empty on miss; walks only directories listed in ENV — CWD is not implicit unless listed; letter+$combos such as%~dp$PATH:1), bare quote-strip (%~1; at most one leading and one trailing quote by position), attribute mask (%~a), short-name full paths (%~sf), locale timestamps (%~t), bare-vs-fqualification (combinedFwithn/x/d/pstill yields the full path), directory operands forz/a/t, the multi-digit batveat (%~10is%~1plus literal0), and%~dp0trailing-backslash quote caution ("%~dp0"foocan break quoting; prefer"%~dp0foo"). With extensions off,%~forms are not expanded (literal~...). Invalid forms (%~*, unknown letters such as%~q1,%~name%spellings) are live syntax errors and the grammar reports them as such. Theinvalid_combinationsletter-regex lists both cases (nxfpdstaz/NXFPDSTAZ) so uppercase forms such as%~DPNX0are not false-positive rejects. -
Percent expansion - in scripts, undefined
%name%/!name!expand to empty; on the interactive prompt undefined%name%often remains literal (script-vs-interactive modes also include%%ivs%iFOR metavars and SET /A%%vs%modulo); incomplete unclosed%forms are not successful expansions (leading%typically stripped, leaving trailing text as literals); percent expands across the whole physical line before&/&&/||segments run (set "x=2"&echo %x%sees the pre-line value); adjacent%...%pairs can eat intervening text as a name (echo Between 60% and 80% of X→Between 60 of Xwhenand 80is undefined — prefer%%for literal percents; when the intervening name is creatable and defined, its value substitutes —echo 60%foo%80withset foo=DEFINED→60DEFINED80; SET strips leading spaces from names so theand 80form is not creatable via ordinary SET); expanded&/|/<>re-enter command parsing (quote or use delayed expansion for poison values) -
Delayed expansion (
!var!) - disabled by default; does not require SETLOCAL; enable via cmd/V:ON, SETLOCAL flags, or the Command Processor registry value. Independent of Command Extensions (plain!var!works with extensions off; substring/replace still need extensions). When disabled,!var!is literal. Supports substring/replace peers of the percent forms (case-insensitive search), FOR accumulate!LIST!, indirect!%name%!(percent then delayed); in-block!prefix%name%!still uses the pre-block percent value (use FOR metavar / CALL reparse such ascall echo %%food!city!%%); SET-time values containing!can corrupt under delayed-on assignment (prefer assign with delayed off — a single^!inside SET is not reliable); intact bang-bearing values are mangled by%var%re-scan under delayed expansion while!var!keeps embedded!; digit-leading names need bang forms;!n!is an env var namedn, never batch parameter%n(assignset "arg=%~1"first);!escaping under delayed expansion is phase-sensitive (^!insufficient;^^!→!,^^^^!→^). CALL can force a second percent-expansion pass (call set "out=%%%name%%%"). Disable via SETLOCAL DisableDelayedExpansion orcmd /V:OFF. SETLOCAL EnableDelayedExpansion takes effect for later&-chained commands on the same physical line (unlike whole-line percent binding). -
FOR variables / forms -
%%iin batch files,%ion the interactive command line; letter charset (letters preferred; digits/punctuation accepted — including bare%%~as a metavar letter — but easy to clash with%0-%9);%%~~is quote-strip of metavar~; unknown FOR tilde letters such as%%~qfare not syntax errors (unlike parameter%~q1); adjacent literal text sticks after expansion (%%n0is metavariablenplus literal0); undeclared%%letterbecomes literal%letter;/D/R/L/Fforms (extensions); FOR/Rroot path may include a trailing\; FOR/Rwith(.)includes the walk root (depth-first on live cmd);/D /Rwith(*)lists subdirs only and commonly enumerates siblings before descending into a sibling's children;*matches the rest of a name component (including dots); mid-mask?matches exactly one character while trailing/?.?may match fewer; short 8.3 names can satisfy masks the long name would not; FOR/Rwithout wildcards synthesizesroot\nameunder each directory; FOR metavars expand in the DO body and share a session letter namespace (nested same-letter restores after inner); classic FOR non-wildcard set members are literals even when missing; unmatched*/?masks iterate zero times (prior ERRORLEVEL unchanged; not EL 5); empty/(,,)sets iterate zero times while("")is one empty-quoted member; unquoted classic set members split on space/tab/comma/semicolon/equals (quoted members stay one iteration); multiple masks/members are allowed (*.txt *.csv);FOR %%i IN (%*)expands%*then re-splits with those delimiters;GOTOfrom a DO body exits the loop early for classic//F//Lwith non-zero step (BREAKdoes not); FOR/Lempty ranges (start past end) run zero times; FOR/Lstep0never terminates and is not escaped byGOTO/EXIT /B; space/tab required betweenIN/DOand(— gluedin(/do(and newline-before-(are live syntax errors -
FOR /F -
eol/skip/delims/tokens/usebackq(and liveusebacksynonym), quote forms, consecutive-delimiter collapse (empty fields are skipped / tokens shift; leading delimiters likewise; honor empty fields by substituting a placeholder for,,before FOR /F), emptydelims=, space-must-be-last indelims(preferdelims=last among options so a trailing space sits before the closing quote), case-sensitive delimiter chars, default first token;tokens=implies further metavars by ASCII succession from the declared letter (live continues pastz/Z, e.g. 27th from%%ais%%{, despite FOR /? “26” wording); a single FOR /F still hard-caps at 31 selected token indexes (tokens=1-31iterates;tokens=1-32/tokens=32silently run zero times);tokens=indexes are sorted ascending before binding (tokens=5,7,1-3≡tokens=1-3,5,7); sparsetokens=1,3assigns selected tokens to successive metavars with no empty slot for skipped indexes; duplicate indexes still allocate successive metavars but later duplicates are empty; trailing remainder may betokens=1*ortokens=1,*; options string may come from percent/delayed expansion; distinct option keywords are order-independent (only repeated keywords last-wins);tokens=*still strips leading delimiters before assigning the remainder (use emptydelims=to keep leading spaces); baretokens=*on a delimiter-only line still iterates once with an empty metavar, whiletokens=1,*/ numeric tokens skip those lines;eol=takes exactly one comment character (extra characters in the sameeol=value commonly break parsing); emptyeol=removes the default semicolon comment character on live Windows 10/11 (a space aftereol=installs space as the eol char — prefer an unused expliciteol=when a comment character is still needed); repeated option keywords use the last occurrence; blank lines in file/command-output input are skipped;skip=ncounts physical lines (including blanks and eol-comment lines) and must be ≥1 when present — omitskipto skip nothing (skip=0is a syntax error); whitespace-only lines are also skipped with default delimiters but kept as a spaces-only token with emptydelims=(a quoted("a" "" "b")file-set is not a blank-line probe); multi-file filesets open in listed order andskip=napplies per file; unquoted fileset members also split on,/;/=; fileset wildcards do not expand (usedir /bcommand form); FOR /F does not clear/set ERRORLEVEL by itself; UTF-16 inputs often needTYPEbefore/F; withoutusebackq, double-quoted string input may contain paired embedded""; withusebackq, parentheses inside single-quoted strings commonly need^(/^); bare file-set names resolve from the CWD (or an explicit path) and do not search%PATH%; command-output form captures stdout only (stderr still prints but is not tokenized); live syntax rejects include non-numericskip=, zeroskip=0/tokens=0(and other zero indexes), malformedtokens=(e.g.1-2-3,1,,2, non-numeric), and multi-charactereol=— these option-string rejects are non-fatal (later statements still run; contrast aborting%~q1); validated structurally by the grammar for both quoted and unquoted caret-escaped option text (delims=semantics are catalog-only) -
Caret escaping -
2^n-1for ordinary multilevel hops; CALL doubles carets on its tail (including inside quotes); line-continuation caret must be the last character of the physical line (CRLF is two bytes escaped by one^; percent runs on that physical line before the join, so%names cannot span the break); caret does not escape%(percent expansion runs first; use%%for a literal percent in scripts); when ECHO-writing child IF/FOR blocks, emit^(...)so parentheses survive the write pass -
Double percent - batch
%%literals; CALL also reduces%%pairs to%on its argument tail; when ECHO-writing a child.bat/.cmd,%%in the parent becomes%in the child (defer expansion), while a single%name%expands while writing; caret write-hops reduce one escape pass per generation (^^^>→^>); with delayed expansion on,^^!var^^!writes!var!for the child -
String ops - require Command Extensions; substring with negative offsets/lengths and omitted length; past-end on a populated string yields empty (distinct from undefined/empty → literal
~offset); length past end returns the remainder; replace-all; empty replacement deletes;*prefix replace; case-insensitive%var:old=new%search; missing/empty substring batveat (%NOSUCH:~-1%/ afterSET name=yields literal~-1); missing/empty replace batveat (%NOSUCH:a=b%/ afterSET name=yields literala=b, and*forms yield*a=b; delayed!…!peers match); with extensions off, substring/replace forms expand to empty; substring/replace do not apply to batch parameters (%1:~0,2%/%1:old=new%leave operator text literal — assignset "s=%~1"first) -
SET /A - requires Command Extensions; operators with documented precedence and same-tier left-associativity (
8/2*2→8), grouping, comma separator, hex/octal (no_binary_literal: not0bbinary;08/09invalid as literals), undefined-as-zero, bare names (silent leading-integer truncation of non-integer env values; bare vs%name%diverge on decimals; barev=010is octal8while bare08/09truncate to0with EL 0), 32-bit wrap on overflow, signed<</>>arithmetic shifts (SET /? says "logical shift"; live cmd is signed/arithmetic), quoting rules (shell_metachar_quoting: unquoted<<is a syntax error; unquoted>>is append redirection; unquoted^escapes before arithmetic; prefer quotes or caret escapes for&|^<<>>); tokens after a quoted/Aexpression stay on the SET /A statement (often Invalid number / Missing operator — not a discarded plain-SET trailer); unary!interacts with delayed expansion; percent-expanded LHS names (set /A %~1=...) and delayed!%~1!read/write when the argument holds a variable name; divide-by-zero / invalid literals leave non-zero ERRORLEVEL (host-specific codes); decimal literals can fail yet partial-assign; expression-only forms (set /A 1+2) are valid (print interactively, silent in scripts); with extensions off, unquotedset /A N=1+1is a plain assignment whose name includes/A(quotedset /A "..."is a syntax error); fractional display uses scaled integers (e.g.set /A out=125*10/100), not native float; the grammar exposes a structuredsetAExprtree for/A -
Plain SET assignment - spaces around
=become part of the name and/or value; prefix query (SET P, extensions); quotedSET "name=value"requires extensions; text after the closing quote (glued or spaced) is discarded and not executed until&/&&/||/|; redirects may follow (set "g=ok">file); missing name/prefix sets ERRORLEVEL 1;SET name=unsets;.batvs.cmdERRORLEVEL matrix after successful SET/PATH/PROMPT/ASSOC/FTYPE (and SET /A / SET /P); APPEND is absent on modern hosts -
SET /P - requires extensions; optional prompt (prompt text is display-only on stdout, never taken from a pipe/redirect or
%*); EOF/NUL keeps prior value; a blank input line (Enter with no text) also keeps the prior value (does not assign empty / cannot clear via blank Enter); a spaces-only line assigns those spaces;SET /P var=<filereads the first line only; pipe-side SET /P (and plain SET / SETLOCAL) updates only the child cmd environment -
Environment variable names -
=forbidden in names; a literal%cannot be stored in a name (percent expansion eats it on the SET line); live cmd accepts.,-,~, spaces, lone;, and punctuation such as@#$;[]and)(prefer underscore-alnum for portability). Quoted SET can also define hazardous names containing&/</>(for exampleset "a&=1"). Names starting with a digit or*are not reachable via%name%(%0-%9/%*win); use delayed!1abc!for digit-leading names. Lexer%name%is a single PERCENT_VAR for any name chars other than%,=, or newlines (and not digit/*-leading). Bracketed names such asarr[1]/arr[!i!]are ordinary env vars used as array/hash idioms, not a separate language type. -
ECHO - blank-line forms (
ECHO.ECHO:ECHO/ECHO[ECHO]and peers;ECHO(is WORD plus LPAREN); bare/whitespace-only ECHO prints on/off status;ECHO ON/OFFand@suppression;ECHO OFFdoes not suppress stderr; withECHO ON, parenthesized IF/FOR bodies still appear on stdout even when they do not run (percent already resolved in the echo; delayed!var!often stays literal) -
CMD processor switches -
/Vdelayed expansion;/ECommand Extensions;/Qecho off;/Ddisable AutoRun;/AANSI //UUnicode pipe/file output;/F:ON|OFFcompletion (Ctrl-F / Ctrl-D and CompletionChar registry values);/T:fgcolors; defaults (extensions on, delayed off); compatibility aliases/X=/E:ON,/Y=/E:OFF,/R=/C;/C/K/Squote-stripping; AutoRun registry unless/D -
Command Extensions off - disable via
cmd /E:OFF,/Y, registry, orSETLOCAL DisableExtensions; base IF ERRORLEVEL/==/EXIST remain; compare-ops//I/DEFINED/CMDEXTVERSION, GOTO:EOFspecial target, CALL:labeljump/%*/%~, SET/A//P, quoted SET, prefix query, string ops, FOR/D//R//L//F, SHIFT/n(under OFF,shift /1behaves like bare SHIFT and still moves%0— it does not preserve%0), dynamic env names, CD/D, ASSOC/FTYPE/COLOR, and PROMPT$+/$Mrequire extensions; delayed expansion remains independently switchable; under OFF,set /Aandset /Pbecome literal plain assignments whose names include the/Aor/Ptoken; prefix-querySET nameis a live syntax error under OFF -
SETLOCAL options - four Enable/Disable Extensions and DelayedExpansion flags (precedence over CMD
/E//V; SETLOCAL /? still says "two valid arguments" while listing all four; same-category duplicates last-wins; quoted flags rejected with "Invalid parameter" and ERRORLEVEL 1); bareSETLOCALinherits the current Enable/Disable state into a new nested scope; nesting limit 32 per CALL level ("Maximum setlocal recursion level reached." — overflow does not change ERRORLEVEL and execution continues); ENDLOCAL is CALL-level scoped and ignores trailing args; argument ERRORLEVEL probe; ENDLOCAL restores prior environment, Extensions/DelayedExpansion state, and current directory (not the PUSHD stack);endlocal & set "out=%in%"same-line (or paren-block, including multi-lineendlocalthenset) survive trick; after ENDLOCAL inside( )with delayed on,%var%still shows the local value while!var!shows the restored outer value -
ERRORLEVEL / CMDEXTVERSION -
IF ERRORLEVEL nmeans>= n; classicIF ERRORLEVEL/CMDEXTVERSIONleading zeros are decimal (010→>= 10), unlike compare-ops octal; dynamic%ERRORLEVEL%/%CMDEXTVERSION%env-var shadowing (classicIF ERRORLEVEL/IF CMDEXTVERSIONstill read the internal code / version);cmd /C exit Nresets ERRORLEVEL without shadowing; barecall/(call)force ERRORLEVEL 1 andcall/(call )(trailing space) force 0; CHOICE sets ERRORLEVEL to the 1-based choice ordinal (255 on tool error; CTRL+C/BREAK returns 0; /CS /T /D switches); CMDEXTVERSION starts at 1 and never true when extensions are off; live Windows 10/11 reports%CMDEXTVERSION%=2; concatenated digit probes such as0000stillEQU 0under numeric compare-ops -
Dynamic environment variables -
%CD%,%DATE%,%TIME%,%RANDOM%,%ERRORLEVEL%,%CMDEXTVERSION%,%CMDCMDLINE%,%HIGHESTNUMANODENUMBER%(SET /?; extensions required); undocumented%=ExitCode%(8-digit hex after an external exit code) /%=ExitCodeAscii%(printable LSB; empty for non-printables such as 0/1/10).EXIT /Bupdates ERRORLEVEL but does not refresh%=ExitCode%on live Windows 10/11 cmd. Hidden from SET;=names cannot be SET-shadowed.%TIME%often space-pads hours 0-9 (%TIME: =0%zero-pads);%DATE%/%TIME%follow locale-specific DATE/TIME formats (separators may include comma);SET CD=...shadows%CD%without changing process CWD;SET DATE=/SET TIME=/SET RANDOM=freeze those expansions until cleared (TIME does not tick; RANDOM does not advance);IF DEFINEDis true for the dynamic names even when unshadowed and absent from SET listings; near-simultaneous cmd processes often share correlated%RANDOM%seeds;%RANDOM% %% Nis biased unless N divides 32768; ordinary startup env (COMPUTERNAME,USERNAME,TEMP, …) appears in SET listings and is distinct from these dynamic names;%CMDCMDLINE%is process-original (Explorer-style launches often embed the script path; interactive consoles often show only comspec) and unchanged across in-processCALLof other scripts -
Keyword boundaries - do not glue keywords to
%,!, quotes, or)(IF%1,SET%x%,rem),if)are not IF/SET/REM); also require space/tab before parenthesizedIN/DO/ELSEbodies (in(/do(/else(are syntax errors). IF may glue(immediately after the IF keyword (if(1==1)is a paren-wrapped predicate, usually silent-false); a true then-body after a complete predicate still needs space/tab before((if 1==1 (echo T), notif 1==1(echo T)) -
IF forms / parentheses - base and extension predicates; EXIST (not EXISTS) for files and directories; a trailing
\on an EXIST operand is true for an existing directory and false for a normal file path that lacks that slash form;IF EXIST nulis true whilenul\is false (avoidIF EXIST con— can block); IF DEFINED cannot address names that contain spaces (first token only; quoted"a b"does not match); IF DEFINED is true for dynamic names (CD/DATE/TIME/RANDOM/…) even when unshadowed and absent from SET; unquoted.%v%padding can collapse a trailing space into inter-token whitespace soif .%v% equ .sadis true afterset "v=sad "while quoted==still sees the space; quoted compare sides are string compares; string order is not raw ASCII (digits before letters; letter-case chaina<A<b<B); letter-vs-digit unquoted compares are string compares (AGTR9); compare-ops accept octal/0xhex, leading+/-, and clamp out-of-range digit operands to signed 32-bit; classicIF ERRORLEVEL/CMDEXTVERSIONaccept-nand%var%number slots; trailing spaces in operands are significant (not stripped); unquoted empty operands are a syntax error; classic.%var%.padding works only for simple values (breaks on spaces); no nativeand/orkeywords inside IF (and/orafter a complete predicate become the THEN command, or a following separate unknown command — often EL 9009; an orphan multi-line(block)after a trailing(still runs, while same-lineand 2==2 (echo X)does not); joined-operand equality such asif "%a%-%b%" equ "X-Y"can stand in for AND but collides when values contain the delimiter;else ifis same-line ELSE plus another IF (not a separate keyword); open(for a then-body on the same line as the predicate with space/tab before((if 1==1 (echo T); gluedif 1==1(echo T)is not a true then-body); ELSE same-line attachment (newline-detached ELSE is an unknown command; a following orphan(block)may still run); ELSE parentheses are optional for a single same-line false-path command; parenthesized ELSE bodies require space/tab before((else(echo F)is a live syntax error;else (echo F)is valid); paren-wrapped predicates such asif(1==1)/if (1==1)are not C-style grouping (usually silent-false); interior space/tab immediately inside both wrapping parens (if ( 1==1 ),if( 1==1),if(1==1 )) is a live syntax error;==splits on the first operator only;/Iis tolerated before non-compare predicates;IF %ERRORLEVEL% nwithout a compare-op is a syntax error; the full predicate text may come from expansion (set "b=a==a"/set "b=true==true"thenif %b%/if not %b%; expansion does not inject spaces around==) -
Command chaining -
&,&&,||,|, and parenthesized groups; spaces around operators are optional (ver>nul&&echo ok||echo failmatches spaced chaining); on live cmd&&binds tighter than||, and|binds tighter than&/&&/||;&&/||are not the same asIF %ERRORLEVEL% EQU 0(ECHO/REM/.batSET and CALLed scripts can succeed without clearing ERRORLEVEL — e.g.cmd /c exit 5 & echo x && echo ANDstill runs AND with ERRORLEVEL 5); after a successful||alternative later alternatives are skipped (including a trailing&&grouped into a later alternative); pipe sides run in concurrent child cmd contexts (child delayed/extensions default independently of parent SETLOCAL); parent ERRORLEVEL after a pipe is the rightmost stage's exit code; with parent delayed expansion on,!var!in the pipeline text is still expanded by the parent;A && (B) || (C)runs C when B fails even after successful A; bare trailing&inside( )is a syntax error; each&statement carries its own redirects -
Redirection -
>,>>,<,n>,>&,<&handle duplication, NUL suppress, group redirects, leading redirects, left-to-right handle order; in2>&1the&is handle-duplication syntax (not the command separator); a handle digit before>/>>/<must be its own whitespace-separated token (echo 2>nul/echo hello 1>f.txt) — a digit glued to prior text stays data (echo hello1>f.txtwriteshello1); after expansion, a trailing separate digit becomes the handle (set "msg=Meet at 2"thenecho %msg%>file→ handle 2; prefer leading>file echo %msg%); output redirects create/truncate the target before the command runs (a failing command may still leave a 0-byte file); each&-chained statement carries its own redirects; redirectingCALL :labelcaptures the whole CALLed context (including nested CALL output) until return; when stdout is redirected to a file, interactive prompts may hang or write into the redirect — leading>CONkeeps prompts on the console; pipe×redirect ownership matters (cmd | find >outvscmd >out | find); bare redirect-only statements and bash-like&>fileare syntax errors -
Parenthesis-block expansion -
%var%expands when the block is parsed;%0–%9/%*/%~are likewise frozen for the block (SHIFT inside( )does not change those percent forms until after));!var!expands at execution when delayed expansion is on; nested blocks still expose only outermost-parse and current values; dual pre-block / in-block values enable swap patterns; a naked(...)group (not IF/FOR body) is a valid command group used for dual-value swaps and ENDLOCAL export -
Batch parameters -
%*and%~require Command Extensions (literal*/~...when off); base%0-%9work without extensions;%10is%1plus literal0;%0spelling mirrors CALL/invocation text (Explorer-stylecmd /c ""fullpath""commonly yields a quoted full-path%0; relative CLI typing keeps the as-typed spelling); do not redirect into%0(can overwrite the running script); drag-and-drop / Open-with of multiple files typically delivers each path as its own quoted argument; SHIFT/nand bare SHIFT (invalid/nprints an error, sets ERRORLEVEL 1, and continues);SHIFT /1preserves%0while shifting%1upward;%*is%1 %2 ...(never includes%0) and is unaffected by SHIFT; empty quoted""occupies a slot (%1is"",%~1empty); unquoted args split on space/tab/comma/semicolon/equals (a=b→ two slots while%*keepsa=b); substring/replace suffixes do not apply to%n/%~n(assign to an env var first) -
CALL / GOTO -
CALL :labelreturn context (fresh%1/%*unless args are passed); CALL requires colon for labels (call namewithout:searches PATH, not:name); gluedgoto:eof/call:labelaccepted; missing CALL label continues with ERRORLEVEL 1;CALL :EOFis not special (ordinary missing-label search unless a user:EOFexists — unlikeGOTO :EOF); successful CALL withoutEXIT /Bpreserves prior ERRORLEVEL;EXIT /BafterCALL :labelreturns whileEXIT /BafterGOTO :labelends the script; bareCALLof a successful non-label non-script command (for examplecall echo ok/call set ...) clears ERRORLEVEL to 0;CALL other.cmdreturns the child's final ERRORLEVEL (ECHO/REM often do not clear, so a child that only echoes can preserve the pre-CALL code;EXIT /B nsets it); bare script invoke does not return (CALL does); bare external.exereturns without CALL; CALL inherits the caller's CWD (cmd does not auto-cd to the script directory); CALL context runs through later labels until EOF /GOTO :EOF/EXIT /B(unfenced fallthrough to physical EOF returns; a later mainline EOF exits); deep recursive CALL aborts near stack limits ("BATCH RECURSION exceeds STACK limits"; host-dependent depth, separate from SETLOCAL's 32 cap);GOTO :EOFvsGOTO EOF; bareGOTOwith no target ends the context ("No batch label specified"); case-insensitive user labels; duplicate labels use the first match in file order;goto labdoes not match a distinct longer label:label; spaced label lines are prefix-matched (goto has/goto :hashits:has space) -
Expanded GOTO/CALL targets -
goto %name%/CALL :%name%resolve after percent (or delayed) expansion; missing targets follow ordinary GOTO/CALL missing-label rules -
Label charset - label lines consume the rest of the physical line (spaces and punctuation allowed); indented labels are accepted (prefer column 0); ANTLR LABEL matches that form; CALL uses the first token after
:as the label and the rest as arguments, while GOTO uses the remainder of the statement as the target (sogoto :has spacetargets:has space, andgoto :hasalso matches that spaced line);::is the remark form; later colons in jump labels (for example:ok:extra) are allowed -
EXIT - bare
EXITends the cmd process;EXIT n(no/B) ends the process with that exit code;EXIT /Bends the script/routine; omit exitCode on/Bto preserve ERRORLEVEL on CALL return, or pass n to set it;EXIT /Bparses a leading signed decimal integer from exitCode and ignores trailing junk (EXIT /B 12x→ 12;EXIT /B 0x10→ 0, not hex 16); a fully non-numeric token (EXIT /B abc) preserves the prior ERRORLEVEL like bareEXIT /B(does not force 1); top-level bareEXIT /Bundercmd /Cmay still yield process exit 0 -
Remarks -
REMvs::label-style remarks; REM as the command verb (line-start or after&/&&/||) remarks out the rest of the physical line (including a trailing&and any>redirect on that line);REM /?is the help exception (prints usage and may redirect); glued forms such asREMCASEare not REM; delimiter-glued forms (rem.rem/rem:rem]rem[and peers, analogous toecho.) still invoke REM at runtime and remark out the rest of the line (rem. & echostill runs the trailing command);rem)/rem{/rem@and similar peers are unrecognized non-REM tokens;rem(remains the REM keyword token; percent expansion still runs on REM lines (delayed!typically stays literal); a::line containing)inside( )can close the block early on some hosts -- prefer REM in paren blocks; real jump labels must not use:in the second character (::remark), but later colons (for example:ok:extra) are valid -
PROMPT
$codes -$P$G,$T,$$, and extensions$+/$M(PROMPT /?); bare PROMPT restores displayed$P$Gand clears the PROMPT env var -
Command-line length - cmd.exe accepts at most 8191 characters on a command line
-
Quoting - double quotes suppress
&|<>^()for command parsing but do not suppress percent or delayed!expansion; embedded""pairs inside a quoted arg are retained after%~outer-quote strip ("a""b"->a""b); only ASCII"(U+0022) quotes arguments — typographic curly quotes are ordinary characters -
Script encoding - UTF-8 BOM prefixes the first token (breaking
@echo off); UTF-16 BOM typically garbles the line worse; prefer BOM-less ASCII/code-page text; CRLF preferred, LF usually works -
Command resolution - cwd then PATH with PATHEXT; bare missing external name sets ERRORLEVEL 9009;
CALLof a missing external sets ERRORLEVEL 1; internals (ECHO/DIR/SET/…) are never shadowed by cwd files, but external tools (for examplewhere.exe/label.exe) can be shadowed by a same-named.bat/.cmdin the CWD -
Directory commands - CD
/Dchanges drive; bareD:/Z:switches the process to that drive's saved cwd; with extensions, CD normalizes case and accepts unquoted paths with spaces; missing CD path leaves ERRORLEVEL 1; PUSHD/POPD stack (bare PUSHD lists; local path does not map a drive; UNC maps Z:↓; POPD takes no args) (PUSHD /?) -
MKDIR/MD - with extensions, creates missing intermediate directories; without extensions parents must exist (MD /?)
-
RMDIR/RD -
/Sremoves a directory tree;/Qquiets/S; tree removal remains available with extensions off (RD /?) -
COLOR - two hex digits for background/foreground (COLOR /?: background then foreground; some Learn pages invert that order in prose; CMD
/T:fguses the same bg-then-fg digit order despite thefgplaceholder wording), or a single hex digit to set foreground only; COLOR /? documents ERRORLEVEL 1 for same fg/bg; Microsoft Learn/SS64 describe success as 0, but live Windows 10/11 cmd leaves ERRORLEVEL 1 after successful COLOR too (not &&-friendly); unavailable when extensions are off (COLOR /?); CLS/COLOR are console-oriented and do not rewrite a redirected stdout log -
DEL/ERASE -
/P/F/S/Q/Aattributes;/Sdisplay shows only deleted files when extensions are on (DEL /?); directory operand deletes files inside; deleting a nonexistent file often leaves ERRORLEVEL 0 despite a stderr message; without/A, wildcard deletes skip hidden/system matches (use/A:Hor clear attributes first) -
ASSOC/FTYPE - extension associations and open-command strings; unavailable when extensions are off (ASSOC /?, FTYPE /?)
-
PATH command - display/set path;
PATH ;clears the search path -
START - quoted title (always pass
""when the command path is quoted so START does not treat the path as the title),/WAIT,/B(^C ignored unless the app enables it; ^Break may be required),/I,/MIN/MAXpriority,/NODE/AFFINITY,/D; batch/internal via new cmd with/K(window remains — prefer explicitcmd /cwhen the child must exit); without/WAITthe parent continues asynchronously (shared CWD/files can race); associations for non-executables;/WAITpropagates the child's exit code into ERRORLEVEL; without/WAIT, successful START leaves the prior ERRORLEVEL unchanged (does not clear to 0); failed START (missing target) sets ERRORLEVEL 9059 on live Windows 10/11; children inherit environment variables (unless/I) but not the parent's SETLOCAL Enable/Disable DelayedExpansion or Extensions state (defaults /cmd /V/ registry / the child itself apply) -
Expansion phases - percent first across each physical line (including
&/&&/||segments), then caret/tokenize/execute (line-continuation joins only after that percent pass, so%na/me%split across^does not form%name%); delayed!at execution; CALL reparses its tail -
SET /A arithmetic details -- integer
/truncates toward zero; modulo sign follows the dividend; no**power operator (^is XOR); Invalid number / divide-by-zero leave the prior value; comma expressions can leave earlier successful assigns while a later failure sets ERRORLEVEL; decimal literals can fail yet partial-assign; bare names silently truncate non-integer env values (including bare octal010→8and bare08/09→0); ERRORLEVEL codes for failures are implementation-defined (Win10 19045 probes often see 1073750993 / 1073750991 / 1073750990) -
BREAK -- DOS-compat internal; no-op for script control flow under Windows (does not break FOR/IF)
-
CHOICE defaults -- omitted
/CusesYN;/C ABCand/C:ABCboth accepted; without/Nthe prompt appends[choices]?;/Nhides that entire trailing list including the auto-?; keys outside/Cbeep and wait for a listed key; classicIF ERRORLEVELafter CHOICE needs descending-order tests (>=semantics) -
FOR /F unquoted options -- caret-escaped
tokens^=...^ delims^=...when quotes cannot wrap options; sparsetokens=1,3assigns token 1 then token 3 to successive metavars (no empty slot for the skipped index) -
Label fallthrough -- labels are not barriers; fence with
GOTO :EOF/EXIT /B -
@ prefix -- suppresses echo of that one statement when ECHO is ON
-
SHIFT vacates -- after SHIFT, vacated high
%nslots expand empty -
Remarks echo visibility -- with ECHO ON,
REMis echoed;::label-remarks typically are not -
DIRCMD -- ordinary env var supplying default
DIRswitches (DIR /?;dir-help.txt); override with-prefixes such asDIR /-W. Documented underdir_command, not PATH. -
%PROMPT% -- expands to the current prompt template when set; empty after a bare PROMPT reset
-
Special devices --
NUL/CONare reliable redirect targets/sources (>CON/<CONfor console I/O when stdout is otherwise redirected);IF EXIST concan block and is separate from CON redirects;PRN/AUX/COM1/LPT1redirects may fail on modern hosts -
DATE / TIME / VERIFY --
/Tprint-without-prompt (extensions); VERIFY ON/OFF; locale-tied formats for DATE/TIME -
SETX / SUBST -- persistent env (SETX space-delimited, not current session); SUBST virtual drives persist for the Windows session across cmd.exe processes until
/D/logoff/reboot; remapping an already-SUBSTed letter or a missing path leaves ERRORLEVEL 1; nested SUBST letters break if the parent mapping is deleted first -
PUSHD / POPD -- bare
PUSHDlists the stack; local-path PUSHD changes directory without mapping a drive (UNC maps Z:↓); POPD takes no arguments (extra tokens ignored); unbalanced UNC PUSHD can leave a temp drive mapped after exit; ENDLOCAL restores CWD but not the PUSHD stack -
COPY / MOVE / REN / DIR / TYPE -- overwrite
/Ydefaults in batch;copy nul fileempty-file idiom; COPY/Astops at Ctrl+Z while/Bdoes not; COPYsrc1+src2concatenates into one destination; COPY missing source EL 1; MOVE across volumes is copy-then-delete of the source; DIRCMD; DIR no-match mask EL 1; DIR/Rlists ADS when present; REN in-place only; REN destination-exists EL 1; wildcard REN destination maps by overwriting a prefix of the preserved source stem (ren her*.txt your*.txt→herfile.txtbecomesyourile.txt); TYPE display (EL 0 success / 1 missing file); TYPE then>>append glues onto a final line that lacked CRLF;TYPE CONreads console keyboard until Ctrl+Z (interactive stdin capture) -
TITLE / PAUSE / CLS / VER / VOL / MKLINK -- console/session builtins and link creation forms (
/Dsymlink dir,/Hhard link,/Jjunction); TITLE keeps surrounding/embedded quotes as literal title-bar text and still updates the window title when stdout is redirected; CLS with redirected stdout does not clear a log file (a form-feed byte may still appear in the redirect); COLOR under redirect does not rewrite the log and leaves ERRORLEVEL 1 on live Windows 10/11 -
CHCP / DOSKEY / HELP / MORE / SORT -- code page, macros/history, help lookup, and common pipe filters; SORT
/UNIQUE(prefix/UNIQ) drops duplicate lines (Microsoft Learn; local SORT /? may omit it) -
CHOICE -- dedicated section:
/C/N/CS/T/D/Mand ERRORLEVEL ordinals -
External tool notes -- FIND/FINDSTR ERRORLEVEL tables (
find_errorlevel,findstr_errorlevel: match 0 / no-match or missing named file 1; FIND wildcard-empty mask or bad switch 2; FINDSTR bad switch 2; FINDSTR missing/wildcard-open EL 1 + "Cannot open"); FINDSTR multi-file hits prefixedpath:, FINDSTR/Ssearches a bare filename under the tree,/COR-vs-literal, default regex vs/L//R,/E//Xtrailing-newline quirk, FINDSTR/Gsearch-string file and/Ffile-list; WHERE (EL 0 hit / 1 miss;/Rrecursive from a directory); FORFILES (EL 0 hit / 1 no-match,@fileplaceholders); FC (EL 0/1/2/-1); COMP (EL 0 identical / 1 different including unequal sizes / 2 cannot open;/Mno-prompt;/N=can compare despite size mismatch); ATTRIB / TREE (missing path often EL 0; ATTRIB refuses non-S/H changes while H or S is set -- "Not resetting hidden/system file", often still EL 0); REG QUERY (EL 0 success / 1 fail including missing key); REPLACE (EL 0 including "No files replaced" / missing named source under existing path; EL 3 path not found; EL 11 bad syntax/switch; live often not EL 2 for missing named file); ROBOCOPY bitmask (robocopy_errorlevel: bits 1/2/4/8/16, success copies often EL 1, bad source dir often EL 16); ROBOCOPY/MOV(files) vs/MOVE(files and dirs); ROBOCOPY missing named file (existing source dir) often EL 0 vs missing source path often EL 16 vs XCOPY missing named often EL 4 (xcopy_errorlevel: documented 0/1/2/4/5 table, F/D prompt non-interactive EL 2, empty wildcard source dir may still be EL 0); TIMEOUT-1..99999(redirected stdin commonly fails with non-zero EL); WAITFOR (signal send/SIor wait/T1–99999; stock external); MSG (session message; stock external); PRINT.exe queues text files (/D:device); bare PRINT reports no file and leaves EL 0; PING (loopback success EL 0; failed probe commonly EL 1); IPCONFIG (success EL 0; bad switch EL 1); TASKLIST (tasklist_errorlevel: success EL 0 even when filter matches zero rows; bad filter/switch EL 1); TASKKILL (taskkill_errorlevel: success EL 0; not found EL 128; bad switch EL 1); CERTUTIL (certutil_errorlevel: hash/encode/decode EL 0; missing file HRESULT 0x80070002; bad verb EL 1); network/admin helpers with captured help (net,sc,schtasks,systeminfo,takeown,fsutil,compact,cipher,arp,route,netstat); DPATH/KEYS/MODE/LABEL/HOSTNAME/WHOAMI/ICACLS/CLIP covered in YAML command notes -
SET /A integers only -- no floating-point type; use scaled integers (multiply/divide at a fixed scale) for fractional recipes; unary
!conflicts with delayed expansion; bare-name truncation vs expanded decimals; expression-only forms silent in scripts -
FOR /F empty fields -- consecutive delimiters collapse (including multiple leading commas); substitute a placeholder for
,,/ leading,before FOR /F when empty fields must be honored -
Delayed bang poison -- with delayed expansion on, unescaped
!in SET values can corrupt the stored value at assignment time -
CALL reparse -- each CALL halves
%%on its argument tail before re-parsing; CALL can also drive a second percent-expansion pass for indirect assignment (call set "out=%%%name%%%")
The ANTLR grammar reports syntax errors for forms that live cmd.exe rejects
as syntax (for example invalid %~ modifiers / %~*, empty unquoted IF
operands, IF EXISTS misspelling, IF %ERRORLEVEL% n without a compare-op,
and multi-character FOR /F eol= values). Corpus fixtures for those forms use
expect_syntax_errors: true.
Forms that fail only at runtime with non-syntax behavior (wrong ERRORLEVEL,
"not recognized", sticky success codes, expansion batveats) may still
should_parse: true. Treat data/expansion.yaml as
the semantic companion for those cases — see invalid_combinations and related
notes. Consumers (for example Blinter) should use the YAML alongside the
grammar, not as a full cmd.exe simulator.