18

Okay, I learned that one can use & or && to combine multiple commands into a single line, but it seems that variables that are set aren't actually available for interpolation in the same line:

C:\Users\Andrew>set foo=hello, world!&& echo %foo%
%foo%

C:\Users\Andrew>echo %foo%
hello, world!

Why can't I make this work, and is there any way to make it work in a single line?

The reason I need a one-liner is that an external program I'm working with accepts a single command as a pre-run hook, and, of course, I need to run multiple commands.


Preemptive Defenses

  1. "hello, world! should be surrounded in double-quotes!" Actually, doing so seems to store literal double-quotes in the variable, which I do not want, e.g.

    C:\Users\Andrew>set bar="hello, world!"&& echo %bar%
    %bar%
    
    C:\Users\Andrew>echo %bar%
    "hello, world!"
    
  2. "There should be a space before the &&!" Actually, doing so seems to store a trailing space in the variable, which I do not want, e.g.

    C:\Users\Andrew>set bar="hello, world!"&& echo %bar%
    %bar%
    
    C:\Users\Andrew>echo %bar%
    "hello, world!"
    
  3. "Both!" >:(

    C:\Users\Andrew>set mu="hello, world!" && echo %mu%
    %mu%
    
    C:\Users\Andrew>echo (%mu%)
    ("hello, world!" )
    
Zoe
  • 27,060
  • 21
  • 118
  • 148
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145

4 Answers4

17

You can do it in the same line, but I would recommend to use a batch file like preHook.bat.

As one liner

set "mu=hello, world!" && call echo %^mu%

At first you can see that the quotes are different than yours.


Here's why:

set test1="quotes"
set "test2=no quotes" with comment

In the first test, the quotes will be part of the test1 variable, as well as all characters after the last quote.

In the second test, it uses the extended syntax of the SET command.
So the content will be no quotes, as only the content to the last quote is used; the rest will be dropped.

To echo the variable content, I use call echo %^mu%, as percent expansion will expand it when the line is parsed, before any of the commands are executed.

But the call command will be executed later, and it restarts the parser, which, when used at the command line, uses different expansion rules than the batch parser: an empty variable (in this case, %^mu%, in the first time) stays unchanged in the line; but, in the next parser phase, the ^ caret will be removed.

In your case, call echo %mu% would also work, but only when mu is always empty. The caret variant also works when mu has content before the line is executed.

More about the parser at SO: How does the Windows Command Interpreter (CMD.EXE) parse scripts? And about variable expansion at SO: Variable expansion

Andrew
  • 5,839
  • 1
  • 51
  • 72
jeb
  • 78,592
  • 17
  • 171
  • 225
  • Perfect! Can you explain what the `%^mu%` is doing? (I don't know how to search for it.) – Andrew Cheong May 02 '14 at 06:11
  • 2
    @AndrewCheong Hope my explanation is understandable – jeb May 02 '14 at 06:17
  • 1
    Great explanation. I didn't realize `set` had this alternative syntax, and also that's a really clever trick, the vanishing caret. Thanks so much! (And yes, I considered putting everything in at `bat` file but for... let's say, psychological/logistical reasons, a one-liner would be much-preferred. It's the difference between, _"Okay, everyone, just copy/paste this line into your window, and voila!,"_ and _"Everyone download this file, put it somewhere in your `$PATH`,"_ etc.) – Andrew Cheong May 02 '14 at 06:25
  • 1
    @jeb If only your English grammar was as good as your batch grammar rofl... I edited your answer to make it a lot more readable, but I'm not quite sure I understand the `%^mu%` part. I **think** what you're saying is that you use `^` to keep the first phase from evaluating `%mu%` prematurely, and you use `call` to have it parse the echo command a second time, after the caret is removed but also after the set command has been flushed through. Is that correct? Thanks. – Andrew Oct 25 '17 at 12:44
  • 1
    @andrew Thanks for improving my answer. I learn many things from the corrections. And yes, you got it right. – jeb Oct 25 '17 at 13:56
  • You saved my life – Junru Zhu Mar 24 '20 at 06:44
  • The caret trick doesn't work in scripts, but you can do this instead: `set "mu=hello, world!" && call echo %%mu%%` (Again, scripts only.) – Vopel Jan 11 '22 at 00:56
8

Though I accepted @jeb's answer, ultimately I had to go another route, because I needed to pipe the output of my command, and doing so on call led to mangled results. Indeed, the documentation for call itself remarks,

Do not use pipes and redirection symbols with call.

Instead, reminded by @Blorgbeard of cmd's /v option (and I believe that should be lowercase, not uppercase), I realized I could simply start a subprocess:

C:\Users\Andrew>cmd /v /c "set foo=hello, world!&& echo !foo!"
hello, world!

(For some reason, /v must appear before /c.) Within the quotes, I was able pipe my output to other utilities. One tip for those taking this path: If you find yourself needing to use quotes within those quotes, I suggest avoiding them altogether and trying character codes, e.g. \x20 for space, \x22 for double quotes, and so on.

For example, this was the eventual solution to my problem (warning: may cause eyes to bleed):

C:\Users\Andrew>cmd /v /c "set source=C:\source& set target=C:\target& set archive=C:\archive& robocopy.exe !source! !target! /l /e /zb /xx /xl /fp /ns /nc /ndl /np /njh /njs | sed -e s/^^[\t\x20]\+// | sed -e /^^$/d | sed -e s/^!source:\=\\!// | sed -e s/.*/xcopy\x20\/Fvikrhyz\x20\x22!source:\=\\!^&\x22\x20\x22!archive:\=\\!^&\x22/"
Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • 5
    _>>> For some reason, /v must appear before /c_ that is because anything specified after /c is argument to /c itself and /v is argument for cmd.exe thus needs to be put before /c – sactiw May 10 '17 at 19:47
5

Try the following:

cmd.exe /v /c "set foo=hello, world & echo !foo!"

The /v argument enables delayed variable expansion. This allows you to access variables' values at execution time rather than at parse time (the default). You do this by writing !foo! instead of %foo% (with this /v option turned on).

Instead of passing /v, you can also turn delayed variable expansion on permanently via the registry, which obviously affects the entire system:

[HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor]
"DelayedExpansion"= (REG_DWORD)
1=enabled 0=disabled (default)
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
1

I could not get an IF command to work with either call or cmd /v /c so I would like to offer another option for those who may need it: use FOR /F to declare and use a variable within a single command. For example:

FOR /F %i IN ('echo 123') DO (IF %i==123 echo done)

The %i is the variable that is set with the result of the IN command, which in this example is 'echo 123'.

For values with single quotes or spaces, use the "usebackq tokens=*" flag to put the command in backquotes:

FOR /F "usebackq tokens=*" %i IN (`echo "hello, ' ' world!"`) DO (IF %i=="hello, ' ' world!" echo done)
JohnP2
  • 1,899
  • 19
  • 17