Skip to content

Latest commit

 

History

History
156 lines (83 loc) · 42.2 KB

File metadata and controls

156 lines (83 loc) · 42.2 KB

Expansion catalog

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

Topics covered

  • 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-f qualification (combined F with n/x/d/p still yields the full path), directory operands for z/a/t, the multi-digit batveat (%~10 is %~1 plus literal 0), and %~dp0 trailing-backslash quote caution ("%~dp0"foo can 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. The invalid_combinations letter-regex lists both cases (nxfpdstaz / NXFPDSTAZ) so uppercase forms such as %~DPNX0 are 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 %%i vs %i FOR 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 XBetween 60 of X when and 80 is undefined — prefer %% for literal percents; when the intervening name is creatable and defined, its value substitutes — echo 60%foo%80 with set foo=DEFINED60DEFINED80; SET strips leading spaces from names so the and 80 form 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 as call 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 named n, never batch parameter %n (assign set "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 or cmd /V:OFF. SETLOCAL EnableDelayedExpansion takes effect for later &-chained commands on the same physical line (unlike whole-line percent binding).

  • FOR variables / forms - %%i in batch files, %i on 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 %%~qf are not syntax errors (unlike parameter %~q1); adjacent literal text sticks after expansion (%%n0 is metavariable n plus literal 0); undeclared %%letter becomes literal %letter; /D /R /L /F forms (extensions); FOR /R root path may include a trailing \; FOR /R with (.) includes the walk root (depth-first on live cmd); /D /R with (*) 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 /R without wildcards synthesizes root\name under 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; GOTO from a DO body exits the loop early for classic//F//L with non-zero step (BREAK does not); FOR /L empty ranges (start past end) run zero times; FOR /L step 0 never terminates and is not escaped by GOTO/EXIT /B; space/tab required between IN/DO and ( — glued in( / do( and newline-before-( are live syntax errors

  • FOR /F - eol / skip / delims / tokens / usebackq (and live useback synonym), 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), empty delims=, space-must-be-last in delims (prefer delims= 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 past z/Z, e.g. 27th from %%a is %%{, despite FOR /? “26” wording); a single FOR /F still hard-caps at 31 selected token indexes (tokens=1-31 iterates; tokens=1-32 / tokens=32 silently run zero times); tokens= indexes are sorted ascending before binding (tokens=5,7,1-3tokens=1-3,5,7); sparse tokens=1,3 assigns 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 be tokens=1* or tokens=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 empty delims= to keep leading spaces); bare tokens=* on a delimiter-only line still iterates once with an empty metavar, while tokens=1,* / numeric tokens skip those lines; eol= takes exactly one comment character (extra characters in the same eol= value commonly break parsing); empty eol= removes the default semicolon comment character on live Windows 10/11 (a space after eol= installs space as the eol char — prefer an unused explicit eol= when a comment character is still needed); repeated option keywords use the last occurrence; blank lines in file/command-output input are skipped; skip=n counts physical lines (including blanks and eol-comment lines) and must be ≥1 when present — omit skip to skip nothing (skip=0 is a syntax error); whitespace-only lines are also skipped with default delimiters but kept as a spaces-only token with empty delims= (a quoted ("a" "" "b") file-set is not a blank-line probe); multi-file filesets open in listed order and skip=n applies per file; unquoted fileset members also split on , / ; / =; fileset wildcards do not expand (use dir /b command form); FOR /F does not clear/set ERRORLEVEL by itself; UTF-16 inputs often need TYPE before /F; without usebackq, double-quoted string input may contain paired embedded ""; with usebackq, 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-numeric skip=, zero skip=0 / tokens=0 (and other zero indexes), malformed tokens= (e.g. 1-2-3, 1,,2, non-numeric), and multi-character eol= — 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-1 for 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% / after SET name= yields literal ~-1); missing/empty replace batveat (%NOSUCH:a=b% / after SET name= yields literal a=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 — assign set "s=%~1" first)

  • SET /A - requires Command Extensions; operators with documented precedence and same-tier left-associativity (8/2*28), grouping, comma separator, hex/octal (no_binary_literal: not 0b binary; 08/09 invalid as literals), undefined-as-zero, bare names (silent leading-integer truncation of non-integer env values; bare vs %name% diverge on decimals; bare v=010 is octal 8 while bare 08/09 truncate to 0 with 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 /A expression 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, unquoted set /A N=1+1 is a plain assignment whose name includes /A (quoted set /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 structured setAExpr tree for /A

  • Plain SET assignment - spaces around = become part of the name and/or value; prefix query (SET P, extensions); quoted SET "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; .bat vs .cmd ERRORLEVEL 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=<file reads 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 example set "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 as arr[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/OFF and @ suppression; ECHO OFF does not suppress stderr; with ECHO 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 - /V delayed expansion; /E Command Extensions; /Q echo off; /D disable AutoRun; /A ANSI / /U Unicode pipe/file output; /F:ON|OFF completion (Ctrl-F / Ctrl-D and CompletionChar registry values); /T:fg colors; defaults (extensions on, delayed off); compatibility aliases /X=/E:ON, /Y=/E:OFF, /R=/C; /C /K /S quote-stripping; AutoRun registry unless /D

  • Command Extensions off - disable via cmd /E:OFF, /Y, registry, or SETLOCAL DisableExtensions; base IF ERRORLEVEL/==/EXIST remain; compare-ops//I/DEFINED/CMDEXTVERSION, GOTO :EOF special target, CALL :label jump/%*/%~, SET /A//P, quoted SET, prefix query, string ops, FOR /D//R//L//F, SHIFT /n (under OFF, shift /1 behaves like bare SHIFT and still moves %0 — it does not preserve %0), dynamic env names, CD /D, ASSOC/FTYPE/COLOR, and PROMPT $+/$M require extensions; delayed expansion remains independently switchable; under OFF, set /A and set /P become literal plain assignments whose names include the /A or /P token; prefix-query SET name is 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); bare SETLOCAL inherits 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-line endlocal then set) 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 n means >= n; classic IF ERRORLEVEL/CMDEXTVERSION leading zeros are decimal (010>= 10), unlike compare-ops octal; dynamic %ERRORLEVEL% / %CMDEXTVERSION% env-var shadowing (classic IF ERRORLEVEL / IF CMDEXTVERSION still read the internal code / version); cmd /C exit N resets ERRORLEVEL without shadowing; bare call / (call) force ERRORLEVEL 1 and call / (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 as 0000 still EQU 0 under 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 /B updates 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 DEFINED is true for the dynamic names even when unshadowed and absent from SET listings; near-simultaneous cmd processes often share correlated %RANDOM% seeds; %RANDOM% %% N is 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-process CALL of 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 parenthesized IN/DO/ELSE bodies (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), not if 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 nul is true while nul\ is false (avoid IF 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 so if .%v% equ .sad is true after set "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 chain a < A < b < B); letter-vs-digit unquoted compares are string compares (A GTR 9); compare-ops accept octal/0x hex, leading +/-, and clamp out-of-range digit operands to signed 32-bit; classic IF ERRORLEVEL/CMDEXTVERSION accept -n and %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 native and/or keywords inside IF (and/or after 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-line and 2==2 (echo X) does not); joined-operand equality such as if "%a%-%b%" equ "X-Y" can stand in for AND but collides when values contain the delimiter; else if is 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); glued if 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 as if(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; /I is tolerated before non-compare predicates; IF %ERRORLEVEL% n without a compare-op is a syntax error; the full predicate text may come from expansion (set "b=a==a" / set "b=true==true" then if %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 fail matches spaced chaining); on live cmd && binds tighter than ||, and | binds tighter than &/&&/||; &&/|| are not the same as IF %ERRORLEVEL% EQU 0 (ECHO/REM/.bat SET and CALLed scripts can succeed without clearing ERRORLEVEL — e.g. cmd /c exit 5 & echo x && echo AND still 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; in 2>&1 the & 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.txt writes hello1); after expansion, a trailing separate digit becomes the handle (set "msg=Meet at 2" then echo %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; redirecting CALL :label captures 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 >CON keeps prompts on the console; pipe×redirect ownership matters (cmd | find >out vs cmd >out | find); bare redirect-only statements and bash-like &>file are 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-%9 work without extensions; %10 is %1 plus literal 0; %0 spelling mirrors CALL/invocation text (Explorer-style cmd /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 /n and bare SHIFT (invalid /n prints an error, sets ERRORLEVEL 1, and continues); SHIFT /1 preserves %0 while shifting %1 upward; %* is %1 %2 ... (never includes %0) and is unaffected by SHIFT; empty quoted "" occupies a slot (%1 is "", %~1 empty); unquoted args split on space/tab/comma/semicolon/equals (a=b → two slots while %* keeps a=b); substring/replace suffixes do not apply to %n/%~n (assign to an env var first)

  • CALL / GOTO - CALL :label return context (fresh %1/%* unless args are passed); CALL requires colon for labels (call name without : searches PATH, not :name); glued goto:eof / call:label accepted; missing CALL label continues with ERRORLEVEL 1; CALL :EOF is not special (ordinary missing-label search unless a user :EOF exists — unlike GOTO :EOF); successful CALL without EXIT /B preserves prior ERRORLEVEL; EXIT /B after CALL :label returns while EXIT /B after GOTO :label ends the script; bare CALL of a successful non-label non-script command (for example call echo ok / call set ...) clears ERRORLEVEL to 0; CALL other.cmd returns 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 n sets it); bare script invoke does not return (CALL does); bare external .exe returns 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 :EOF vs GOTO EOF; bare GOTO with no target ends the context ("No batch label specified"); case-insensitive user labels; duplicate labels use the first match in file order; goto lab does not match a distinct longer label :label; spaced label lines are prefix-matched (goto has / goto :has hits :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 (so goto :has space targets :has space, and goto :has also matches that spaced line); :: is the remark form; later colons in jump labels (for example :ok:extra) are allowed

  • EXIT - bare EXIT ends the cmd process; EXIT n (no /B) ends the process with that exit code; EXIT /B ends the script/routine; omit exitCode on /B to preserve ERRORLEVEL on CALL return, or pass n to set it; EXIT /B parses 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 bare EXIT /B (does not force 1); top-level bare EXIT /B under cmd /C may still yield process exit 0

  • Remarks - REM vs :: 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 as REMCASE are not REM; delimiter-glued forms (rem. rem/ rem: rem] rem[ and peers, analogous to echo.) still invoke REM at runtime and remark out the rest of the line (rem. & echo still 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$G and 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; CALL of a missing external sets ERRORLEVEL 1; internals (ECHO/DIR/SET/…) are never shadowed by cwd files, but external tools (for example where.exe / label.exe) can be shadowed by a same-named .bat/.cmd in the CWD

  • Directory commands - CD /D changes drive; bare D: / 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 - /S removes a directory tree; /Q quiets /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:fg uses the same bg-then-fg digit order despite the fg placeholder 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 /A attributes; /S display 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:H or 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 /MAX priority, /NODE /AFFINITY, /D; batch/internal via new cmd with /K (window remains — prefer explicit cmd /c when the child must exit); without /WAIT the parent continues asynchronously (shared CWD/files can race); associations for non-executables; /WAIT propagates 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 octal 0108 and bare 08/090); 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 /C uses YN; /C ABC and /C:ABC both accepted; without /N the prompt appends [choices]?; /N hides that entire trailing list including the auto-?; keys outside /C beep and wait for a listed key; classic IF ERRORLEVEL after CHOICE needs descending-order tests (>= semantics)

  • FOR /F unquoted options -- caret-escaped tokens^=...^ delims^=... when quotes cannot wrap options; sparse tokens=1,3 assigns 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 %n slots expand empty

  • Remarks echo visibility -- with ECHO ON, REM is echoed; :: label-remarks typically are not

  • DIRCMD -- ordinary env var supplying default DIR switches (DIR /?; dir-help.txt); override with - prefixes such as DIR /-W. Documented under dir_command, not PATH.

  • %PROMPT% -- expands to the current prompt template when set; empty after a bare PROMPT reset

  • Special devices -- NUL/CON are reliable redirect targets/sources (>CON / <CON for console I/O when stdout is otherwise redirected); IF EXIST con can block and is separate from CON redirects; PRN/AUX/COM1/LPT1 redirects may fail on modern hosts

  • DATE / TIME / VERIFY -- /T print-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 PUSHD lists 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 /Y defaults in batch; copy nul file empty-file idiom; COPY /A stops at Ctrl+Z while /B does not; COPY src1+src2 concatenates 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 /R lists 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*.txtherfile.txt becomes yourile.txt); TYPE display (EL 0 success / 1 missing file); TYPE then >> append glues onto a final line that lacked CRLF; TYPE CON reads console keyboard until Ctrl+Z (interactive stdin capture)

  • TITLE / PAUSE / CLS / VER / VOL / MKLINK -- console/session builtins and link creation forms (/D symlink dir, /H hard link, /J junction); 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 /M and 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 prefixed path:, FINDSTR /S searches a bare filename under the tree, /C OR-vs-literal, default regex vs /L//R, /E//X trailing-newline quirk, FINDSTR /G search-string file and /F file-list; WHERE (EL 0 hit / 1 miss; /R recursive from a directory); FORFILES (EL 0 hit / 1 no-match, @file placeholders); FC (EL 0/1/2/-1); COMP (EL 0 identical / 1 different including unequal sizes / 2 cannot open; /M no-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 /SI or wait /T 1–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%%%")

Parse vs catalog

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.