5

I ran into an "odd" (to me) problem while writing a batch file, and hope someone can explain why it does what it does... I had a batch file that built-up a moderately complex command-line, and I wanted it to echo what it had built to the screen before executing so that I could check that it had been built correctly. There are other ways I could have done this1, but for various reasons I tried something of the form:

for %%a in (echo "") do %%~a complicated-command-line-with-parameters

The thought being that it would run the do part twice: once with %%a set to echo (displaying the command to the console) and once with it set to "": the expectation was that it would actually execute the command (using %%~a would strip the double-quotes and all that would be left is the command itself). However, while the echo worked, the command itself was not executed.

For a minimal replication, run the following from a command-prompt:

C:\>for %a in (echo "") do %~a dir

C:\>echo dir
dir

C:\> dir

As can be seen, it runs the echo command fine, but it seems that the space in front of dir prevents it from being executed.

Note, though, that manually running a command with a leading space is fine:

C:\> dir
 Volume in drive C is OS
 Volume Serial Number is A8BD-F861
...

and, were it somehow trying to run the command <space>dir, then it (probably) would have complained of a missing command:

C:\>" dir"
'" dir"' is not recognized as an internal or external command,
operable program or batch file.

Question: The space between %~a (which is an empty string) and the command to be run seems to be causing the whole line to be ignored... does anyone know why?

Note: It make no difference whether it is run from the command-prompt (as above) or from within a .BAT file (after changing %a to %%a etc.). Nor does it make any difference whether the command-to-be-run is a built-in command (e.g. dir as above) or a standalone program.

Further evidence that it is the space between %~a and the command that is causing the problem comes from the "fix" that I found:

C:\>for %a in ("echo " "") do %~adir

C:\>echo dir
dir

C:\>dir
 Volume in drive C is OS
 Volume Serial Number is A8BD-F861
...

By adding the "separating space" to the "echo " string inside the for command, and removing it from the do clause (...do %~adir), the command works as I originally expected (although I dislike not having a space in front of the command).

After skimming the (somewhat daunting) top answer to the question How does the Windows Command Interpreter (CMD.EXE) parse scripts? that SomethingDark helpfully linked to, a cleaner alternative that seems to work is:

C:\>for %a in (echo call) do %~a dir

C:\>echo dir
dir

C:\>call dir
 Volume in drive C is OS
 Volume Serial Number is A8BD-F861
...

There are probably idiomatic situations where having the extra call might affect something, but for the moment, it seems to work as desired.


1 I could have put @echo on just before the line inside the batch file, but that also causes the prompt (e.g. C:\MyDirectory\SubDir>) to be shown, which I didn't want. The other "standard fallback" is to just duplicate the line and stick echo in front of the first copy, but then it's too easy for them to get out-of-sync!

TripeHound
  • 2,169
  • 17
  • 30
  • 3
    I suspect that somebody smarter than I am will point you towards https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts, but I'm not sure which part is relevant here. – SomethingDark Aug 23 '19 at 12:09
  • You could substitute `"cmd/c"` for the `""` in your first piece of code. It's not really the same thing, but might work for your use case. – Klitos Kyriacou Aug 23 '19 at 13:08
  • @SomethingDark Thanks for the link... something to study when I'm bored :-) From a skim, and [jeb's answer](https://stackoverflow.com/a/57626831/2096401) it looks like the `%~a dir` is tokenised to `%~a` and `dir` first, _then_ substitution happens and – when it turns out that `%~a` is an empty string, nothing happens. – TripeHound Aug 23 '19 at 13:20
  • 2
    @KlitosKyriacou Using `call` also works. – TripeHound Aug 23 '19 at 13:22
  • 1
    @TripeHound: please don't include your solution(s) in the question. Put them into an answer instead. (btw: nice analysis `:)`) – Stephan Aug 24 '19 at 08:28
  • @Stephan But I wasn't asking _how_ to do what I wanted (I'd already worked that out, and included it as part of the analysis); I was asking _why_ my first attempt doesn't work. – TripeHound Aug 24 '19 at 09:56

1 Answers1

3

SomethingDark already pointed to the explanation.

It's the command vs argument token splitting of a line.
In your case the command token is always %~a this will be replaced later, but even when its empty, the parser will not reevaluate the command token.

With your fix, the dir command is always part of the command token.
But when echo is prefixed it can still echo the remaining part.

aschipfl
  • 28,946
  • 10
  • 45
  • 77
jeb
  • 70,992
  • 15
  • 159
  • 202
  • 1
    Yes, from the answer @SomethingDark linked, that seems to be what's happening: `%~a` is remaining a separate token. _Slightly odd_ that it doesn't complain about not knowing about an "empty command"... `"" dir`, which is about as close as I can get to what seems to be happening, gives the `'""' is not recognized as an internal or external command` message. One might think `%~a dir` would do something similar... – TripeHound Aug 23 '19 at 13:18
  • @TripeHound, the fact that the empty command token does not cause an error surprises me too! The `%a` (no `~`!) causes an error like literal `""`... – aschipfl Aug 23 '19 at 14:11
  • 1
    Another proof that tokenisation occurs before `for` meta-variable expansion is this: `for %a in ("echo text") do %~a` (this fails)... – aschipfl Aug 23 '19 at 15:49
  • Well, I have to retract my previous comment, because `for %a in ("echo text") do %~a` correctly echos `text` (sorry, I do not know what I have done then); also `for %a in ("find text") do %~a` succeeds, meaning that the external command `find` is correctly recognised; however, `for %a in ("echo(text") do %~a` really fails since `echo(text` is seen as a single command token... – aschipfl Aug 26 '19 at 15:09
  • Ooh - Not sure why I never thought of this before - another form of comment. Better yet, as long as delayed expansion is enabled, then `!! This is a remark`, as well as `!!=This is a remark`. Obviously a token delimiter is required after the `!!` – dbenham Aug 26 '19 at 15:35