I've always hated batch and I still do, even for the most simple things I prefer C or PHP/Perl. But this time I could not go without it, ****sigh****.
I wanted to redirect an echo command to an other command. For example:
echo example | more
but I also wanted to be able to use special characters in the echo part of the pipe:
echo & | more
Which of course did not work. So I tried:
echo ^& | more
Which did not work either. Then by trial-and-error I found:
echo ^^^& | more
and that worked. But as interested programmer I wonder why. Why did ^& not work and ^^^& did?
The reason has to do with how Windows implements pipes - Each side of the pipe is executed in its own CMD shell. Your command echo ^& | more actually attempts to execute
C:\Windows\system32\cmd.exe /S /D /c" echo & "
on the left and
C:\Windows\system32\cmd.exe /S /D /c" more "
on the right. You can see why the left hand side fails - trying to echo an unescaped &. The escape was consumed by the initial parsing phase before the actual left side is executed.
It is also easy to see why your solution works. The left side of echo ^^^& | more becomes
C:\Windows\system32\cmd.exe /S /D /c" echo ^& "
There are many subtle complications when working with Windows pipes. Refer to Why does delayed expansion fail when inside a piped block of code? for more info. The selected answer has the best info, but I recommend reading the question and all answers to get the context of the selected answer.
The first ^ is escaping the ^ character itself (the second ^) and the third ^ is escaping the &.
When you run a command like ECHO ^& | MORE, the ^& is replaced with & by the shell before the output is piped to MORE.
So, when you run ECHO ^^^& | MORE, the shell replaces ^^ with ^ and the ^& with & and then pipes the output to MORE.
Related
This part works well until there is an ampersand in the filename, in which case it crashes my script completely.
echo %filename% | findstr /i /b /c:"%name% (%year%)"
I can't just put the filename into quotation marks because I need to find the string at the beginning. So how can I do both?
For command line use:
echo %file^name:^&=^^^&% | ...
Inside a batch file
echo %%filename:^&=^^^&%% | ...
How it works?
As a pipe creates two new cmd.exe instances, the echo ... will be parsed twice.
The trick is to expand the filename only in the second expansion.
And then to expand and replace the & with ^& to avoid problems with the &.
The caret will be used to escape the ampersand and itself will be removed.
In the second expansion the parser only sees echo %filename:&=^&%.
To force the expansion into the second parse step, the percent signs have to be doubled for batch files.
From the command line, this doesn't work, but a simple caret anywhere in the variable name works.
Alternative solution:
echo "%filename%" | findstr /i /b /c:^"\"%filename% (%year%)\""
This adds simply quotes and uses also quotes in the search expression
Another option is to use delayed expansion, which requires an explicit cmd with the /v:on option.
cmd /v:on /c "(echo !filename!)" | findstr /i /b /c:"%name% (%year%)"
If your batch script already has enabled delayed expansion, then parentheses around the left side are needed to prevent the delayed expansion from occurring within the parent script (see Why does delayed expansion fail when inside a piped block of code?). The child process will still default to disabled delayed expansion, so the cmd /v:on /c ... is still needed.
#echo off
setlocal enableDelayedExpansion
...
(cmd /v:on /c "(echo !filename!)") | findstr /i /b /c:"%name% (%year%)"
Another way to delay the expansion until the sub-process is to escape the expansion
#echo off
setlocal enableDelayedExpansion
...
cmd /v:on /c "(echo ^!filename^!)" | findstr /i /b /c:"%name% (%year%)"
I've got this simple test script:
echo start
falsey ^
&& echo nope
echo end
falsey is a tiny C program I made to help test, it simply returns 1:
int main(int argc, const char** argv) {
return 1;
}
The problem is, when I open up cmd and run this batch script, it inserts a couple spaces between the two &s, which naturally causes it to incorrectly echo nope:
C:\jenkins-foo-test>foo.bat
C:\jenkins-foo-test>echo start
start
C:\jenkins-foo-test>falsey & & echo nope
nope
C:\jenkins-foo-test>echo end
end
I have figured out a few workarounds for this, such as putting quotes around the &&:
falsey ^
"&&" echo nope
Quotes don't actually work in this case, see #dbenham's answer for why.
dropping the ^ and merging the two lines:
falsey && echo nope
or adding a space in front of the &&:
falsey ^
&& echo nope
Each of those three options correctly do not print nope. But they do all still have extra spaces when cmd.exe prints the commands it's running.
So why are these extra spaces being inserted and how can I stop cmd.exe from breaking things without having to contort my code into mildly unnatural shapes all the time?
The line continuation ^ escapes the first character on the next line, so the first & is treated as a literal and is passed as an argument to falsey. The second & is treated as simple command concatenation.
One simple solution to get && to be treated as conditional command concatenation is to put the && on the line before:
falsey &&^
echo nope
or you could put a space before the &&
falsey ^
&& echo nope
The other option is to use the redirection trick that jeb points out in his linked answer, but I never use that trick.
Regarding your "solution" with quoted "&&" - you are fooling yourself. It appears to work when falsey fails. But if your command succeeds, then you have trouble:
echo ONE ^
"&&" echo TWO
-- OUTPUT --
C:\test>echo ONE " && " echo TWO
ONE "
'" echo TWO' is not recognized as an internal or external command,
operable program or batch file.
Note that the && works because the first " is escaped, so the && is not quoted. If you add a space before the "&&"
echo ONE^
"&&" echo TWO
then you get the following
C:\test>echo ONE "&&" echo TWO
ONE "&&" echo TWO
because now the space is escaped and the "&&" is quoted.
Regarding your comment, you can ignore those extra spaces - they are an artifact of how the parser displays the line when ECHO is ON. The parser often rearranges the line significantly, but normally it does not affect the outcome.
For example
< nul set test=OK
echo [%test%]
--OUTPUT--
C:\test>set test=OK 0<nul
C:\test>echo [OK]
[OK]
Note how the echoed SET line is completely rearranged. The redirection is compressed and moved to the end of the statement, with a space before the redirection. You might think the space would be included in the assignment, but you can see that it is not :-)
The only time you have might have to worry about the rearrangement is if the command is used in a pipe.
For example:
(set test=OK&call echo [%%^^test%%])|findstr "^"
--OUTPUT--
C:\test>(set test=OK & call echo [%^test%] ) | findstr "^"
[OK ]
You can see that there is a single unwanted extra space that is included in the SET value. This is an artifact of how pipes are implemented - each side is executed in a new CMD.EXE process, and the line gets parsed multiple times. You can see where the space comes from by using %CMDCMDLINE% to display the command line passed to the left side's cmd.exe.
(set test=OK&call echo [%%^^test%%] %%^^cmdcmdline%%)|findstr "^"
--OUTPUT--
C:\test>(set test=OK & call echo [%^test%] %^cmdcmdline% ) | findstr "^"
[OK ] C:\WINDOWS\system32\cmd.exe /S /D /c" ( set test=OK & call echo [%^test%] %^cmdcmdline% )"
See Why does delayed expansion fail when inside a piped block of code? for more information about many quirks with pipes. In particular, pay attention to the selected answer for a good explanation of what is going on.
I am trying to create a batch file to restart multiple computers from a TXT file. Everything works fine as long as the /c "comment here" parameter has no spaces. If I pass "Testing" as the comment, I get "Testing" in the pop-up for the restart, as expected. If I pass "Testing spaces" as the comment, I still only get "Testing" in the pop-up. With #echo off, I have verified the comment retains the spaces when it is passed to VBS, so I think the problem is that I am running through an "invisible.vbs" script to prevent another CMD window from opening and hanging the original BAT script.
I would like to be able to have a final command run similar to:
shutdown /r /m \\127.0.0.1 /t 120 /c "Your computer will shut down for maintenance tasks in two minutes"
Any help with this would be greatly appreciated!
The essential part of restart.bat:
for /f "tokens=1-3" %%c in (%FilePath%) do WScript /nologo "%windir%\myscripts\invisible.vbs" "shutdown /r /m \\%%c /t %delay% /c %message%"
I have also tried adding extra quotes around %message% with no success:
for /f "tokens=1-3" %%c in (%FilePath%) do WScript /nologo "%windir%\myscripts\invisible.vbs" "shutdown /r /m \\%%c /t %delay% /c "%message%""
The invisible.vbs script (found on StackExchange):
CreateObject("Wscript.Shell").Run "" & WScript.Arguments(0) & "", 0, False
Any help would be greatly appreciated!
EDIT: Using your third method suggested, I am getting a script error:
Script: C:\Windows\myscripts\invisible.vbs
Line: 1
Char: 46
Error: The system cannot find the file specified
Code: 80070002
Source: (null)
EDIT 2: I just tried the first method also, and I now get a vbscript echo pop-up with the correct command that I wish to be sent, but the command is not sent. I am far from a programmer, and the only VBS I have used is stuff I have found online. I do appreciate the help you have offered so far, but I still can't get this to work.
You can not do it. The logic behind the Arguments object in WScript seems to remove quotes.
So, i can think in at least three alternatives
1) The most simple: use another character as an indicator of a quote and replace it with quote in the vbs script
cmd code : cscript myscript.vbs "shutdown /r /m \\%%c /t %delay% /c '%message%'"
vbs code : WScript.CreateObject("WScript.Shell").Run replace(WScript.Arguments(0),"'",""""), 0, False
The only problem with it is at some point, probably, you will need to use the placeholder character as a real character.
2) The most complex: use wmi to retrieve the current process id and from here retrieve the original command line of the script. A lot of code, if interested, here at StackOverflow there are some nice samples.
3) The easy, fast, and unusual. Use environment variables. Save the command into a variable and pass the name of the variable to the script. From it, retrieve the variable contents and use it
cmd code : set "runVar=shutdown /r /m \\%%c /t %delay% /c "%message%""
wscript //nologo "%windir%\myscripts\runInvisible.vbs" runVar
vbs code : With WScript.CreateObject("WScript.Shell") : .Run .ExpandEnvironmentStrings("%" & WScript.Arguments(0) & "%"), 0, False : End With
cmd code or bat code:
wscript //nologo invisible.vbs "hello world" 0
somehow the zero on the end, causes the part between quotes being seen as one argument still
I was having an issue with a script so I went to ss64.com like I usually do to see what gems I might find to help me.
In looking at the page for the CALL command I came across this line:
Redirection with & | <> also does not work as expected.
However, that page and just about anywhere else I've looked does not explain how it works unexpectedly. I know that the | can do some unexpected things in general but I don't know about the others.
What is this unexpected function? Does it depend on how you use the CALL command (calling a label vs script)?
I suppose Rob think of things like this:
call :func | more
exit /b
:func
echo line1
echo line2
exit /b
Or something like
setlocal EnableDelayedExpansion
set var=Line2
( echo Line1
echo !var! ) | more
For this the explanation can be found at(you mentioned this)
SO:Why does delayed expansion fail when inside a piped block of code?
But redirection in the first step works with CALL as expected (and the other characters)
call echo Hello > output.txt
But if you try to use any of the special characters in the second expansion of the CALL command, the complete command will not be executed.
About the special effects of CALL for the batch parser is described at
SO:How does the CMD.EXE parse scripts? (especially phase 6)
set "myCmd=echo 1 & echo 2"
call %%myCmd%%
The double %% have the effect that the first expansion results in %myCmd% and the real expansion of the content will be done not until the second run of the parser, so the & have to be interpreted in the call context.
And also this will result in nothing (because of the parenthesis)
set "myCmd=echo Line1"
call (%%myCmd%%)
But this will obviously call a batch file named 1.bat
echo echo Hello > 1.bat
set "myCmd=echo Line1 && echo Line2"
call (%%myCmd%%)
echo End
This was discussed at dostips:CALL me, or better avoid call
Currently I suppose, CALL works somehow with tokens, but can't handle them in the correct way.
I was wondering if it is possible to read from a pipe in a batch file. If I write:
echo Test
i get, unsurprising, Test. That's nice. But what if I want to pipe the output, and read it from another command?
echo Test | echo ???
How to obtain the same result as before, but through a pipe?
Thanks!
EDIT: what I am after really after is this.
I have a list of files, and i need to filter this list with some words that i put, line by line, in a file named filter.txt. So I have to use findstr /g:filter.txt.
But then I need to do something to the list files that matches, and since findstr returns one row for each file, i have to read the matches line by line.
This is how i did it:
dir /b | findstr /g:filter.txt | for /F "delims=" %a in ('more') do del "%a"
SOLUTION:
It looks like that what I wanted to do was not reading from a pipe but just reading the output of another command in a batch file.
To do a single line read, you could use this:
echo Test | ( set /p line= & call echo %%line%%)
or you can use this, that works also with multi line input:
echo Test | for /F "delims=" %a in ('more') do #echo %a
(this trick of using more could be useful in some situations). But in my particular case, the solution is this:
for /F "delims=" %a in ('echo Test') do #echo %a
Thanks to everyone!
Based on this answer https://stackoverflow.com/a/6980605/1630171 it looks like that a way to answer my question is this:
echo Test | for /F "delims=" %a in ('more') do #echo %a
It's a bit weird but it works :)
It only looks a little strange to me that there's no native solution to this... but this does exactly what i want!
Excuse me, I think there is a confusion here...
You said you want to read from a pipe. A pipe is used to redirect the output of one command into the input of another command; the second command is called filter. For example, in
dir /b | findstr /g:filter.txt
there is a pipe between dir and findstr commands. A pipe is always established between two processes. There is no way to read the data that flow from dir command to findstr command (that is the only pipe that exist here). However, you can read from the output of findstr command.
If we insert an additional filter, the behavior is the same. For example, in
dir /b | findstr /g:filter.txt | more
there are two pipes, but there is no way to read from anyone of them. However, you can read from the output of the last command (more in this case). What is the native Batch solution to read the output of one command? It is the FOR /F command. For example, the native way to get echo command output in:
echo Test | for /F "delims=" %a in ('more') do #echo %a
is:
for /F "delims=" %a in ('echo Test') do #echo %a
Please note that in the first example the %a parameter does NOT get the information from the pipe that exist between echo and for commands, but from the output of more command.
In the same way, the natural method to achieve this task:
dir /b | findstr /g:filter.txt | for /F "delims=" %a in ('more') do del "%a"
is this way:
for /F "delims=" %a in ('dir /b ^| findstr /g:filter.txt') do del "%a"
that process the multi-line output of findstr command.
Second method is not just faster than the former, but it is also clearer because the inclusion of a more command that really do nothing may lead to undesired misconceptions or errors.
Antonio
For reading a single line, you could also use set /p, but this only works with one line.
echo test | ( set /p line= & call echo %%line%%)
The problem is here, that a pipe creates two new cmd.exe contexts, for each side one.
They run in the same window as the parent cmd.exe, they can't change any variables of the parent cmd, as they are only childs.
That's the cause why this one fails
echo test | set /p line=
echo %line%
line will be set, but it will be destroyed when the pipe ends.
Building on another answer I saw elsewhere, it is possible to capture the output of a command and store it in a variable without an intermediary file quite simply, so long as it is numeric.
Child processes, like those inside the pipe cannot share their environment variables, but can return a value which is picked up by %errorlevel%. %errorlevel% isn't an environment variable though, and is calculated by the shell every time it is invoked. It also cannot be set normally, and must be set using a child process. Example:
#echo off
echo %errorlevel%
cmd /c exit 56
echo %errorlevel%
Returns:
0
56
Interestingly, you can also do:
#echo off
echo %errorlevel%
cmd /c exit 56 & echo hi
echo %errorlevel%
Returns:
0
hi
56
Which I believe is because the echo hi is run by another child process in turn, which doesn't wait for the exit statement to finish before printing. This might be changed by a race condition though if the text printed is longer, I'm not to sure if the child process (running exit) which is parent to the one printing 'hi' will wait for it's child to exit (or any subsequent children either) before it completes the exit command. I tried to test this with a longer-running command like Tree, but I got a zero returned by a query to %errorlevel% which is probably due to Tree affecting the results by returning 0, possibly after the exit 56.
Anyway, to get back to what most will find useful:
#echo off
echo %errorlevel%
echo 456 | ( set /p line= & call exit %%line%% )
echo %errorlevel%
pause
Returns:
0
456
Here, the 456 printed by echo is captured and returned by subsequent queries to %errorlevel%. You can capture any command's output this way, although it's limited to one numeric value. This is still very useful, but unfortunately doesn't allow you to store textual output and I can't think of a way to make it work for multi line output either. (unexplored)
I think in theory you can chain as many commands as you want, and should be able to use && to force the order in which they run. I don't know how or if this can be used to capture multiple lines of input or allow the return of text, but should provide some additional wiggle room inside the pipe by providing nested child processes sharing their environment down and possibly return value up. (again untested, perhaps try multiple exit statements or something, and if I learn anything later I'll try to post it here)