-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexpansion.yaml
More file actions
3533 lines (3455 loc) · 202 KB
/
Copy pathexpansion.yaml
File metadata and controls
3533 lines (3455 loc) · 202 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
version: 59
# Percent-tilde (%~) expansion rules
# Authoritative syntax: audit/cmd-help/call-help.txt (CALL /?)
# Requires Command Extensions (CALL /?); see command_extensions_off.
valid_modifiers:
n: name
x: extension
f: full path
d: drive
p: path
s: short names
t: timestamp
z: size
a: attributes
$: path search (e.g. %~$PATH:1)
# Letter modifiers for %~dpnx... combinations. Path-search $ is separate; see path_search_modifier.
valid_modifier_chars: "nxfpdstaz"
valid_parameter_patterns:
- digit_0_9
- for_var_letter
invalid_combinations:
- modifiers: "*"
reason: "%~ modifiers may not be used with %* (CALL /?)"
# Matches forms like %~q1 or %~dq1 where a letter is outside nxfpdstaz.
# Does not match %~1, %~dpnx0, %~DPNX0, %~*, or %~$PATH:1. Letter classes
# include both cases because modifiers are case-insensitive (FOR /?, CALL /?).
# Note: %~* is still semantically invalid (see modifiers: "*" above); the
# letter-regex simply does not catch the star form because * is an allowed
# terminator.
- pattern: "%~[nxfpdstazNXFPDSTAZ]*[^0-9*$nxfpdstazNXFPDSTAZ]"
reason: >-
Percent-tilde letter modifiers must be from nxfpdstaz (case-insensitive;
CALL /?, FOR /?); the parameter must be 0-9, *, or a path-search $ENV: form
valid_combined_examples:
- "%~1"
- "%~dpnx1"
- "%~f1"
- "%~nx1"
- "%~fs1"
- "%~dp0"
- "%~z0"
- "%~ftza1"
- "%~dp$PATH:1"
path_search_modifier:
pattern: "%~$ENV:parameter"
description: "Searches PATH or other env var for file; expands to first match"
example: "%~$PATH:1"
empty_on_miss: >-
When the named environment variable is undefined or no matching file is
found, %~$ENV:n expands to an empty string (CALL /?).
cwd_not_implicit: >-
Path search walks only the semicolon-separated directories listed in the
named environment variable. A file that exists only in the current
directory is a miss (empty expansion) unless that CWD path is present in
the env var. First listed match wins (CALL /?; confirmed on live Windows
10/11 cmd).
combine_with_letter_modifiers: >-
Letter modifiers may precede the $ENV: search (for example %~dp$PATH:1
expands to the drive+path of the first PATH hit). The $ENV: form still
terminates the modifier sequence; a miss expands to empty.
percent_tilde_notes:
requires_command_extensions: >-
Percent-tilde (%~) modifiers and path-search forms require Command
Extensions (CALL /?). With extensions disabled, %~1 / %~dp0 / %~$PATH:1
are not expanded (the leading % is consumed and the remainder such as
~1 or ~dp0 remains literal text). Base %0-%9 still expand without
extensions.
bare_quote_strip: "%~1 strips surrounding quotes from the parameter with no letter modifiers"
multi_digit_parameter: >-
%~10 is %~1 followed by literal 0; a single digit or a path-search $ENV:
form terminates %~ (CALL /?). %~* is not a valid terminator (CALL /?
forbids percent-tilde with %*) and is a live / grammar syntax error.
adjacent_literal_after_digit_param: >-
After a single-digit %~ parameter (%~dp0, %~1, and peers), immediately
adjacent alphanumeric text is literal (for example %~dp0log.txt expands as
%~dp0 followed by log.txt, and %~10 is %~1 plus literal 0). Cross-ref
batch_parameters.multi_digit_percent and percent_tilde_notes.multi_digit_parameter.
syntax_rejects: >-
Live cmd rejects invalid percent-tilde forms with a path-operator message
and "The syntax of the command is incorrect." (exit 255). Covered rejects
include unknown letter modifiers (%~q1 / %~q1%), %~*, and %~name% spellings
that look like env vars. The grammar emits syntax errors for those forms;
valid %~1 / %~dp0 / %~$PATH:1 remain clean parses. Parallel FOR metavariable
forms such as %%~qf / %%~* / %%~q$PATH:i are NOT syntax errors: live leaves
the unresolved %~... text literal and continues (see
for_loop_variables.tilde_invalid_modifier_not_syntax).
for_metavar_parallel: >-
The same letter modifiers (nxfpdstaz) and path-search $ENV: forms apply to
active FOR metavariables (%%~fI / %%~$PATH:I / %%~zA for file size on
metavar A). Unlike batch-parameter %~q1,
an unknown letter before a FOR metavar is absorbed into the %%~ scan and is
not a path-operator syntax abort.
modifier_order_independent: >-
Letter modifiers may be combined in any order (%~dpnx0 and %~nxdp0 are
equivalent).
modifier_case_insensitive: >-
Letter modifiers are not case-sensitive (%~DPNX0 and %~dpnx0 are equivalent;
FOR /?).
attribute_mask: >-
%~a / %%~a expands to a fixed-width attribute mask (commonly 11 characters)
using letters and dashes in DIR-like slots (for example --a-------- for an
archive file). Missing files expand the attribute field to empty (CALL /?).
bare_vs_f_qualification: >-
Bare %~1 / %%~ strips quotes but keeps the argument spelling (relative or
as given). The f modifier (%~f1 / %%~f) expands to a fully qualified path
based on the current directory when the argument has no drive/path; this
happens even when the target file does not exist. Combining F with other
letters (for example %~Ff1) is equivalent for qualification (CALL /?).
f_overrides_component_selectors: >-
When F is combined with component-selector letters (n/x/d/p), live cmd still
expands to the fully qualified path: %~fnx1, %~nxf1, %~df1, %~pf1, and peers
all yield the same full path as %~f1. F dominates; the other letters do not
narrow the result to name/ext/drive/path alone. Confirmed on live Windows
10/11 cmd.
bare_quote_strip_leading_trailing: >-
%~1 removes at most one leading quote character and one trailing quote
character by position. Interior quotes remain (call :label "a""b" with an
embedded doubled pair keeps interior quotes after strip; call :label
"abc"def"ghi" yields abc"def"ghi for %~1). An unbalanced leading or trailing
quote is still stripped on that side alone. Confirmed on live Windows 10/11
cmd.
directory_zat_modifiers: >-
%~z / %~a / %~t apply to directory arguments as well as files. For an
existing directory, %~z returns a filesystem-dependent size value (often 0
for a newly created empty directory and a non-zero value for some populated
directories), not the empty miss used for missing paths. %~a / %~t likewise
report directory attributes and timestamps. Confirmed on live Windows 10/11
cmd; treat exact %~z digits as host/filesystem-dependent.
short_name_form: >-
The s modifier requests short (8.3) name components. %~s1 / %%~s and
combinations such as %~sf1 expand to a fully qualified path whose path
segments use short names where available (not a basename-only short name).
When short names are unavailable, cmd may still return a qualified path
(CALL /?; filesystem-dependent).
timestamp_format: >-
%~t / %%~t expands to the file's last-write date/time using the locale's
DATE/TIME formatting (separators and AM/PM style vary). Missing files
expand the timestamp field to empty (CALL /?).
dp0_trailing_backslash_quote: >-
%~dp0 (and other %~dp forms) always end with a trailing backslash. Closing
a quoted path immediately after that backslash then appending more text
(for example "%~dp0"foo) can treat the \" as an escaped quote and break
the intended argument boundary. Prefer "%~dp0foo" or "%~dp0%foo%" so the
closing quote does not follow the trailing backslash. Confirmed on live
Windows 10/11 cmd that %~dp0 ends with \.
percent_expansion:
undefined_is_empty: >-
Inside a .cmd/.bat script, an undefined name expands to an empty string for
both %name% and (when delayed expansion is enabled) !name!. Incomplete forms
without a closing % are not treated as successful expansions.
interactive_undefined_literal: >-
On the interactive command line, an undefined %name% often remains as the
literal text %name% rather than expanding to empty (script behavior differs;
see undefined_is_empty). Confirmed on live Windows 10/11 cmd as a common
interactive-vs-script mode difference; portable scripts must not assume
prompt behavior.
script_vs_interactive_modes: >-
Several expansion spellings differ between .bat/.cmd scripts and the
interactive prompt: undefined %name% (empty in scripts; often literal at
the prompt); FOR metavars (%%i in scripts; %i interactively); SET /A modulo
(%% in scripts; % interactively). Prefer always testing scripted forms when
authoring SoT fixtures or consumer tooling. Cross-ref
interactive_undefined_literal, double_percent, and set_a.modulo_in_batch.
incomplete_unclosed: >-
An unclosed %name form (no matching closing %) is not a successful expansion.
In scripts, cmd typically strips the leading % and leaves the following text
as literals (for example %NOSUCH becomes NOSUCH; a lone % may yield empty).
Prefer always-closed %name% / %% forms for portable scripts.
compound_line_expansion: >-
Percent expansion runs over the entire physical line before any chained
command on that line executes. Confirmed on live Windows 10/11 cmd:
set "x=1" then set "x=2"&echo %x% prints 1 (the pre-line value), while a
following echo %x% prints 2. The same pre-command binding applies across
&& on a successful left-hand side. Prefer delayed expansion or CALL
reparse when a later chained command must see an earlier same-line SET.
injects_command_metacharacters: >-
After percent expansion, & | < > in the expanded text re-enter command
parsing. Storing set "a=val&ue" then unquoted echo [%a%] splits on &
(often "'ue]' is not recognized..."). Quoted forms such as echo ["%a%"]
and delayed echo [!a!] keep the ampersand intact. Confirmed on live
Windows 10/11 cmd.
adjacent_percent_pair_eating: >-
Percent expansion pairs the nearest closing % after an opening %, so
intervening text (including spaces and ordinary words) becomes a candidate
variable name. In a script, an undefined name expands to empty. Confirmed
on live Windows 10/11 cmd: echo Between 60% and 80% of X prints
Between 60 of X because % and 80% is treated as an undefined name (the
candidate includes the leading space after 60; SET strips leading spaces
from names, so that exact name is not creatable via ordinary SET and the
pair always collapses to empty). When the intervening name is creatable
and defined, its value is substituted (echo 60%foo%80 with set foo=DEFINED
prints 60DEFINED80). Prefer %% for literal percents in scripts
(echo Between 60%% and 80%% of X) or break the pair. Distinct from
incomplete_unclosed (no closing %).
delayed_expansion:
pattern: "!VAR!"
requires_setlocal: false
default_off: >-
Delayed expansion is disabled by default (CMD /?, SET /?). Enable it
explicitly via cmd /V:ON, SETLOCAL EnableDelayedExpansion, or the Command
Processor DelayedExpansion registry value.
literal_when_disabled: >-
When delayed expansion is disabled, !var! (including substring and replace
forms) is ordinary literal text and is not expanded.
orthogonal_to_extensions: >-
Delayed expansion is independent of Command Extensions. cmd /E:OFF /V:ON
(or SETLOCAL DisableExtensions EnableDelayedExpansion) still expands plain
!var! forms. Percent and delayed substring/replace ops (%var:~% / !var:~!
and :old=new) still require extensions; with extensions off those forms
expand to empty rather than performing substring/replace or leaving the
!var:~...! / %var:~...% text literal.
enable_keywords:
- "setlocal enabledelayedexpansion"
- "setlocal enableextensions enabledelayedexpansion"
disable_keywords:
- "setlocal disabledelayedexpansion"
- "setlocal enableextensions disabledelayedexpansion"
enable_paths:
- "cmd /V:ON"
- "SETLOCAL EnableDelayedExpansion"
- "registry Command Processor DelayedExpansion"
disable_paths:
- "cmd /V:OFF"
- "SETLOCAL DisableDelayedExpansion"
- "registry Command Processor DelayedExpansion=0"
midline_enable_immediate: >-
SETLOCAL EnableDelayedExpansion takes effect for later chained commands on
the same physical line. Confirmed on live Windows 10/11 cmd: set "x=1" then
setlocal enabledelayedexpansion & echo !x! prints 1 (not literal !x!).
Percent expansion remains bound for the whole line before any command runs
(see percent_expansion.compound_line_expansion).
substring_pattern: "!var:~start,length!"
replacement_pattern: "!var:old=new!"
not_batch_parameters: >-
Delayed !n! / !name! forms expand ordinary environment variables, not
batch argument slots. !1! is the env var named 1 (if defined), never %1.
There is no delayed peer of %1-%9 / %* / %~. Assign parameters to env vars
first (set "arg=%~1") then use !arg! / %arg:~...% / %arg:old=new% as needed.
Confirmed on live Windows 10/11 cmd: with %1=hello and set 1=ENV, !1!
expands to ENV while %1 remains hello.
digit_leading_names: >-
Environment names that start with a digit expand with delayed !name! forms;
percent %1name% is parsed as argument %1 plus literal text (not %1name%).
Likewise %*name% is always %* plus literal text; a name cannot start with *
for percent expansion.
exclamation_escaping: >-
With delayed expansion enabled, a literal ! commonly needs caret escaping
such as ^^!. A single ^! is not enough: live cmd typically emits nothing
useful for that form (confirmed: echo [^!] prints empty brackets). The same
^^ before a non-! character yields a single caret (for example ^^2 becomes
^2). Extra caret pairs continue to interact with bang phases (for example
^^^^! yields a single caret and ^^^^^^! yields ^!), so bang escaping is
phase-sensitive and not identical to ordinary caret doubling for other
characters.
case_insensitive_search: >-
Delayed !var:old=new! search is case-insensitive, matching percent
%var:old=new% behavior; replacement text is literal.
indirect_expansion: >-
With delayed expansion enabled, percent expansion runs before delayed
expansion, so !%name%! first substitutes %name% then expands the resulting
!value! at execution time (for example set Car=Toyota & set Toyota=Prius
yields !%Car%! -> Prius). Related forms such as !prefix%name%! /
!%name%suffix! follow the same two-phase substitution. Without delayed
expansion, !...! is literal text.
block_indirect_percent_binding: >-
Inside a parenthesized IF/FOR block, %name% is still the pre-block value, so
!prefix%name%! builds the bang name from that frozen percent value even when
set name=... runs earlier in the same block (for example set "city=NYC"
then echo !food%city%! still uses the pre-block city). To compose a name
assigned in the block, bind it through a FOR metavar
(FOR /F %%x in ("!name!") do ...), CALL with %~1, or a CALL reparse such as
call echo %%food!city!%%, rather than expecting !prefix%name%! to see the
in-block assignment.
for_accumulate_pattern: >-
With delayed expansion enabled, a FOR body can accumulate with forms such
as set "LIST=!LIST! %%i" so each iteration sees the updated LIST. Plain
%LIST% expands when the FOR block is parsed and stays fixed for the loop;
if LIST starts empty, set LIST=%LIST% %%i therefore keeps only the last
iteration's contribution rather than building a growing list.
value_with_exclamation: >-
When delayed expansion is enabled, a value that contains ! (and often ^) can
be corrupted at SET time if delayed expansion is already on during the
assignment. Live cmd commonly stores set "A=hi!there" as hithere (the bang
and following text are mangled) even before a later read. Prefer disabling
delayed expansion while assigning such data (setlocal DisableDelayedExpansion
/ endlocal around the SET), then re-enable before reading with !var!. Do not
rely on a single ^! inside the SET value under delayed-on: live probes such
as set "B=hi^!there" still corrupt the stored text. Separately, once an
intact bang-bearing value exists (stored with delayed off), reading it with
%var% re-scans the inserted text for !name! under delayed expansion and can
mangle it, while direct !var! keeps embedded ! characters (confirmed on live
Windows 10/11 cmd: store aa!bb!cc with delayed off, then with delayed on %V%
yields aacc and !V! yields aa!bb!cc). Prefer !var! (or delayed off) when
values may contain !.
call_second_pass: >-
CALL reparses its command tail after percent expansion, so a second
percent-expansion pass is available. With delayed expansion enabled,
patterns such as call set "out=%%%name%%%" (after set name=other and
set other=value) expand %name% on the first pass, then expand %other% on
the CALL reparse, storing value in out. Related forms call echo %%var%%
similarly print the current value after reparse. This is distinct from
single-pass !%name%! indirect expansion.
writing_batch_files: >-
With delayed expansion enabled, emitting a later-expanding bang form into
a child .cmd/.bat requires caret-escaping the ! in the parent so the
parent does not expand it while writing. A common pattern is
echo echo ^^!var^^!>>child, which stores echo !var! in the child for the
child's delayed-expansion pass (confirmed on live Windows 10/11 cmd).
See also double_percent.writing_batch_files and
caret_escape.writing_batch_files.
for_loop_variables:
batch_file_prefix: "%%"
command_line_prefix: "%"
letter_charset: "a-zA-Z0-9?#$@_`[]{}+.-\\!*():/~"
letter_charset_notes: >-
Live cmd.exe accepts letters, digits, and many punctuation characters as the
single FOR variable letter, including tilde (for %%~ in (hello) do ...).
Bare %%~ is a metavar whose letter is ~; %%~~ is quote-strip of that metavar.
When additional letters follow %%~, longest-match %%~ modifier forms
(FOR_VAR_TILDE) take precedence (for example %%~fI). Operators that fail as
FOR letters include % | & = ; < >. Prefer letters to avoid clashing with
%0-%9.
tilde_invalid_modifier_not_syntax: >-
Unlike batch-parameter %~q1 (a live syntax error), FOR forms with unknown
tilde letters such as %%~qf, %%~*, or %%~q$PATH:i are accepted: live cmd
leaves the unresolved %~... text as literals and continues the script. The
grammar likewise does not report those FOR forms as syntax errors.
Confirmed on live Windows 10/11 cmd.
case_sensitive: >-
FOR variable letters are case-sensitive (%i and %I are distinct). Up to 52
active FOR variables may be in use (a-z and A-Z).
metavar_adjacent_literal: >-
After a FOR metavariable expands, immediately adjacent literal text stays
attached with no separator (for example in a batch file %%n0 is the value of
%%n followed by literal 0, not a two-digit metavariable). This is distinct
from %%~n modifiers (tilde forms) and from %~10 / %%~n0 parameter-style
multi-digit batveats. Prefer letters for metavars when a digit suffix is
intended as a literal.
undeclared_metavar_literal: >-
A %%letter form that is not an active FOR metavariable in the current DO
body expands as a literal %letter (the %% percent-escape), not as empty.
Example: FOR /F "tokens=1-2" %%a IN ("one two three") DO echo %%c prints
%c because only %%a and %%b were implied. Digits are valid FOR letters;
multi-token implication walks ASCII succession from the declared letter
(including digit starts such as %%7 -> %%8 -> %%9), not only a-z / A-Z.
for_forms:
description: >-
With Command Extensions, FOR supports /D (directories only), /R (recursive
tree; optional root path, default current directory), /L (numeric
start,step,end), and /F (parse text from files, strings, or command output).
Source: FOR /?.
slash_d: >-
FOR /D matches directory names only when the set contains wildcards.
slash_r: >-
FOR /R walks the directory tree rooted at an optional path (default: current
directory). A trailing backslash on the root path is accepted and equivalent
to the same path without it (for /R C:\dir\ and for /R C:\dir). A set that
is only '.' enumerates each directory visited (including the root of the
walk). When the set has no wildcards (* or ?), FOR /R synthesizes a
candidate path of root\setname under each directory visited (even when that
file does not exist) rather than matching existing files by wildcard
(FOR /?). On live Windows 10/11 cmd, FOR /R %%D in (.) visits depth-first:
the walk root, then a subdirectory before that subdirectory's siblings that
sort later in the filesystem enumeration.
slash_d_r: >-
FOR /D /R (directories recursively) with a wildcard set such as (*) lists
matching subdirectory names under the walk and does not emit the walk root
itself as a match. Prefer FOR /R %%D in (.) when the root directory must be
included in the enumeration. Enumeration order differs from FOR /R (.): on
live Windows 10/11 cmd, FOR /D /R %%D in (*) commonly yields sibling
directories before descending into a sibling's children (for example A, B,
then A\child), so scripts must not assume identical walk order across the
two forms.
slash_l: >-
FOR /L iterates a numeric sequence (start,step,end), inclusive of endpoints
that the step reaches. The end need not be hit exactly; iteration stops once
the index passes the end in the step direction. When the start is already
past the end for a positive step (for example for /l %%i in (5,1,3)) or
before the end for a negative step, the DO body runs zero times (confirmed
on live Windows 10/11 cmd). A step of 0 never advances the index, so the
end condition is never met: on live Windows 10/11 cmd the DO body repeats
forever. Neither GOTO nor EXIT /B from that DO body terminates the loop
(the process must be interrupted or ended with bare EXIT); avoid step 0
entirely.
slash_f: >-
FOR /F parses text from a file-set, a quoted string, or command output; see
for_f for option details.
classic_literal_set: >-
In the classic FOR %%i IN (set) DO form, set members without wildcards (* or
?) are treated as literal strings and are iterated even when no matching
file exists (for example FOR %%i IN (nosuch.xyz) DO ... runs once with
%%i=nosuch.xyz). Wildcards match existing filesystem entries (FOR /?).
wildcard_no_match_zero_iterations: >-
When a classic FOR set member contains wildcards (* or ?) and no filesystem
entry matches, the DO body runs zero times for that mask (it does not run
once with the literal pattern). Prior ERRORLEVEL is left unchanged (not set
to 5). Confirmed on live Windows 10/11 cmd; secondary docs that claim
ERRORLEVEL 5 or a single literal iteration for unmatched masks are wrong
on current hosts.
empty_set_zero_iterations: >-
An empty classic FOR set (FOR %%i IN () DO ...) iterates zero times and does
not run the DO body. Separator-only sets such as (,,) or ( ; ) likewise
yield zero iterations after consecutive delimiters collapse. A quoted empty
member ("") is one iteration whose metavar text is two quote characters.
Confirmed on live Windows 10/11 cmd; the grammar accepts empty and
separator-only forList forms.
set_member_delimiters: >-
Unquoted members in a classic FOR %%i IN (set) are split on the same
delimiters as batch arguments: space, tab, comma, semicolon, and equals
(FOR /?; confirmed on live Windows 10/11 cmd). Example: FOR %%i IN (a,b;c=d)
yields four iterations a, b, c, d. A quoted member stays one iteration,
including internal = / , / ; (for example "a=b").
percent_star_as_set: >-
FOR %%i IN (%*) DO ... expands %* first, then re-tokenizes that text with
classic FOR set delimiters (see set_member_delimiters). Unquoted a=b or e;f
become separate iterations; a quoted argument such as "c d" remains one
member (quotes typically still visible on the metavar). This is distinct
from walking %%1.. via SHIFT. Cross-ref batch_parameters.percent_star.
multiple_set_members_or_masks: >-
A classic FOR set may list multiple literal members and/or masks (space-
and/or comma-separated). Each mask is expanded independently (for example
*.txt *.csv or Italy*.jpg,Italy*.bmp). Combine with set_member_delimiters
when diagnosing unexpected splits on = / ; / ,.
wildcard_question_trailing: >-
In FOR file-set masks, a trailing ? (or ? immediately before a dot) can match
fewer characters than the number of question marks, including matching a
shorter basename (for example ???.txt may match a.txt as well as ab.txt).
The same trailing-? behavior appears in other cmd file masks (DIR, COPY,
DEL, and peers). Short 8.3 names can also satisfy masks that the long name
alone would not (for example *.tar matching a file whose long extension is
.targa when a short .tar alias exists); treat mask matching as
filesystem-dependent and inspect DIR /X when hits look unexpected.
wildcard_question_mid: >-
A ? that is not at the end of a name component and not immediately before a
dot matches exactly one character. Confirmed on live Windows 10/11 cmd:
sh?t.txt matches shot.txt and shut.txt but not sht.txt or shunt.txt, while
sh??t.txt matches shunt.txt and sh*t.txt matches all of those basenames.
Combine with wildcard_question_trailing when diagnosing flexible trailing-?
matches versus exact mid-mask ?.
wildcard_asterisk: >-
In classic FOR / DIR / COPY / DEL masks, * matches any remaining characters
in that name component, including dots (for example filea*c.txt matches
fileabc.txt; *.tar matches names whose final extension component is tar,
not *.tar.gz). Combine with wildcard_question_trailing and short-name notes
when diagnosing unexpected hits. Confirmed on live Windows 10/11 cmd.
goto_exits_body: >-
A GOTO from a FOR DO body exits the loop early and continues at the target
label for classic FOR, FOR /F, and FOR /L with a non-zero step. BREAK does
not terminate FOR iteration (see break_command.not_loop_break). Confirmed on
live Windows 10/11 cmd for both FOR %%i IN (...) DO ... & GOTO :label and
parenthesized DO bodies. FOR /L with step 0 is excluded: GOTO and EXIT /B
do not escape that infinite loop (see slash_l).
in_do_paren_spacing: >-
Live cmd requires space or tab between IN and '(' and between DO and a
parenthesized DO body. Glued forms are syntax errors: for %%i in(a) do
echo x reports "in(a) was unexpected"; for %%i in (a) do(echo x) reports
"do( was unexpected". A newline between DO and '(' is also a syntax error
("The syntax of the command is incorrect"). Spaced forms
(for %%i in (a) do (echo x) / do echo x) are valid. Confirmed on live
Windows 10/11 cmd; the grammar rejects the glued IN(/DO( forms.
for_f:
description: "FOR /F parsing options and input forms (FOR /?)"
options:
- eol
- skip
- delims
- tokens
- usebackq
option_details:
eol: >-
eol=c sets a single end-of-line comment character (default semicolon).
A line is skipped when its first parsed token begins with that character;
the same character later on the line is ordinary data (not a C-style
mid-line comment). Only one character is accepted after eol=; additional
characters in the same eol= value are not a multi-character comment
prefix and commonly break option-string parsing on live cmd (for example
eol=#x yields a message such as x" was unexpected at this time).
skip: >-
skip=n skips the first n physical lines of each input file (or each
string / command-output unit), including blank lines and lines that would
later be treated as eol= comments. Omitting skip is the legal way to
process from the first line; it is not equivalent to writing skip=0
(skip=0 is a syntax error — see skip_requires_integer). In a multi-file
fileset such as IN (a.txt b.txt), skip applies independently to each file
in listed order, not once across the whole fileset (FOR /?; confirmed on
live Windows 10/11 cmd).
delims: >-
delims=xxx replaces the default delimiter set (space and tab). An empty
delims= means no delimiters (whole line is one token); confirmed for forms
such as FOR /F "delims=" %%a IN ("a b c"). When a space is included among
delimiters it must be the last character of the delims= value; otherwise
the options string can misparse. Prefer placing the delims= clause last
among FOR /F options so a trailing space delimiter sits immediately
before the closing quote (for example "tokens=1,2 delims=, "). Delimiter
characters are case-sensitive (delims=D does not treat lowercase d as a
delimiter). The grammar accepts any delims= character payload without
validating which characters are safe delimiters; metacharacters in the
delimiter set (for example | & < > ^) remain catalog/runtime concerns and
often need caret-escaping in unquoted option forms (see
unquoted_options_caret).
tokens: >-
tokens=x,y,m-n selects token indexes; a trailing * assigns the remainder
of the line to one variable. Selected indexes are sorted ascending before
metavar assignment, so tokens=5,7,1-3 and tokens=1-3,5,7 bind the same
successive letters (confirmed: "A B C D E F G H" yields %%a=A %%b=B
%%c=C %%d=E %%e=G for both spellings). Sparse lists skip indexes:
tokens=1,3 with %%a assigns token 1 to %%a and token 3 to %%b (the next
letter); there is no empty metavariable for the skipped token 2
(confirmed: "one,two,three" with delims=, yields %%a=one and %%b=three).
Additional FOR variables are implied by ASCII succession from the
declared letter (not only a-z / A-Z): for example tokens=1-27 starting at
%%a assigns the 27th token to %%{ on live Windows 10/11 cmd. FOR /? and
some references claim a 26-token / past-z ceiling; live cmd continues
past z/Z, though some successor characters are unusable in DO text
(notably | as a pipe). Prefer staying within a-z / A-Z for portable
scripts. Independently of letter succession, a single FOR /F selects at
most 31 token indexes: tokens=1-31 iterates, while tokens=1-32 or
tokens=32 silently produce zero iterations (no syntax error). tokens=*
still applies leading-delimiter stripping first (default space/tab), so
leading spaces are removed before the remainder is assigned; use empty
delims= when leading whitespace must be preserved (confirmed: tokens=*
on " spaced" yields spaced, while delims= yields the spaced string
including leading spaces).
usebackq: >-
usebackq switches quoting: backtick `command` for command output, single
quotes for literal strings, and double quotes for filenames. Without
usebackq, single quotes wrap a command and double quotes wrap a string.
With usebackq, a single-quoted string that contains unescaped parentheses
commonly fails to parse (for example ') was unexpected at this time');
caret-escape them as ^(...^) inside the quotes when those characters are
data. Prefer the non-usebackq "string" form when the text holds parentheses
and no embedded double quotes are required.
useback_synonym: >-
Live cmd.exe also accepts useback as a synonym for usebackq (trailing q
optional). Prefer the documented usebackq spelling in new scripts.
tokens_syntax_rejects: >-
Live cmd rejects malformed tokens= values as option-string syntax errors
(messages such as abc" was unexpected at this time). Confirmed rejects
include non-numeric tokens=abc, empty slots such as tokens=1,,2,
multi-hyphen forms such as tokens=1-2-3, and any index whose integer value
is 0 (tokens=0, tokens=00, tokens=1-0, tokens=0*). Leading zeros on a
non-zero index are accepted (tokens=01 is index 1). Valid forms are a
single *, a comma-separated list of positive integers, and single hyphen
ranges m-n (optionally mixed), with an optional trailing * on the last
item (with or without a comma before the *: tokens=1* and tokens=1,* are
both accepted). These option-string syntax errors are non-fatal (later
statements still run), matching skip= / eol= option rejects. The grammar
applies the same checks to quoted and unquoted (caret-escaped) option text.
tokens_comma_before_star: >-
A trailing remainder star may be written as tokens=1* or tokens=1,* (and
likewise after ranges/lists). Both forms assign the first selected token to
the declared metavar and the unparsed remainder of the line to the next
letter. Confirmed on live Windows 10/11 cmd.
skip_requires_integer: >-
skip= requires a numeric line count whose integer value is at least 1.
Non-numeric values such as skip=xyz and zero counts such as skip=0 /
skip=00 are live syntax errors (for example xyz tokens=1" or " was
unexpected at this time). Leading zeros on a non-zero count are accepted
(skip=01 skips one line). Omitting the skip keyword entirely skips nothing
and is the correct spelling when no lines should be skipped — do not write
skip=0. These option-string syntax errors are non-fatal: the FOR statement
fails to iterate, but the batch continues with later statements (process
exit 0), unlike invalid percent-tilde forms such as %~q1 that abort.
Confirmed on live Windows 10/11 cmd.
skip_counts_physical_lines: >-
The skip=n budget counts physical lines before blank-line and eol= comment
filtering. Empty lines and lines whose first parsed token begins with the
eol character still consume skip counts. After the skip budget is exhausted,
blank lines are ignored as usual and remaining eol-comment lines are
skipped. Confirmed on live Windows 10/11 cmd: a file of two blank lines then
DATA with skip=2 tokens=* iterates DATA; two semicolon comment lines then
KEEP with default eol and skip=2 iterates KEEP.
option_keyword_last_wins: >-
If the same FOR /F option keyword appears more than once in the options
string, live cmd applies the last occurrence (for example
tokens=1 tokens=2 keeps tokens=2; skip=1 skip=2 skips two lines;
eol=# eol=; uses semicolon). Prefer listing each keyword once.
option_keyword_order_independent: >-
Distinct option keywords may appear in any order; live cmd does not apply a
fixed cross-keyword precedence such as usebackq > skip > delims > eol >
tokens. Confirmed on live Windows 10/11 cmd: tokens=1 delims=, and
delims=, tokens=1 bind the same first token; eol=# delims=, and
delims=, eol=# agree on which lines are comments. Prefer a readable order
with delims= last when a trailing space delimiter is required.
options_from_expansion: >-
The FOR /F options string may be produced by percent (or delayed) expansion
(for /f "%opts%" %%a in (...) when opts holds tokens=... delims=...). Mixed
forms such as "%tok% delims=," also work. Confirmed on live Windows 10/11
cmd.
whitespace_only_lines: >-
Truly empty/blank lines in FOR /F file or command-output input are skipped.
A whitespace-only line is also skipped with default delimiters; with empty
delims= it is kept as a spaces-only token; with tokens=* it still iterates
but often yields empty after leading-delimiter stripping. Confirmed on live
Windows 10/11 cmd.
tokens_hard_max: 31
tokens_star_delimiter_only: >-
When tokens=* appears alone (no numeric indexes), a non-empty input that
consists only of delimiter characters still causes one DO iteration with an
empty metavar (for example tokens=* delims=x on "xxx" sets %%A to empty and
runs the body). Forms that include any numeric token index such as
tokens=1,* or tokens=1 skip delimiter-only lines and do not iterate.
Confirmed on live Windows 10/11 cmd.
eol_empty: >-
An empty eol= value (for example "eol=" alone) removes the default
semicolon comment character on live Windows 10/11 cmd, so lines beginning
with ; are kept as data. A space after eol= before the next keyword (as in
"eol= delims=,") installs space as the eol character rather than leaving
comments fully disabled. Prefer setting eol= to an unused character that
will not appear as a line prefix when a defined comment character is still
required; do not treat empty eol= as a portable multi-character "off"
switch across hosts or documentation.
unquoted_options_caret: >-
When an options string cannot use surrounding double quotes (for example
because delims must include a quote character), the options may be written
unquoted with caret-escaped equals and spaces, such as
tokens^=1-2^ delims^=^". Prefer quoted options whenever possible.
consecutive_delims: >-
Consecutive delimiter characters collapse to one delimiter; empty fields
between repeated delimiters are not preserved by default.
empty_fields_token_shift: >-
Because consecutive delimiters collapse, a,,b with delims=, and
tokens=1,2,3 yields token1=a, token2=b, and token3 empty (the middle empty
field is skipped). A leading delimiter such as ,a,b likewise shifts so
token1=a and token2=b. Multiple leading delimiters collapse the same way:
confirmed on live Windows 10 (build 19045) cmd, for /f "tokens=1-3 delims=,"
%%a in (",b,c") yields %%a=b %%b=c %%c=empty, and ",,,d" with tokens=1-4
yields %%a=d with the later metavars empty. There is no built-in FOR /F mode
that preserves empty fields between collapsed delimiters.
empty_fields_workarounds: >-
To honor empty fields, transform the line before FOR /F so consecutive
delimiters become delimiter-plus-placeholder-plus-delimiter, then parse.
Confirmed on live Windows 10/11 cmd with delayed expansion: for line a,,b,
set "line=!line:,,=,__EMPTY__,!" then for /f "tokens=1-3 delims=," %%a in
("!line!") yields %%a=a, %%b=__EMPTY__, %%c=b. Replace the placeholder with
an empty string after the loop when a true empty field is required. Leading
delimiters still need an explicit leading placeholder (for example prepend
a sentinel before the first character, or rewrite a leading "," into
"__EMPTY__,") if an empty first field must be preserved.
default_first_token: >-
By default, /F passes the first blank-separated token from each line
(FOR /?).
variables_are_global: >-
FOR replaceable parameters are single-letter, case-sensitive, and drawn from
a shared session namespace of up to 52 active letters (FOR /?). Metavar
expansion applies in the FOR DO body (including &-chained commands on that
body), not in later unrelated statements or in a CALL :label body unless the
value is passed as an ordinary argument. Nested FOR loops may reuse the same
letter: during the inner loop the letter holds the inner value; after the
inner FOR completes, the outer value is restored on modern Windows.
input_forms: >-
file-set; "string" (or 'string' with usebackq); 'command' (or `command`
with usebackq). A bare/multi fileset IN (a.txt b.txt) opens files in listed
order (each file is a separate input for skip= and eol=). Blank lines in
file and command-output input are skipped (FOR /?): a physical empty line
does not produce a loop iteration. A quoted multi-string file-set form such
as ("a" "" "b") is not a blank-line probe; cmd treats that as a single
string argument rather than three lines.
fileset_member_delimiters: >-
Unquoted FOR /F file-set members split on the same delimiters as classic
FOR sets: space, tab, comma, semicolon, and equals. IN (a.txt,b.txt),
IN (a.txt;b.txt), and IN (a.txt=b.txt) each open two files in order
(confirmed on live Windows 10/11 cmd). Quoted members stay one name.
fileset_no_wildcards: >-
A FOR /F file-set does not expand * or ? wildcards into multiple files.
for /f %%a in (x*.txt) reports that the file cannot be found and yields no
iterations when no literal name x*.txt exists. Enumerate matches via a
command-output form such as for /f %%a in ('dir /b x*.txt') do ... instead.
Confirmed on live Windows 10/11 cmd.
tokens_duplicate_indexes: >-
Repeating the same tokens= index still allocates successive metavars, but
later duplicates receive empty values rather than re-binding the token.
Confirmed on live Windows 10/11 cmd: tokens=1,2,1 delims=, on A,B,C with
%%a yields %%a=A, %%b=B, %%c empty; tokens=1,1 on A B yields %%a=A and
%%b empty. Prefer unique indexes.
does_not_set_errorlevel: >-
FOR /F itself does not clear or set ERRORLEVEL. A missing fileset file
prints a cannot-find message and leaves the prior ERRORLEVEL unchanged; a
command-output child's exit code likewise does not overwrite the parent
ERRORLEVEL merely because FOR /F iterated. Confirmed on live Windows 10/11
cmd (prior cmd /c exit 7 remains 7 after a missing fileset and after a
successful for /f %%a in ('echo OUT') loop).
input_encoding: >-
FOR /F file and command-output tokenization expects text in the active
console/OEM code page (or compatible ANSI). UTF-16LE files with a BOM are
typically not tokenized usefully by a bare fileset; piping through TYPE
(for /f %%a in ('type u16.txt') do ...) often recovers readable lines on
live Windows 10/11 cmd. Prefer ASCII/OEM inputs or an explicit conversion
step when encoding matters.
string_embedded_quotes: >-
Without usebackq, a double-quoted string input may contain an even number of
embedded " characters as paired quotes (for example
for /f "tokens=1-3" %%a in ("one ""two"" three") do ...). Confirmed on live
Windows 10/11 cmd: tokens become one, ""two"", three. A solitary unescaped "
inside that form commonly breaks parsing; prefer usebackq with '...' when
the data holds awkward quote mixes.
command_stdout_only: >-
The FOR /F command-output form captures only the command's stdout as token
lines. Stderr still prints to the console (or follows redirects on the
command) but does not become FOR tokens. Confirmed on live Windows 10/11
cmd: for /f %%a in ('cmd /c "echo OUT&echo ERR 1>&2"') yields token OUT
while ERR appears on stderr.
bare_fileset_no_path_search: >-
A bare FOR /F file-set name (for example for /f %%a in (data.txt) do ...)
is resolved relative to the current directory (or an explicit drive/path on
the name). Unlike command resolution, live Windows 10/11 cmd does not search
%PATH% for that file: a data.txt that exists only under a PATH directory is
reported as "The system cannot find the file ..." and yields no iterations.
Use an absolute or relative path, pushd to the directory, or a command-output
form such as for /f %%a in ('type "path\data.txt"') do ... when the file is
not in the CWD.
caret_escape:
description: >-
Caret escaping is phase-driven. For ordinary multilevel escaping without
CALL, each re-parse hop commonly needs 2^n-1 carets before a special
character. CALL adds an extra re-parse that doubles every caret on its
command tail (including carets inside double quotes), so nested CALL or
percent-expanded command execution requires compensating counts per hop
rather than relying on 2^n-1 alone across CALL boundaries.
call_doubling: >-
CALL doubles all carets once on its arguments before the line is parsed
again; quoted carets are doubled even though quotes suppress escaping.
line_continuation: >-
A caret as the last character of a physical line continues onto the next
line (the CRLF is escaped). CRLF is two bytes escaped by that single
trailing caret (the unusual case where one ^ spans a two-byte sequence). A
trailing space after ^ escapes the space instead, so the next line is not a
continuation. Percent expansion runs on each physical line before
continuation joins the next line, so a name cannot span the break: echo
%NA^ / ME% does not form %NAME% (live cmd typically leaves incomplete-%
stripping such as NAME). Keep %name% on one physical line. Confirmed on
live Windows 10/11 cmd.
percent_not_escaped: >-
Caret does not escape percent. Percent expansion runs before caret-escape
processing, so ^%name% still expands %name% (use %% in scripts for a
literal percent). Escaping ! under delayed expansion is a separate
phase-sensitive rule (see delayed_expansion.exclamation_escaping).
metachar_inventory: >-
Characters that commonly require caret escaping outside quotes include
& | < > ^ and sometimes (). Double quotes suppress those metacharacters
for command parsing but do not suppress percent expansion or delayed
! expansion (see quoting). Literal % uses %% in scripts; literal ! under
delayed expansion commonly needs caret doubling such as ^^!. Multilevel
re-parse uses 2^n-1 carets (or CALL doubling) rather than a single ^
always being enough.
writing_batch_files: >-
When a parent ECHO-redirects into a child .cmd/.bat, one caret-escape pass
applies while writing: ^> stores >, ^^^> stores ^>, and ^^^^^> stores ^^>
in the child (confirmed on live Windows 10/11 cmd). Parentheses written into
IF/FOR blocks likewise need caret escaping on the write pass (echo if 1==1
^( / echo ^) ) so the parent does not treat them as grouping while emitting
the child. Pair with double_percent.writing_batch_files (%% hops) and
delayed_expansion.writing_batch_files (! emit) for multigenerational
generators.
quoting:
description: >-
Double quotes group an argument and suppress command metacharacters, but
they do not disable percent or delayed-expansion parsing.
suppress_metacharacters: >-
Inside double quotes, & | < > ^ and () lose their command-separator /
redirect / escape meaning for that quoted span (they may appear as literal
text in the argument). Quotes themselves are stripped from %~1 bare
quote-strip forms.
no_suppress_percent_or_delayed: >-
Percent expansion (%name%, %1, %~...) still runs inside double quotes.
When delayed expansion is enabled, !name! also expands inside quotes.
Use %% / phase-aware ! escaping when a literal percent or exclamation
mark must survive.
embedded_doubled_quotes: >-
Inside a quoted argument, "" is an embedded quote pair and is not collapsed
to a single quotation mark by %~1. Example: call :t "a""b" then %~1 expands
to a""b (outer quotes stripped; embedded "" retained). %1 keeps the outer
quotes as ["a""b"].
ascii_double_quotes_only: >-
Only ASCII double quote (code point U+0022) opens and closes cmd argument
quoting. Typographic / curly quotes (U+201C left and U+201D right double
quotation marks, and similar) are ordinary characters: they do not group
arguments or suppress metacharacters. Confirmed on live Windows 10/11 cmd:
a CALL whose first token is curly-quoted hello followed by world yields %1
containing those curly quote characters around hello and %2=world, whereas
ASCII-quoted "hello" world yields one quoted argument slot for hello.
Prefer ASCII quotes in scripts; editors that insert smart quotes break
argument parsing.
double_percent:
for_body_rule: "FOR loop variables use %% in batch files"
echo_literal_rule: "Literal % in ECHO often requires %% escaping"
batch_vs_cmdline: >-
Inside a .cmd/.bat file a literal percent is written as %% (for example
echo Progress: 50%% prints Progress: 50%). On the interactive command line
a single % is usually enough for a literal percent. FOR variables follow the
same split: %%i in batch files versus %i interactively. SET /A modulo also
uses %% in batch files (single % interactively).
call_percent_reduction: >-
CALL performs an extra parse of its argument tail that reduces each %% pair
to a single % (in addition to caret doubling). Example: call echo %%%%
prints a single %.
writing_batch_files: >-
When a parent script writes another .cmd/.bat via ECHO redirection, each %%
pair in the parent becomes a single % in the child file (so echo %%~nx0>>child
stores %~nx0 for the child to expand later). A single %name% or %~ in the
parent expands while writing, baking the current value into the child.
Multilevel generators need additional %% layers per hop. This is ordinary
percent reduction when emitting text, distinct from CALL's argument-tail
reparse (call_percent_reduction) though both halve %% pairs. Caret and
delayed-! emit hops are documented under caret_escape.writing_batch_files
and delayed_expansion.writing_batch_files.
string_operations:
substring_pattern: "%var:~start,length%"
replacement_pattern: "%var:old=new%"
description: "Batch substring and search-replace expansion"
requires_command_extensions: >-
Substring (%var:~...) and replace (%var:old=new%) forms require Command
Extensions (SET /?). With extensions disabled, both percent and delayed
substring/replace forms expand to empty rather than performing the
operation or leaving the operator text literal.
negative_offsets: >-
Negative start counts from the end (%PATH:~-10%). Negative length trims
from the end (%PATH:~0,-2%). Combined forms like %PATH:~7,-5% are valid.
omit_length: >-
If the length is omitted (%PATH:~10%), the substring runs from the start
offset through the remainder of the value (SET /?).
missing_or_empty_substring_batveat: >-
When a name is undefined or expands to empty (including after SET name=),
substring forms like %var:~-1% or %NOSUCH:~0,1% expand to the literal
~-offset[,length] text rather than an empty string. Delayed peers such as
!empty:~-1! behave the same when delayed expansion is enabled. SET name=
unsets the name (IF DEFINED is false); it does not leave a defined empty
value.
missing_or_empty_replace_batveat: >-
The same undefined/empty name rule applies to replace forms. When a name is
undefined or expands to empty (including after SET name=), %var:old=new%
and delayed !var:old=new! do not yield an empty string. Percent expansion
removes %var: (or !var:) and continues scanning, so the remainder
old=new% / old=new! typically becomes the literal old=new text after the
trailing % / ! is consumed (for example %NOSUCH:a=b% and %EMPTY:a=b% after
SET EMPTY= both expand to a=b). Star-prefix forms leave the asterisk as
well (%EMPTY:*a=b% -> *a=b). Prefer IF DEFINED before substring/replace, or
use delayed expansion only after confirming the name is defined. Distinct
from empty_replacement_deletes (%var:old=% on a defined non-empty value).
beyond_length_returns_empty: >-
When the name is defined and non-empty, a substring that starts past the
end of the value (for example %var:~10% on a 6-character string) expands to
an empty string rather than the literal ~offset text. A start within the
string with a length that runs past the end returns only the remaining
characters (for example %var:~3,10% on abcdef yields def).
case_insensitive_search: >-
In %var:old=new%, the search for old is case-insensitive; the replacement
text new is inserted literally. The same case rule applies to delayed
!var:old=new! forms.
replace_all_occurrences: >-
%var:old=new% substitutes every occurrence of old in the value (SET /?),
not only the first match.
empty_replacement_deletes: >-
When new is empty (%var:old=%), each matched occurrence of old is deleted
(SET /?).
star_prefix_replace: >-
In %var:*old=new%, a leading asterisk on the search text replaces from the
beginning of the value through the first match of old.
not_on_batch_parameters: >-
Substring and replace forms apply to environment variable names, not to
batch parameter spellings. %1:~0,2% and %~1:~0,2% do not take a substring of
the argument: after %1 / %~1 expand, the :~... text remains literal (for
example with %1=hello, %1:~0,2% yields hello:~0,2). The same applies to
replace (%1:_=-% yields foo_bar:_=- when %1=foo_bar). Assign first
(set "s=%~1" then %s:~0,2% / %s:_=-%). Confirmed on live Windows 10/11 cmd.
Cross-ref batch_parameters.no_substring_or_replace and
delayed_expansion.not_batch_parameters.
set_a:
description: "SET /A arithmetic expression operators (SET /?)"
requires_command_extensions: >-
SET /A requires Command Extensions (SET /?). With extensions disabled, an
unquoted form such as set /A N=1+1 is treated as a plain assignment whose
variable name includes the /A token (for example name "/A N" with value
"1+1"), not as arithmetic (see command_extensions_off.set_a_and_string_ops).
Quoted forms such as set /A "N=1+1" are a syntax error when extensions are
off (same family as quoted SET "name=value" under OFF).
unary_operators:
- "!"
- "~"
- "-"
binary_operators:
- "*"
- "/"
- "%"
- "+"
- "-"
- "<<"
- ">>"
- "&"
- "^"
- "|"
assignment_operators:
- "="
- "*="
- "/="
- "%="
- "+="
- "-="
- "&="
- "^="
- "|="
- "<<="
- ">>="
grouping: "Parentheses () group subexpressions"
expression_separator: >-
Comma separates multiple expressions in one SET /A; the final expression
value is the overall result (and is printed when SET /A runs outside a script).
hex_literal: "0x12 style hexadecimal integers"
octal_literal: >-
Leading-zero octal (022). Digits 08 and 09 are invalid octal and fail SET /A.
no_binary_literal: >-
SET /A does not accept 0b-style binary integer literals. Only decimal,
0x hexadecimal, and leading-zero octal forms are recognized (SET /?). A
form such as 0b10 fails the expression (non-zero ERRORLEVEL; the target
variable is left unset/unchanged for a failed assignment).
modulo_in_batch: "Modulo uses %% in batch files (single % on the interactive command line)"
quoting_rule: >-
Logical operators and modulus that would be misparsed as command syntax
need the expression quoted (SET /?). Operators that are also cmd shell
metacharacters — & | ^ << >> — must be quoted or caret-escaped; otherwise
cmd applies shell parsing before SET /A arithmetic (SET /?; confirmed on
live Windows 10/11 cmd).
shell_metachar_quoting: >-
Unquoted << is a live syntax error (for example set /A x=1<<2 yields a
message such as 1<< was unexpected at this time). Prefer set /A "x=1<<2".
Unquoted >> is not a SET /A right-shift: cmd treats >> as append redirection
(set /A x=8>>1 appends the command's stdout to a file named 1 and does not
shift). Unquoted & and | split or pipe the command line; use quotes or
caret escapes (set /A x=5^&3, set /A "x=5&3"). Unquoted ^ escapes the next
character (set /A x=2^3 stores 23); XOR needs quotes (set /A "x=2^3") or a
doubled caret (set /A x=5^^1). Confirmed on live Windows 10/11 cmd.
undefined_as_zero: >-
An environment name that is not defined is treated as numeric 0. Names may
appear without % percent signs inside the expression.
bare_names: "Variable names in SET /A expressions expand without requiring %name% wrappers"
bare_name_numeric_truncation: >-
A bare environment name inside SET /A is read with SET /A's integer scanner,
not full percent expansion. A leading signed integer prefix is taken and any
following non-integer junk is ignored with ERRORLEVEL 0 (for example
amt1=1.99 and amt2=2.50 yields set /A sum=amt1+amt2 -> sum=3; v=12abc yields
set /A r=v -> r=12). A value that does not begin with a digit (for example
.5) is treated as 0.
bare_vs_expanded_noninteger: >-
Bare names and %name% / !name! are not equivalent when the stored value is
not a pure integer. Bare names truncate silently (bare_name_numeric_truncation).
Percent or delayed expansion substitutes the literal text first, so an
expression such as set /A r=%amt% with amt=1.99 sees the token 1.99, fails
with a non-zero ERRORLEVEL (often 1073750990 on live Windows 10/11 cmd), and
may still store a truncated leading integer (see decimal_literal_partial_assign).
octal_in_variable_value: >-
Leading-zero octal applies to bare-name values as well as literals: v=010
yields set /A r=v -> r=8. Invalid octal digits in a bare-name value truncate
rather than fail: v=08 or v=09 yields r=0 with ERRORLEVEL 0. By contrast,
literal set /A r=08 fails with Invalid number, leaves the prior value
unchanged, and sets a non-zero ERRORLEVEL (often 1073750991).
integer_range: "Signed 32-bit integers (-2147483648 through 2147483647)"
spaces_around_assignment: >-
Unlike plain SET, SET /A allows spaces around = and other operators inside
the expression.
interactive_display: >-
When SET /A runs from the interactive command line outside a script, it
prints the final expression value (SET /?). Inside a .cmd/.bat script the
same expression is evaluated silently (no automatic print).
expression_without_assignment: >-
SET /A accepts a bare expression with no assignment (for example
set /A 1+2). Assignment operators are optional; when present they require
an environment variable name on the left (SET /?). In scripts the
expression still runs (and can set ERRORLEVEL on failure) without printing.
unary_bang_vs_delayed: >-
SET /A lists unary ! (logical NOT). When delayed expansion is disabled,
set /A N=!0 yields 1. When delayed expansion is enabled, ! is also the
delayed-expansion delimiter, so bare ! in a SET /A expression is consumed as
delayed syntax first: forms such as set /A N=!Z! expand the variable before
arithmetic, and a lone !0 does not reliably apply unary NOT (live cmd often
leaves N=0). Prefer testing unary ! with delayed expansion disabled, or
quote/escape so SET /A still sees a literal bang.
operator_precedence: >-
Operators apply in decreasing precedence (SET /?): grouping (); unary
! ~ -; * / %; + -; << >>; &; ^; |; assignment operators; comma.
left_associative_same_tier: >-
Operators at the same precedence tier associate left-to-right on live cmd
(for example 8/2*2 yields 8, and 8/2/2 yields 2). Parentheses change