Escape percent signs in given variables - windows

My first post, most questions already solved using this friendly provided knowldge here. But now I run out of ideas, again with a question about handling of poison characters in cmd.exe.
Let's assume there is a given string variable enclosed in double quotes. Most poison characters has already been replaced by common chars before, the left ones disturbing the script are "&", "(", ")" and "%". The string must be echoed to a file without quotes afterwards. So I had the idea to escape the poison characters tripled:
#echo off & setlocal ENABLEEXTENSIONS
SET AlbumArtist=%1
CALL :EscapePoisonChars %AlbumArtist% AlbumArtist_VDN
SET "FlacHyperLink==hyperlink^("file://%AlbumArtist_VDN%"^;"LossLess"^)")
echo %FlacHyperLink%
echo %AlbumArtist_VDN%
endlocal &GOTO:EOF
:EscapePoisonChars
#echo off & setlocal ENABLEEXTENSIONS
SET TmpString=%1
SET TmpString=%TmpString:&=^^^&%
SET TmpString=%TmpString:(=^^^(%
SET TmpString=%TmpString:)=^^^)%
endlocal&SET %2=%TmpString:~1,-1%&GOTO :EOF
When I call my script above I get the expected output - apart from the missing percent sign:
G:\YAET\20130204_Work>TryAmper.bat "100% Rock & Roll (7' UpMix)"
=hyperlink("file://100 Rock & Roll (7' UpMix)";"LossLess")
100 Rock & Roll (7' UpMix)
G:\YAET\20130204_Work>
I know that the percent can be escaped by itself. So "%%" will normally lead to a single literal "%". But it was not possible for me to find a working replace procedure for percent signs because cmd always interprets it as a variable and tries to expand it. Is this the complete wrong direction to handle this issue or just misunderstanding of variable expansion? Any hints welcome! Thanks!
Cheers, Martin
Edit
Removed own code, see below Jeb's answer for clean solution.
Thanks for help, Martin

Nice question!
At first, yes you can replace even percent signs, but not within a percent expansion, you need a delayed expansion here.
Setlocal EnableDelayedExpansion
set tmpstr=!tmpstr:%=%%!
But if you use the delayed expansion, you don't need the escapes anymore, as the delayed expansion is the last phase of the batch parser and all characters lose any special meaning.
You only need to echo with delayed expansion.
Echo !tmpvar!
EDIT: Clean solution
#echo off
setlocal DisableDelayedExpansion
REM * More or less secure getting the parameter
SET "AlbumArtist=%~1"
setlocal EnableDelayedExpansion
SET "FlacHyperLink==hyperlink("file://!AlbumArtist!";"LossLess")"
echo !FlacHyperLink!
echo !FlacHyperLink!> hugo.txt
You need disableDelayedExpansion first, to get even exclamation marks from %1.
After that, you should switch to delayed expansion and use it anywhere.

Related

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!

Percent sign disappears when passed by CALL

This question is originally coming from Escape percent signs in given variables. I do not want to disarrange the good answer over there. But my issue changed a little bit...
Let's assume there is a given string variable enclosed by double quotes which may include one or more percent signes. It is not possible to switch to enabled delayed expansion permanently (other code is already usable). Calling a function including the string variable as a parameter is necessary. This is what I determined so far:
#echo off & setlocal ENABLEEXTENSIONS
SET AlbumArtist=%1
CALL :EscapePoisonChars %AlbumArtist% AlbumArtist_VDN
echo %AlbumArtist_VDN%
CALL :EscapePoisonChars %%AlbumArtist%% AlbumArtist_VDN
echo %AlbumArtist_VDN%
endlocal &GOTO:EOF
:EscapePoisonChars
#echo off & setlocal ENABLEEXTENSIONS
SET TmpString="%~1"
SET TmpString=%TmpString:&=^^^&%
SET TmpString=%TmpString:(=^^^(%
SET TmpString=%TmpString:)=^^^)%
endlocal&SET %2=%TmpString:~1,-1%&GOTO :EOF
I know that this is probably not a "clean solution". But I would like to understand why when the routine is invoked by CALL :EscapePoisonChars %AlbumArtist% AlbumArtist_VDN the percent sign disappears. When called with the string variable %%AlbumArtist%% enclosed by doubled percent signs it gives the wanted output:
D:\Batch>PercentTwins.bat "100% Rock & Roll"
100 Rock & Roll
100% Rock & Roll
D:\Batch>
Why there is a different result if %AlbumArtist% is expanded in- or outside the function :EscapePoisonChars? With echo on I see that the percent sign just disappears with SET TmpString="~1". Any explanations will help me to improve my further cmd techniques. Thanks!
Anyone correct me if I am wrong, but I think when single percent signs in one command are passed on to another command, they will disappear. If it is just used within the same command, it normally will not disappear.
(There is probably a more 'correct' way of saying this, or this idea might be wrong altogether)
(some info on percent signs at Microsoft Support website)
The call command initiates the % parsing phase a second time (reference the accepted answer to the question post How does the Windows Command Interpreter (CMD.EXE) parse scripts?); this lets the % signs disappear. You could double the % signs in advance, but you would need to enable delayed expansion for that, like this:
#echo off & setlocal ENABLEEXTENSIONS EnableDelayedExpansion
SET AlbumArtist=%1
set AlbumArtist=!AlbumArtist:%%=%%%%!
CALL :EscapePoisonChars %AlbumArtist% AlbumArtist_VDN
echo %AlbumArtist_VDN%
endlocal &GOTO:EOF
In your second sub-routine call, the two parsing phases do not ever see the % signs of the original string; the first phase expands %%AlbumArtist%% to %AlbumArtist% literally, the second phase expands it to 100% Rock & Roll.
But:
There is no need for all that, you do not need the sub-routine at all, when you stick to the only safe set syntax and ensure to always have the string quoted properly:
set "AlbumArtist=%~1"
%1 would return the string as given; %~1 removes potential surrounding quotation marks. Enclosing the entire assignment expression within quotation marks makes the syntax robust against poisonous characters by not considering the "" as part of the assigned string itself.
When using/returning the string (by echo as in the example here), you need to enclose the string within quotes again to maintain safety, in case you are using normal (immediate) % expansion:
set "AlbumArtist=%~1"
echo "%AlbumArtist%"
Alternatively, delayed expansion makes reading variables safe even without quotation marks:
set "AlbumArtist=%~1"
setlocal EnableDelayedExpansion
echo !AlbumArtist!
endlocal

Difference between %variable% and !variable! in batch file

I am writing a batch file where I need to output a string containing '!' to another file. But when I echo that string to another file, it removes "!" from the output.
Eg:
Input:
set LINE=Hi this is! output
echo !LINE!>>new_file.txt
Output in new_file.txt is:
Hi this is output
Also, if input is
set LINE=Hello!! this is output!!
echo !LINE!>>new_file.txt
Output in new_file.txt:
Hello
Hence, it skips the ! (Exclamation mark) from the output to the new_file.
If I use %LINE%, then it simply displays "echo is on" to the output file.
Please suggest a way to overcome this problem.
If you have delayed expansion enabled and want to output an exclamation mark, you need to escape it.
Escaping of exclamation marks needs none, one or two carets, depending on the placement.
#echo off
REM No escaping required, if delayed expansion is disabled
set test1=Test1!
setlocal EnableDelayedExpansion
REM One caret required
REM Delayed expansion uses carets independent of quotes to escape the exclamation mark
set "test2=Test2^!"
REM Two carets required
REM The first caret escapes the second caret in phase2 of the parser
REM Later in the delayed expansion phase, the remaining caret escapes the exclamation mark
set test3=Test3^^!
echo !test1!
echo !test2!
echo !test3!
The difference between !var! and %var% in blocks is explained at DOS batch: Why are my set commands resulting in nothing getting stored?
An explanation of the batch parser can be found at How does the Windows Command Interpreter (CMD.EXE) parse scripts?
It seems you have called SETLOCAL EnableDelayedExpansion somewhere higher in the code. Take a look here to see what the effects from that are.

Why does a specific windows batch parameter cause a crash?

Contents of test.bat are:
setlocal EnableExtensions EnableDelayedExpansion
set param1=%~1
echo %param1%
Can someone explain why test.bat "^^!^^^&^^^^" makes the cmd window crash but test.bat "^^^&^^^^" has an expected result of setting &^ to variable param1?
I can do test.bat "pass^^!word" and I get the expected result of pass!word.
Update: test.bat "^^!^^^^^&^^^^^^^^" works. But I'm not completely sure why. This gets interpreted to set param1=^!^^&^^^^. Why does ^ need ^^^ in front of it?
You got many problems, as the special characters will be evaluated mulitple times in your case.
First in the set command, the special character phase will reduce your string "^^!^^^&^^^^" to
^!^&^^
But as delayed expansion is enabled and your string contains an exclamation mark,
another phase will be enabled to reduce the carets again to.
!&^
At this point param1 contains !&^, you can test it with set param1
But as you try to echo the value with echo %param1% another expansion will be executed.
And now you get a problem, as %param1% will expand to !&^,
The exclamation mark will be removed, as the second exlamation mark is missing for expanding a variable,
the ampersand will be treated as new command separator and
the ending caret will be treated as multiline character.
echo ! & ^<next line>
It's much safer to use the delayed expansion here, as this never change the content, as this phase is the last one of the parser.
setlocal EnableDelayedExpansion
set param1=%~1
set param1
echo !param1!
And all these explanations can be found at How does CMD.EXE parse scripts?
It is because the escape character for the Windows shell is ^, so:
"^^!^^^^^&^^^^^^^^"
Each ^^ becomes ^
Each ^& becomes &
So finally you will get:
"^!^^&^^^^"

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)

Resources