A batch line is not split into tokens once. It goes through several passes, each with its own special characters, and the order of the passes explains most of the surprises of the language. Microsoft documents the commands but not this machinery. Everything on this page about the passes, about carets and about comments comes from the Microsoft command pages where they say something and from independent analyses (SS64 and the writers of the “how does the CMD parser work” threads) where they do not, and it is marked as reported where Microsoft is silent.
The phases of reading a line
For a line in a batch file, cmd.exe is reported to work in this order:
- Percent expansion.
%%becomes%;%0to%9and%*become the arguments of the batch file;%name%becomes the value of the variable, or nothing when it is not set. This happens first, on the raw text, before any other character is looked at, so a value that holds&or|is read as part of the command. A line is expanded when it is read; a whole parenthesised block is expanded in one go before any command in it runs. - Special characters. The expanded line is read: quotes, carets, the command
separators, redirections and parentheses are recognised, a line that ends in
^is joined to the next, and the first word of each command is looked at (remis found here, and the rest of its line is thrown away). - Echo. If echo is on, the command is shown.
- For variables.
%%i(and its%%~nxiforms) inside the body of aforare replaced for each pass. - Delayed expansion. If it is enabled,
!name!is replaced, and a^in a line that holds a!is removed a second time. - Redirection and pipes are set up and the command is run. A
callrepeats percent expansion on its own arguments; a pipe starts two newcmd.exeinstances, each of which parses its side of the line again.
At the prompt there is no phase 1 for %%, and a variable that is not set is left in the
text as typed. The rest of the page names the characters that matter at each phase.
Special characters
| Character | Meaning |
|---|---|
| space, tab | Separate a command from its arguments and arguments from each other |
, ; = | Also separators between a command name and its arguments, and between arguments, in most commands; they are not separators inside quotes. if and set treat = as part of their syntax |
" | Starts and ends quoted text, in which the special characters below are ordinary |
^ | The escape character, and a line continuation at the end of a line |
% | Starts a variable or argument reference; %% is a literal percent sign in a batch file |
! | Delimits a delayed variable reference, when delayed expansion is enabled |
& | Runs the next command after this one |
&& | Runs the next command only if this one succeeded |
|| | Runs the next command only if this one failed |
| | Pipe: the output of this command is the input of the next |
< > >> >& <& | Redirection (Redirection and pipes) |
( ) | Group commands into a block; ) ends the block |
@ | In front of a command, stops that command being echoed |
: | At the start of a line, makes it a label |
/ | Starts a switch of a command; also ends a command name (dir/w) |
\ | Path separator; also ends a command name (cd\) |
* ? | Wildcards in file names |
Microsoft’s cmd page says the characters & < > [ ] | { } ^ = ; ! ' + , ` ~
and white space need quotation marks when they are part of a file name or other argument, and that
&, | and ( ) must be preceded by ^ or be quoted when
passed as arguments.
The name of a command ends at the first space, tab, ,, ; or =, or
at a character such as ., /, \, :, +,
[ or a quote, so echo., echo:, echo(,
dir/w, cd.. and cd\ all work as commands. Names are not case
sensitive; neither are variable names, labels or file names. for variable names are.
Escaping
| To get | Write | Notes |
|---|---|---|
a literal &, |, <, >, (, ) | ^& ^| ^< ^> ^( ^) | Or put the text in quotes. Microsoft’s echo page: inside a parenthesised block, escape both the opening and closing parentheses of text |
a literal ^ | ^^ | Each caret escapes the next character |
a literal % in a batch file | %% | At the prompt a lone % is already literal. Quotes do not protect a percent sign |
a literal ! with delayed expansion on | ^^! or "text^!" | Microsoft’s echo page gives "Hello World^!" and Hello World^^! |
a literal " | ^" outside quotes | Inside quotes there is no escape for a quote: a "" is a close and an open. Programs read \" or "" in their own way |
| a line continued on the next | ^ as the last character | A space after the caret breaks it. The next line is joined to this one |
Inside double quotes a caret is an ordinary character and does not continue the line. Other tools
have their own escapes: findstr, reg and runas use
\, not ^. A ^ that reaches a pipe is parsed again by the two new
interpreters, so a character that must survive a pipe needs a caret for each parse.
Quotes
A double quote toggles quoting for the rest of the command: while it is on, a space, a
&, a |, redirection characters, parentheses and the caret lose their
special meaning. It does not stop % and ! expansion. An unterminated quote
runs to the end of the line. There are no single-quoted strings in the language itself: the single
quote is an ordinary character, except in for /f, where 'text' or
`text` in the set is a command (Control flow).
There are no string escapes such as \n.
Quotes are kept as part of an argument. %~1 removes the surrounding quotes from a
batch argument. Microsoft’s cmd /c rule: if the string after /c starts with a
quote and is not exactly one quoted program name that has white space and no special characters, the
first and last quote are removed and the rest is kept.
Comments
| Form | What it does |
|---|---|
rem text | A comment, ended by the end of the line. It is read at the start of any command, so echo a & rem b works, and &, | or ) after rem do not end it. rem must be followed by a space, tab, ,, ; or = or the end of the line. Without text, rem alone is also valid |
@rem text | The same, with the @ that stops echo |
:: text | Written as a label whose name starts with a colon, so it is a comment only where a label may stand: at the start of a line. It is not echoed and is faster than rem. A :: after & is an error, and a :: line as the last line of a parenthesised block is reported to give a syntax error |
%= text =% | Reported idiom, usable inside a line: %= followed by any text without a percent sign and then =% expands, as a variable name, to nothing. It works only in a batch file or after call, and the text must contain no % |
rem and a caret | A ^ at the end of a rem or :: line joins the next line to the comment, because the line is joined before rem is looked at. SS64 notes the same for ::. Add a character after the caret, or leave the caret out |
| a label with text after it | :name text: the whole line is ignored after the name, so this also works as a comment, and what follows the name is never run |
text after goto :eof | Not a comment, but never reached, so it is used for documentation |
A rem line does not stop % expansion in phase 1: rem %1 with an
argument that contains a quote and an ampersand can still change how the line is read. Percent
expansion is the reason a comment can break a file when it holds %.
Labels
A line that begins with a colon is a label and nothing on it is run (Microsoft’s goto page:
“If a line begins with a colon, it’s treated as a label and any commands on that line are ignored”).
Leading spaces and tabs are allowed. The name is what follows the colon up to the first space; reported
additional ends are ,, ; and = (which
Microsoft’s page confirms cannot be used in a label) and +. Label names are not case
sensitive, may hold most punctuation, and the first of two equal labels is the one found. In
goto name the name is matched against the labels, so goto Done finds
:done. goto :eof and call :eof, with command extensions enabled,
mean the end of the current batch file or subroutine and need no label; the colon is required, and
goto:eof without a space is accepted.
Command separators and blocks
| Syntax | Reading |
|---|---|
| end of line | Ends a command, unless the line ends with ^ |
a & b | Run a, then b, whatever the result of a |
a && b | Run b only if a returned exit code 0 |
a || b | Run b only if a returned a non-zero exit code |
a | b | Run both at once, the output of a into the input of b |
( a & b ) | A block: the commands run as one, and if, else and for take a block as their body. ( is only a block start where a command is expected; elsewhere it is an ordinary character. A ) outside quotes and not escaped ends the innermost open block |
Microsoft’s cmd page describes & as the way to group or nest commands, && as
running the next only when the previous “completes successfully” and || as running the next
only when the previous fails. else has to follow a ) on the same line, or follow
the command of a one-line if on the same line, as Microsoft’s if page says.
A block spanning several lines is read and expanded as one unit before it runs, which is why
delayed expansion exists.
Token forms
| Form | Where | Meaning |
|---|---|---|
%NAME% | anywhere | Value of an environment variable |
%NAME:~n,m%, %NAME:old=new% | anywhere | Substring and replacement |
%0 … %9, %* | batch files | Arguments of the batch file or subroutine |
%~1, %~dp0, %~$PATH:1 | batch files | An argument with modifiers |
%%i, %%~nxi | for in a batch file | The loop variable (%i at the prompt) |
!NAME! | when delayed expansion is on | Value of a variable, read when the command runs |
%=ExitCode%, %=C:% | anywhere | Undocumented variables whose names start with = |
123, 0x1F, 017 | set /a, if, for /l | Numbers: decimal, hexadecimal (0x) and octal (leading 0). There is no number type outside these places, and no floating point. set says 0x12, 18 and 022 are the same value |
"text" | arguments | Quoted text |
*, ? | file names | Wildcards |
nul, con, prn, aux, com1–com9, lpt1–lpt9 | file names | Reserved device names, valid with or without an extension in older Windows |
An example
@echo off
setlocal enabledelayedexpansion
rem Special characters: ^& is escaped, "a & b" is quoted
echo Fish ^& chips "a & b" 100%%
set /a total=0x10 + 017 %% 3 >nul
for /f "tokens=1,2 delims=," %%a in ("x,y") do (
set "pair=%%a-%%b"
echo !pair! %= inline comment =% done
)
:: a label that is used as a comment
goto :eof
How the viewer colours it
The file viewer and the code blocks on these pages use one scanner. It marks:
| Colour class | What gets it |
|---|---|
| comment | rem to the end of the command line (over a trailing caret), :: lines, the text after a label name, %= … =% |
| tag | A label line’s name, the label after goto and after call : |
| keyword | if for goto call exit else where a command goes; not exist defined errorlevel cmdextversion equ neq lss leq gtr geq == in an if; in and do in a for |
| function | The built-in commands (Commands), where a command goes |
| variable | %x%, %1, %~dp0, %*, %%i, !x!, also inside strings; the name in set name=; names in a set /a expression |
| string | Double-quoted text; the quoted command of a for /f set |
| number | Decimal, 0x hexadecimal and octal numbers standing alone as an argument or in set /a, with an optional sign |
| literal | on and off after echo, verify and break; the four setlocal options; device names such as nul and con |
The words of an if and a for are keywords only in their place, and the text
of echo, title and prompt and the value in set have no
keywords at all. Redirection operators, &, |, parentheses, switches such as
/b and external program names are plain.
Not described
Unknown
- Reference: the order of the phases, the rules for
^in the second parse of a pipe or aftercall, the characters that end a label and the behaviour of::inside blocks are from reported analysis, not from Microsoft and not run oncmd.exefor this page. - Viewer: the colouring follows this reading; where
cmd.exediffers in a corner case the viewer will too.