Batch File: Prevent literal interpretation in a for /f loop - windows

Currently I have a loop that runs through a list of items and copies them to a directory (archive). However, one of the items in the list (which has a global variable in the path-name) is being interpreted 'literally' (as text instead of code).
I know usually you can just escape the line via (^^) to have it interpreted as code instead of text, but evidently I'm doing something wrong here, because it's not working...
The item in the my.list (with the escape in it) that is having issues is:
location\foo^^%date:~0,3%*.zip
The code I'm using is...
for /f "delims=" %%a in (my.list) do (
echo "%%a"
)
Echo's
"location\foo^^%date:~0,3%*.zip"
Instead of
location\fooMON*.zip
Any help would be greatly appreciated.

You are confused as to when you need to escape a character.
Some characters have special meaning ("code" as you describe it). Often times you can escape the character such that it is interpretted as a literal (text) instead of "code".
The most frequent method to escape a character within Windows CMD.EXE is to prefix it with a single ^ character. Sometimes a string is parsed twice, which can require an escape sequence of ^^^, (or perhaps ^^ when dealing with ! when delayed expansion is enabled). More rounds of parsing require ever more ^ characters. It can quickly become confusing, and requires practice to get the hang of it.
But your situation is completely different - It cannot be solved by escaping. You have "code" within your FOR variable, and you want it to be interpreted as such. But instead, it is being interpreted as text. In order to understand why, you must understand the order in which various stages of batch parsing occur. You could refer to How does the Windows Command Interpreter (CMD.EXE) parse scripts?, but it is pretty advanced stuff that takes time to digest.
Here is a crude synopsis showing when various types of expansion occur. (Note - these step numbers do not match up exactly with the phase numbers of the linked answer)
1) Parameter expansion - %1
2) Normal variable expansion - %var%
3) FOR variable expansion - %%A
4) Delayed variable expansion - !var!
5) CALL expansion - repeat steps 1) and 2) if CALL involved
You want your %date:~0,3% string to undergo normal (percent) expansion. Your FOR loop reads the line of text verbatim, without any expansion. The first time the parser sees your "code" is at step 3) when the %%a FOR variable is expanded. You can see that this is already too late to get
%date:~0,3% to expand the way you want.
You have two choices to solve your problem. But beware - each of these solutions potentially add new issues that may need to be solved.
I am assuming the ^^ is your naive attempt to force expansion of the embedded "code". The ^^ should be removed from your list file.
Option 1: Add an extra round of normal expansion by using CALL
for /f "delims=" %%a in (my.list) do call echo "%%a"
But now you have a potential problem that you might have a % literal in your list that you do not want to be expanded. Percents within batch scripts cannot be escaped with ^. Instead you escape a percent by doubling it as %%. So if you have percent literals in your list, they must be doubled.
Note that the original code that was posted with the question was significantly more complicated. It included an IF statement that referenced %%a. You cannot CALL an IF or FOR command. The solution is to CALL a subroutine, passing the value, and include the complex logic in the subroutine.
for /f "delims=" %%a in (my.list) do call :processValue "%%a" >>Logs\xfer.log
exit /b
:processValue
echo Attempting to archive %1...
if exist "c:\%~1" (
echo f | xcopy "c:\%%a" "c:\Lucas\archive\%~1" /E /C /H /R /Y
if %errorlevel%==0 (
echo ...%1 added to archive for transfer
echo.
) else (
echo ERROR: %1 not added to archive
echo.
)
) else (
echo ERROR: %1 Not found on client computer
echo.
)
Option 2: Use delayed expansion
Enable delayed expansion, and change your list to use !date:~0,3! instead of %date:~0,3%. Delayed expansion occurs after FOR variable expansion, so it will be expanded properly.
setlocal enableDelayedExpansion
for /f "delims=" %%a in (my.list) do echo "%%a"
But now you have a potential problem that you might have a ! literal in your list that you do not want to be expanded. You can preserve ! literals by escaping them as ^!.

Related

Why are strings from text file cut by the FOR loop on passing them to another function?

Prerequisites
I have a file called urls.txt where I store my URLs, e.g.
www.google.de/test1.yaml?at=refs%2Fheads%2Fmaster
www.google.de/test2.yaml?at=refs%2Fheads%2Fmaster
www.google.de/test3.yaml?at=refs%2Fheads%2Fmaster
My goal
Now I want to loop through these URLs and pass them to another function to download them.
:downloader
FOR /F "delims=" %%i IN (urls.txt) DO (call :sub_function %%i)
goto :eof
:sub_function
echo here url is: %~1
Output
The output is that it cuts off the query strings from the URLs and does not pass them completely to the next function.
For example, the output is: www.google.de/test1.yaml?at
What do I miss?
To protect special characters (like the =-sign in your situation, which constitutes a standard token separator just like SPACE, TAB, , and ;), use quotation for the argument, so it is really treated as one.
Then the call command initiates a second %-expansion phase, which is the reason why the %-signs in your argument cause issues (actually the sequence %2 represents the second argument of your script). To circumvent that problem, store the argument string in a variable and ensure that is is going to be expanded during said second %-expansion phase.
Since URLs may also contain the &-symbol, the argument in the sub-function should not become expanded unquoted in order not to misinterpret it as the command concatenation operator.
Here is the corrected code:
:downloader
FOR /F "delims=" %%i IN (urls.txt) DO (
set "ARG=%%i" & call :sub_function "%%ARG%%"
)
goto :eof
:sub_function
echo here url is: "%~1"

Trying to concatenate the last 10 lines of a log file to a batch variable using powershell

I'm new to Windows scripting, but have quite a lot of experience in bash and python.
Here's the issue. Whenever I run this, (and this is the best result I've gotten so far) it makes it most of the way through and then errors with "The filename, directory name, or volume label syntax is incorrect."
Ignore the code designed for newlines, I'm still fighting with that as well.
setlocal EnableDelayedExpansion
set LF=^
set LAST_TEN=Here are the last 10 lines of the download log:
for /f "tokens=* usebackq" %%x in (`powershell -command "& {Get-Content download.log | Select-Object -last 10 | ForEach-Object {$_.substring(2)}}"`) do (
set LAST_TEN=!LAST_TEN!%%x
)
echo %LAST_TEN%
The reason I'm taking the substring is because some of the lines in the logfile start with < and > . I thought that was my only issue, but that is not the case. Please let me know if any more info is needed. Thank you!
Note: Your own answer shows the effective solution, but I thought I'd provide some background information.
Squashman has provided the crucial pointer:
Switching from echo %LAST_TEN% to echo !LAST_TEN! avoids problems with metacharacters (special characters such as < and >) in the variable value, which are what caused your error message.
The alternative would be to double-quote the variable reference - echo "%LAST_TEN%" - but, sadly, the double quotes are then included in the output.
In other words: If you need to echo the value of a variable that (potentially) contains metacharacters unquoted:
Place setlocal EnableDelayedExpansion at the start of your batch file.
Then reference the variable of interest as !VAR! instead of %VAR%: the delayed expansion this results in prevents the value from becoming part of the source-code line that cmd.exe parses (due to the macro-style up-front expansion that happens with %VAR%).
As an aside: Loop variables - such as %%x in your code - despite using % rather than ! as the delimiter, are of necessity always expanded in a delayed fashion, which is the reason that set LAST_TEN=!LAST_TEN!%%x worked even without the double-quoting around enclosing both the variable name and value that is normally required for literals and values of non-delayed variable references containing metacharacters (e.g.
set "LAST_TEN=a < b")
A simplified example:
#echo off
setlocal enableDelayedExpansion
:: Define a sample variable
:: Note the "..." enclosing both the name and the value.
set "var=a value with metacharacters: < > & |"
:: Thanks to using !var!, echoing the value *unquoted* works
echo !var!
Scoping setlocal enableDelayedExpansion:
One pitfall of delayed expansion is that that all ! characters are then considered part of delayed variable references, typically resulting in their quiet removal; e.g., echo hi! outputs just hi.
To escape ! characters in literal strings that should be used verbatim, you need ^^! (sic) in unquoted strings, and ^! inside "...".
The escaping is also needed for %...% variable references (e.g., echo %var:!=^^!%), but is again avoided for !...! ones.
To avoid such escaping headaches you can enable setlocal enableDelayedExpansion on demand, for a given line or block of lines, and disable it again with endlocal:
#echo off
:: Define a sample variable
:: Note the "..." enclosing both the name and the value.
set "var=a value with metacharacters: < > & |"
:: Because setlocal enableDelayedExpansion is NOT (yet)
:: in effect, the use of "!" is not a problem.
echo hi!
:: Localize the use of delayed expansion
setlocal enableDelayedExpansion
echo !var!
endlocal
:: Use of "!" is again fine.
echo hi again!
Caveat: Since setlocal creates a copy of the environment variables, which endlocal then discards, do not try to set variables between setlocal and endlocal if you need later code to see these changes.
As you're already using PowerShell, why not let it do the donkey work?
Grab the last ten lines and concatenate them within parentheses, for example:
For /F Delims^=^ EOL^= %%G In ('%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -Command "(Get-Content 'download.log' | Select-Object -Last 10) -Join ''"') Do Set "LAST_TEN=%%G"
Changed
echo %LAST_TEN%
to
echo !LAST_TEN!

how to deal with possible special characters when iterating files with for command in windows *.cmd file?

the code is:
setlocal EnableDelayedExpansion
FOR /f "usebackq tokens=*" %%X in (`dir /a-d /s /b "!search_path!" 2^>^&1`) DO #(
set file_path=%%X
rem do other stuff
)
Delayed expansion is on because the source path might have special characters like backticks percentages exclamation and ^ escape sing. All these characters are allowed in windows paths and I don't know if and where they will be present.
The problem arise what to do with double percent parameter %%X, how to pass it to another variable without expansion. If DE is on the exclamation sings will be treated as variables with and that would result with a range of weird errors. The same thing is if I disable DE - the same situation, but this time with percentages.
Any idea how to make these lines safe for every possible allowed path that can be found in windows system with no matter how weird characters ?
The problem boils to how to safe pass data from double percent for parameter into normal %variable% so the data can safe passed through delayed expansion from that moment.
I would try to adapt FOR /R to your needs, which will solve some of your escape efforts. You can check the format/match of the file listing in your loop vs. in the dir.
FOR /R will traverse your directory tree (which you're doing anyway) and return the files that match the pattern you give.
Quick example to list all files of type TXT in a directory and it's sub-directories goes like this:
UPDATED:
This prints the contents of two files in my directory that have exclamation points in them:
#echo off
for /r %%i in (ex*!*.txt) do (
type %%~i
)
Note the absense of delayed variable expansion. Add'l variable references are found at the bottom of the for /? listing.

Why is delayed expansion in a batch file not working in this case?

This code
#echo off
setlocal EnableDelayedExpansion
set myvar=first
set first=second
echo myvar:!myvar!
set myvar=!myvar!
echo myvar:!myvar!
gives
myvar:first
myvar:first
on Windows Vista SP2.
The output I had expected is
myvar:first
myvar:second
Why the difference and how to obtain desired effect?
The problem is that set myvar=!myvar! expands to set myvar=first,
you set it with the same content, and then you ask echo myvar:!myvar! to show the content of myvar.
I will try to add some more explanations, even if Aacini and shf301 already answered the question.
Both showed the double expansion with the !%var%! construct, and Aacini explained why it can work, and why the reversed version %!var!% can't work.
IMHO there are four different expansions.
Delayed Expansion:
As Aacini explained the delayed expansion is safe against any special characters in the content (it can handle ALL characters from 0x01 to 0xFF).
Percent Expansion:
The percent expansion can't handle or removes some characters (even with escaping).
It can be useful for simple content, as it can expand variables after an endlocal barrier.
setlocal
set "myVar=simple content"
(
endlocal
set result=%myVar%
)
FOR-Loop-Parameters expansion:
It is safe, if the delayed expansion is disabled, else the delayed expansion phase is executed after the expansion of the %%a variables.
It can be useful, as it can expand variables after an endlocal barrier
setlocal EnableDelayedExpansion
set "var=complex content &<>!"
for /F "delims=" %%A in ("!var!") DO (
endlocal
set "result=%%A"
)
SET Expansion:
set var expands also a variable, and it is always safe and works independent of the delayed expansion mode.
Aacini just explained how the call %%%var%%% construct work, I only want to give some additional remarks.
call is stackable, you can use many of them and each restarts the parser.
set "var=%%var%%#"
call call call call call echo %var%
results to %var%######
But call have many disadvantages/side effects!
Each call double all carets ^
You can say: "Hey I've tested it and I can't see any doubling"
call call call call echo ^^
result ^
Nevertheless it's true, but it's mostly hidden, as each restart also have a special character phase where carets escapes the next character, but you can see the doubling effect with
call call call call echo "^^"^^
result "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"^
Even if a call expansion restarts the parser, you can never use the delayed expansion in any phase (only in the first one).
call stops working if it detects unescaped special characters.
echo you ^& me
call echo you & me
call echo you ^& me
call echo you ^^& me
call echo you ^^^& me
Only the first results to the output you & me, all the others fails.
Another problem is that call is extremly slow, a call set var=content is ~50times slower than set var=content, the cause is that call try to start an external program.
#echo off
setlocal
(
echo echo *** External batch, parameters are '%%*'
) > set.bat
set "var="
call set var=hello
set var
I hope it was interesting a bit ...
And if you want to go more in depth you can read CALL me, or better avoid call
and How does the Windows Command Interpreter (CMD.EXE) parse scripts?
This problem is not directly related to Delayed variable Expansion, but to the fact that two value expansions are required: the first one give the variable name and the second one must replace this name by its value. The direct way to do that is via two expansions in the same line as shown in the previous answer: set myvar=!%myvar%! that works because %var% expansion is done before the command-line is analyzed for execution whereas !var! expansion is done later, just before the command is executed (hence the "delayed" name). This mean that %var% expansion may provide parts of the command and may cause syntax errors, but !var! not. For example if %var%==value ... cause an error if var is empty or have spaces, but if !var!==value ... never cause a syntax error.
The double expansion of values may be achieved in other ways that does not involve Delayed variable Expansion. For example, we may create an auxiliary Batch file that do the second expansion:
echo myvar:%myvar%
echo set myvar=%%%myvar%%%> auxiliary.bat
call auxiliary
echo myvar:%myvar%
Previous method may be used to do a third or even deeper level expansions, and even be combined with Delayed Expansions to create very complex value managements. This matter is not just a curiosity, but the key to access array elements or linked lists. For example:
set month[1]=January
set month[2]=February
. . .
set month[12]=December
for /f "tokens=1-3 delims=/" %%a in ("%date%") do echo Today is !month[%%a]! %%b, %%c
What you're trying to do won't work - delayed expansion only changes the variable expansion behavior of a variable inside of a block. It doesn't allow you the aliasing/nesting (for a lack of a better word) that you are attempting.
set myvar=first sets the variable myvar to the text "first". set first=second sets the variable first to the text "second. There is no link between those two lines. myvar will never evaluate to something that it wasn't explicitly set to.
I don't believe there is anyway to accomplish what you are trying to do here.
* Edit *
OK after taking a look at your answer I seeing how that works, you can get your desired output with this:
#echo off
setlocal EnableDelayedExpansion
set myvar=first
set first=second
echo myvar:%myvar%
set myvar=!%myvar%!
echo myvar:%myvar%
So the magic seems to happen because of the way that standard and delayed expansion occur. The line set myvar=!%myvar%! is seems be expanded first by the standard expander to set myvar=!first! (you'll see this if you run the script with echo on). Then the delayed expander runs and expands !first to "second" and set's myvar to that.
I have no idea if this is documented behavior as to how standard and delayed expansion should work or just an implementation detail (which means it could break in the future)

What does this batch file code do?

What does this bat code do?
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
set n=%%~ni
set t=!n:~0,-4!
cp Client\Javascript\%%i build\Client\Javascript\!t!.js
)
What does %%~ni,~n:~0,-4!,%%i,!t! mean?
Keep in mind that in batch files, you need to escape percentage signs unless you're referring to arguments given to the batch file. Once you remove those, you get
for /f %i in ('dir /b Client\Javascript\*_min.js') do (
set n=%~ni
set t=!n:~0,-4!
cp Client\Javascript\%i build\Client\Javascript\!t!.js
)
%i is the declaration of a variable used to place the current file for has found. %~ni extracts the filename portion of %i. !n:~0,-4! uses delayed expansion to remove the last four characters from %n% (set in the previous line) !t! is simply delayed expansion of the %t% variable set in the previous line.
Delayed expansion is used because otherwise, the variables will be substituted as soon as the line is encountered, and future iterations will not re-expand the variable.
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
Iterate over every file in the Client\Javascript folder that match "*_min.js". Thedircommand andfor /f` are totally unneeded here, though and only complicate things, especially when file names contain spaces, commas and the like. A more robust and simpler alternative would be
for %%i in (Client\Javascript\*_min.js) do (
But that's just beside the point. People tend to write unelegant batch files sometimes, ignoring the pitfalls and common errors. That's just one example of that.
set n=%%~ni
Creates a variable n, containing the file name (without any directory information or extension) of the file currently processed. We remember that the for statement iterates over every file it finds. With this line starts what it does with those files.
set t=!n:~0,-4!
Creates a second variable, t, containing everything but the last four characters of the file name. This essentially strips away the "_min"
cp Client\Javascript\%%i build\Client\Javascript\!t!.js
Finally, this copies the original file to the directory build\Client\Javascript with the new name, just constructed. So a file like Client\Javascript\foo_min.js will be copied to Client\Javascript\foo.js. The !t! here is just a delayed-evaluated environment variable. More on that below. Here it should suffice that it just inserts the contents of said variable at that point in the line.
Again, bad practice here that will break in numerous interesting ways:
cp is not a command on Windows so this batch will assume cygwin, GNUWin32 or similar things installed. I tend to avoid having too many unneeded dependencies and stick to what Windows provides; in this case the copy command. Two bytes won't kill anyone here, I think.
No quotes are around either argument. Leads to interesting results when spaces start appearing in the file name. Not good, either.
As for why delayed expansion was used (! instead of % surrounding the variables: The for command consists of everything in the block delimited by parentheses here as well. The entire block is parsed at once and normal variable expansion takes place when a line/command is parsed. That would mean that every variable in the block would be evaluated before the loop even runs, leaving just the following:
for /f %%i in ('dir /b Client\Javascript\*_min.js') do (
set n=%%~ni
set t=
cp Client\Javascript\%%i build\Client\Javascript\.js
)
which is certainly not what you want in this case.
Delayed expansion is always needed when creating and using variables in a loop such as this. A workaround not needing delayed expansion would be to offload the loop interior into a subroutine:
for /f %%i in ('dir /b Client\Javascript\*_min.js') do call :process "%%i"
goto :eof
:process
set n=%~n1
set t=%n:0,-4%
copy "Client\Javascript\%~1" "build\Client\Javascript\%t%.js"
goto :eof
Since the subroutine is not a single "block" (something delimited by parentheses) it will be parsed line by line as usual. Therefore it's safe to use normal expansion instead of delayed expansion here.
A complete help for the FOR command can be found on the Microsoft TechNet site. See here for more information on delayed expansion :
// Pseudo code
for each file named *_min.js in the specified directory
n is set to the file name (*_min)
t is set to the file name, excluding the last 4 characters (*)
the file is copied and renamed t.js to the specified directory
%~ni expands to just the filename part of i.
!n:~0,-4! expands to all but the last four characters of n.
In general, help for at the command prompt will give an overview of the multitude of ways for can expand variables these days.

Resources