batch: ampersand causing issues with findstr - batch-file

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%)"

Related

Deferring expansion in a Windows command line with nested commands

I have a command line that's being invoked by a third party executable (Intune Management Extension). Unfortunately, this executable prefers to expand environment variables in my command line before executing them. Further, it doesn't load system environment variables before it does this expansion.
So, if I put something like this in the command line, it resolves to nothing:
cmd /s /c "echo profile is: %userprofile%"
profile is:
I've found I can work around the problem by using delayed expansion as follows:
cmd /s /v /c "echo profile is: !userprofile!"
However, this breaks down in my real scenario, which is the following:
cmd /s /v /c "cmd /s /v /c "cmd /s /v /c "echo hello" > !userprofile!\tst.log""
The system cannot find the path specified.
however, oddly, this works:
cmd /s /v /c "cmd /s /v /c "echo hello" > !userprofile!\tst.log"
Can someone explain to me why the additional level of nesting breaks this?
An alternative acceptable answer to this question: how can I take an arbitrary sequence of valid command lines, combine them into a single line command chain safely (using &&) and redirect the output of ALL of them to an output log file? Keep in mind, my limitation here is that this is third party software and has the ability to execute a single command line.
A real world example - I have 3 commands:
Installs some software: powershell .\Install.ps1
Records if the software was installed successfully: reg add "HKEY_CURRENT_USER\SOFTWARE\Foo\bar" /v "Installed" /d "%date:~4% %time%" /f /reg:64 > nul
Outputs to a log file in the user profile directory: >> %userprofile%\tst.log
How can I combine this into a single command line?
Right now, I have
cmd /s /c "cmd /s /c "cmd /s /c "powershell .\Install.ps1" && reg add "HKEY_CURRENT_USER\SOFTWARE\Foo\bar" /v "Installed" /d "%date:~4% %time%" /f /reg:64 > nul" >> "%userprofile%\install.log" 2>&1"
This works, with the exception of the early expansion of %userprofile%...
Note - the executing third party software blows up if my outer command redirects the output (>>), hence the extra layer of wrapping...
The special characters in the command line is %, &, > and !. Some of these characters may need to be escaped to delay their use. If present in a string i.e. ">", then this can make them literal or if cmd removes the surrounding "" from the command line, may just delay their special action once they are no longer part of a string.
Can escape % with by doubling or more if needed i.e. %%. &, > and ! can be escaped with a caret ^ if needed. % and ! can remain special in strings. View ss64.com about Escape Characters, Delimiters and Quotes at the Windows command line.
In the OP, you have tried delayed expansion using cmd /v and replacing % with !.
You have also tried nesting cmd commands with up to 3 instances.
Consider example 1:
cmd /c echo 1 && echo 2 > file.txt 2>&1
Expectation is:
cmd /c echo 1 will execute.
echo 2 > file.txt 2>&1 will execute if already in a cmd process.
Consider example 2:
cmd /c (echo 1 ^&^& echo 2) > file.txt 2>&1
Expectation is:
cmd /c (echo 1 ^&^& echo 2) > file.txt 2>&1 will log line 1 (+ Space) and line 2 to file.txt.
The && is delayed by the preceding ^ before each character and the echo output is grouped by () so that file.txt gets all output.
The actual command to try:
cmd /v /c (powershell -file .\Install.ps1 ^&^& reg add "HKCU\SOFTWARE\Foo\bar" /v "Installed" /d "!date:~4! !time!" /f /reg:64 ^> nul) >> "!userprofile!\install.log" 2>&1
I do not have or have I previously used Intune Management Extension to be certain of success. The use of cmd /v and use of ! instead of % is based on your testing results etc.

Batch file - How to use FindStr with percent

I have a batch file with the following header:
#Echo off
SETLOCAL EnableDelayedExpansion EnableExtensions
Within a If statement, enclosed in parenthesis (), I have the following code:
Echo SOME_VAR|FindStr /r /C:"^.*SOME.*$"
Echo Error: !errorlevel!
Echo %%SOME_VAR%%|FindStr /r /C:"^.*SOME.*$"
Echo Error: !errorlevel!
This will print:
SOME_VAR
Error: 0
Error: 1
If SOME_VAR is an existing environment variable, this is what I get. If I delete the variable, I get the expected success.
What happens here? Do I need to escape something more? How can I get a successful find on the 2nd one if the variable exists? I'm only interested in doing a text search where the searched text contains the % character and happens to match an existing variable name.
By the way, the source for the comparison will eventually be a variable too, in which I've loaded the PATH as read from the Windows Registry. So eventually, the string I am searching for will become /C:"^.%%SOME_VAR%%;.*$"
My PATH variable looks like this:
%SOME_VAR%;C:\Windows\system32...................etc
Yes, there is another layer of escape required because each side of the pipe is executed via CMD /C with a command line context, not a batch context. The initial batch parser transforms %%SOME_VAR%% to %SOME_VAR%. The command line parser then leaves %SOME_VAR% as is if the variable is undefined. We need to prevent the expansion if the variable is defined. Doubling the percents does not work in a command line context. The solution is to insert a disappearing caret somewhere between the percents, like %^SOME_VAR%. The caret is treated as part of the variable name, so it prevents expansion of the variable (unless you have a variable named ^SOME_VAR). After failed expansion, the caret is consumed by the normal escape process. The caret must be escaped so that the batch parser passes the caret to the CMD /C command line context.
The final batch line becomes:
Echo %%^^SOME_VAR%% | FindStr SOME_VAR
Note: I simplified the FINDSTR command into a much simpler, but equivalent search.
When you modify the search on the right to include the percents, you will need to insert the escaped caret as well.
Update in response to question in comment
The following code demonstrates some possible ways to work with variables:
#echo off
setlocal disableDelayedExpansion
:: Put both the text you want to search, as well as the search itself, in variables
set "text=%%SOME_VAR%%;C:\Windows\system32...................etc"
set "search=%%SOME_VAR%%"
echo text=%text%
echo search=%search%
echo(
echo(
echo Starting with delayed expansion disabled
echo -----------------------------------------
echo(
:: Use delayed expansion to safely expand any variable without worrying about content.
:: Both sides of the pipe are executed in a command line context with delayed expansion disabled.
:: You must use CMD /V:ON to enable delayed expansion within the pipe.
echo Test when SOME_VAR is undefined:
set "SOME_VAR="
if 1==1 (
cmd /v:on /c echo !text!|cmd /v:on /c findstr "!search!"
)
echo(
echo Test when SOME_VAR is defined:
set "SOME_VAR=DEFINED"
if 1==1 (
cmd /v:on /c echo !text!|cmd /v:on /c findstr "!search!"
)
echo(
setlocal enableDelayedExpansion
echo(
echo(
echo Now try with delayed expansion enabled
echo -----------------------------------------
echo(
:: Even though delayed expansion is enabled within the batch script, it is still disabled
:: within the pipe, so we still have to explicitly enable it via CMD /V:ON.
:: But now we must prevent expansion of the variables while in the batch context.
:: You have two options:
:: 1) Escape the !. The escape syntax changes depending on whether it is inside quotes or not:
echo Escape test:
if 1==1 (
cmd /v:on /c echo ^^!text^^!|cmd /v:on /c findstr "^!search^!"
)
echo(
:: 2) Enclose both sides of the pipe within parentheses (very weird, but it works)
echo Parentheses test:
if 1==1 (
(cmd /v:on /c echo !text!)|(cmd /v:on /c findstr "!search!")
)
--OUTPUT--
text=%SOME_VAR%;C:\Windows\system32...................etc
search=%SOME_VAR%
Starting with delayed expansion disabled
-----------------------------------------
Test when SOME_VAR is undefined:
%SOME_VAR%;C:\Windows\system32...................etc
Test when SOME_VAR is defined:
%SOME_VAR%;C:\Windows\system32...................etc
Now try with delayed expansion enabled
-----------------------------------------
Escape test:
%SOME_VAR%;C:\Windows\system32...................etc
Parentheses test:
%SOME_VAR%;C:\Windows\system32...................etc

jrepl.bat only prints the first line

I'm trying to make jrepl.bat work on several lines. See this little test:
set NLM=^
set NL=^^^%NLM%%NLM%^%NLM%%NLM%
echo First line%NL%second line | jrepl.bat i o
Is there a reason why it only prints the first line:
Forst lone
?
As #MCND mentioned it's a problem of the child process and that the line is parsed two times.
But it can be done with a simple linefeed.
set LF=^
cmd /q /v /c"echo First line^!LF^!second line" | jrepl.bat i o
This works independent of the delayed expansion mode.
As if delayed expansion is enabled in the batch file, the exclamation marks are escaped in the delayed expansion parser phase (and the carets are removed).
Else with delayed expansion disabled the line with the carets will be executed unchanged in the child cmd.exe process, but there the carets are removed in the special charaters phase so the delayed expansion will also work later.
Pipes are created between processes, so, the echo command is executed in a separate cmd instance that does not parse the input line (with the inner line feeds) as you expect.
Try with
setlocal enableextensions disabledelayedexpansion
set NLM=^
set NL=^%NLM%%NLM%^%NLM%%NLM%
cmd /q /v /c"echo First line!NL!second line" | jrepl.bat i o

Batch Removing double quotes or escaping properly ampersand

I want to execute some program passing an argument. The argument changes depending of the day of week and it is an url
Code:
#echo off
setlocal enableDelayedExpansion
for /f %%a in ('wmic path win32_localtime get dayofweek /format:list ^| findstr "="') do (set %%a)
if %dayofweek% == 7(
EXIT
)
set link="http://backlog.telecom.pt/maps/selector/download?map_name=workline_fornecedores&organization_id=1"
if %dayofweek%==5 (
set link="http://backlog.telecom.pt/maps/selector/download?map_name=all_fornecedores&organization_id=1"
)
REM start /wait D:\Planview\Services\BackLog_Integration_Client_Service\Backlog_Integration_Client_Exe\Backlog_Integration_Client_Exe.exe %link%
REM timeout /t 600 /nobreak > NUL
REM start D:\Planview\Services\PV_Backlog_ProcessData_Service\PV_Backlof_ProcessData_Exe\PV_Backlog_ProcessData_Exe.exe
I read that ^ before & would work to escape the & char, but for me it never did and the only way i managed to do it was enableDelayedExpansion and encapsulate the url in ", but this brought me a problem.. my variable instead of having the url has "url".
I tried to do set link=%link:"% but it did not worked.
I'll try to give you some advice via simple examples:
#setlocal enableDelayedExpansion
rem This fails because & is a "poison" character (an instruction or operator)
echo abc&xyz
rem This succeeds because & is escaped
echo abc^&xyz
rem This succeeds because & is quoted
echo "abc&xyz"
rem This succeeds, but it consumes the escape: stored value = abc&xyz
set test=abc^&xyz
rem So this fails because & is not escaped
echo %test%
rem You could "fix" above by double escaping when doing SET so stored value = abc^&xyz
rem But I don't like this - I pretty much never do it
set test=abc^^^&xyz
rem Now this works
echo %test%
rem Better to single escape and use delayed expansion.
set test=abc^&xyz
rem This works because poison characters don't bother delayed expansion
echo !test!
rem You could use quotes instead of escape, but now quotes are in value
set test="abc&xyz"
rem Delayed expansion is not needed because value is quoted
echo %test%
rem Putting the leading quote before the variable still quotes everything
rem But now the quotes are not in the value, as desired. Value = abc&xyz
set "test=abc&xyz"
rem Now you must use delayed expansion because value is not quoted
echo !test!
So the general rules of thumb that I like to use when poison characters are involved:
Use quotes around the entire assignment when SETting a variable: set "var=value"
Use delayed expansion when expanding a variable: !var!
Wouldn't it be nice if those rules solved everything. But of course batch is not that simple. There are situations where these simple rules will fail, but it should be enough to get you started.

Setting variables and taking effect in one batch command line

I just found out that if you do the following:
set Variable=Test & echo %Variable% --Outputs "%Variable%"
echo %Variable% --Outputs "Test"
The change won't take effect until a new line runs. I need to have it take effect immediately as I need to use it with a very long, one-lined command.
You need delayed expansion or call echo:
#echo off
setlocal enableDelayedExpansion
set var=val&echo !var!
endlocal
set var=val&call echo %%var%%
If you have compositions of commands put together with & or in brackets the set command will take effect after all of them are executed.So you need or delayed expansion (which will allow you to access the variables with ! instead of %) or call
To enable the delayed expansion in command prompt you need to start like this cmd /v:on :
>cmd /v:on
>set Variable=Test & echo !Variable!

Resources