The curious case of the missing exclamation mark in CMD files - windows

I have whittled down a more complex CMD script to the essentials. It reads an input file line by line, unquotes it (if quoted) and writes it out to another CMD file.
The problem is that if the input file contains exclamation marks (! or bang) the character gets stripped out somewhere along the line.
Here is the CMD script, BANG1.CMD:
#echo off
setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
if exist bang2.cmd del bang2.cmd
for /f "tokens=*" %%a in (bang1.txt) do call :doit1 %%a
exit /b
:doit1
set P1=%1
if %P1%. EQU . exit /b
call :unquotex P1 %P1%
echo>>bang2.cmd echo P1:[%P1%]
exit /b
:unquotex
set X=%2
set Q=%X:~0,1%
if "!Q!" EQU ^""" SET X=!X:~1,-1!
set %1=%X%
exit /b
Here is the input file BANG1.TXT:
HelloWorld
"Hello World"
Hello!World
"Hello!World"
The resulting file BANG2.CMD ends up containing this:
echo P1:[HelloWorld]
echo P1:[Hello World]
echo P1:[HelloWorld]
echo P1:[HelloWorld]
The question is, what happened to the embedded bangs? I have tried with and without ENABLEDELAYEDEXPANSION. I have even tried escaping (^) the bangs in the input file, still with no luck.
Is there any way to preserve them?
Thanks.

The problem at all is delayed expansion here.
With delayed expansion, exclamation marks are used to expand variables, but when there is only one exclamation mark in a line it will be removed.
Specially in FOR /F loops delayed expansion is tricky to handle, as the expansion of the FOR parameter is directly affected by the delayed expansion. The only solution is to disable it temporarily.
The next problem is the CALL, you can't transfer content with CALL (without destroying it).
It's better to transfer the variable by reference (only the variable name) and then get the content in the called function.
The last problem in your code are the percent expansions, do not use them
when delayed expansion is enabled, as the delayed expansion is evaluated after the percent expansion an expanded line will be expanded a second time by the delayed expansion.
Sample.
Assume the content of var is Bang!
echo %var% expands to Bang! but then the delayed expansion will evaluate Bang! to Bang.
With echo !var! you simply get Bang!
#echo off
setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
if exist bang2.cmd del bang2.cmd
for /f "tokens=*" %%a in (bang1.txt) do (
setlocal DisableDelayedExpansion
set "line=%%a"
setlocal EnableDelayedExpansion
call :doit1 line
endlocal
endlocal
)
exit /b
:doit1
set "P1=!%1!"
if "!P1!" EQU "" exit /b
call :unquotex P1
echo>>bang2.cmd echo P1:[!P1!]
exit /b
:unquotex
set "param=!%~1!"
if "!param:~0,1!" == ^""" (
set "param=!param:~1,-1!"
)
set "%1=!param!"
exit /b

Like this :
#echo off
(for /f "delims=" %%a in ('type bang1.txt') do echo echo P1:[%%~a])>bang2.cmd

Try this:
#echo off
if exist bang2.cmd del bang2.cmd
for /f "tokens=*" %%a in (bang1.txt) do call :doit1 %%a
exit /b
:doit1
set "P1=%1"
if %P1%.==. exit /b
call :unquotex P1 %P1%
echo>>bang2.cmd echo P1:[%P1%]
exit /b
:unquotex
set "%1=%~2"
exit /b
Using parameters, you can get the version without quotes using %~1 instead of %1. If %1 contains "hello world" for example, then %~1 contains hello world. This allows for an easier unquoting mechanism, removing the need for delayed expansion.

Related

Win Batch: Help defining variable in a FOR loop

I made this script that finds all directories and echoes the directorie's name to a .txt file. The script is working but it ends up echoing only %A without any value. My script is below!
set /a count=0
setlocal EnableDelayedExtensions
FOR /D %%A in ("*") DO (call :sub)
endlocal
pause
exit
:sub
(echo [DIR] %%A)>>%count%.txt
set /a count+=1
The output in the .txt files is [DIR] %A.
Any idea how to fixe this? Thanks -David
First remark, you are using an invalid option for setlocal but that is probably just a typo.
The problem is that you are try to use a for-parameter where it cannot be used.
The rule is "A for-parameter can only be used within the command or command block () of a for loop"
Your subroutine is not within the command block of a for loop, but you can start a dummy for loop in the subroutine which will give you access to all available for-parameters.
set /a count=0
setlocal EnableDelayedExpansion
FOR /D %%A in ("*") DO (call :sub)
endlocal
pause
exit/b
:sub
For %%. in (.) do (echo [DIR] %%A)>>%count%.txt
set /a count+=1
You need to pass the parameter to the subroutine.
From https://www.informit.com/articles/article.aspx?p=1154761&seqNum=11 :
for %%f in (*.dat) do call :onefile %%f
exit /b
:onefile
echo Processing file %1...
echo ... commands go here ...
exit /b
As you've already enabled delayed expansion, there's no need to use a Call to a label, just do it within the loop.
#Echo Off
SetLocal EnableExtensions EnableDelayedExpansion
Set "count=0"
For /D %%G In (*) Do (
Set /A count += 1
(Echo [DIR] %%G) 1>"!count!.txt"
)
On this version, I've started at 1 instead of 0 for the first text file name, if you really want to start at 0, change line 3 to Set "count=-1"

Keep variable after endlocal in Batch

I have a folder structure, which is like for example C:\Temp\ and there are a lot of folder and file, and within each folder there are a "callme.bat". I would like to create a so called main.bat which is one after another call the callme files within the main' window. But there is a problem, within the callme files are some echo which contains "!" mark what make a problem for me.
I realized the problem with the setlocal-endlocal combo, because the batch scrip wants to interpret the message within the "!" marks, so I must use endlocal, but if I did I not able to run the callme bats.
callme.bat
#echo off
echo !!! hidden message !!! not hidden message
pause
main.bat variant 1
#echo off
setlocal enabledelayedexpansion
set PATH=C:\Temp
for /F %%x in ('dir /B/A:D %PATH%') do (
set CURR_DIR=%PATH%\%%x
set ACTUAL_BATCH=!CURR_DIR!\callme.bat
echo !ACTUAL_BATCH!
call !ACTUAL_BATCH!
pause
)
pause
exit
main.bat variant 2
#echo off
set PATH=C:\Temp
for /F %%x in ('dir /B/A:D %PATH%') do (
setlocal enabledelayedexpansion
set CURR_DIR=%PATH%\%%x
set ACTUAL_BATCH=!CURR_DIR!\callme.bat
echo !ACTUAL_BATCH!
ENDLOCAL & SET VAR=!ACTUAL_BATCH!
echo %VAR%
pause
)
pause
exit
main.bat variant 3
#echo off
set PATH=C:\Temp
for /F %%x in ('dir /B/A:D %PATH%') do (
setlocal enabledelayedexpansion
set CURR_DIR=%PATH%\%%x
set ACTUAL_BATCH=!CURR_DIR!\callme.bat
echo !ACTUAL_BATCH!
REM source: https://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal
for /f "delims=" %%A in (""!ACTUAL_BATCH!"") do endlocal & set "VAR=%%~A"
echo %VAR%
call %VAR%
pause
)
pause
exit
So I don't know what to do. Anyone has an idea?
variant 1's output:
C:\Temp\1\callme.bat
not hidden message
C:\Temp\2\callme.bat
not hidden message
variant 2-3's output:
C:\Temp\1\callme.bat
ECHO is off.
C:\Temp\2\callme.bat
ECHO is off.
TL;DR
ENDLOCAL&set "varname=%sourcevarname%"
probably, where varname is the variablename to set and sourcevarname is the variable whose value is to be assigned to varname - and they CAN be the same name, even if the statement appears logically null - it's exporting the variable from within the setlocal/endlocal block.
Key point: MUST be on one physical line and may be repeated if necessary (ie
ENDLOCAL&set "varname=%sourcevarname%"&set "varname2=%sourcevarname2%"
So
ENDLOCAL&set "fred=%fred%"&set "bill=%george%"
is perfectly valid, to set the value of fred outside the setlocal/endlocal bracket to its final value inside and of billoutside to the final value of george inside.
Some points about your code:
Never use PATH as a variable name, as it destroys the PATH variable for searching executable files.
Use the extended SET syntax set "varname=content" to avoid problems with trainling spaces.
You only need to disable the delayed expansion mode by using setlocal DisableDelayedExpansion
#echo off
setlocal EnableDelayedExpansion
set MY_PATH=C:\Temp
for /F %%x in ('dir /B/A:D %PATH%') do (
set "CURR_DIR=%MY_PATH%\%%x"
set "ACTUAL_BATCH=!CURR_DIR!\callme.bat"
call :execute ACTUAL_BATCH
pause
)
pause
exit /b
:execute ACTUAL_BATCH
set "batFile=!%~1!"
echo Calling !batFile!
setlocal DisableDelayedExpansion
call %batFile%
endlocal
exit /b

Variable substring Edit/Replace inside a for loop

I want to replace a variable's substring, previously stored on a variable inside a for loop, I tried to do it like this but it didn't work:
setlocal EnableDelayedExpansion
set checkVar=abcd
FOR %%Y IN (*.pdf) DO (
SET meu=%%Y
CALL SET meuWithoutChar=!meu:%%%checkVar%%%=!
ECHO meuWithoutChar=!meuWithoutChar!
)
For example here if %%Y==blepabcdnnnn.pdf; I want to have meuWithoutChar=blepnnnn.pdf on the output
Thank you in advance
You are bit confused on the concept of delayed expansion and the use of CALL to get an extra phase of expansion. Here are the examples. I am just using your single file example. You can change it back to using the wildcard.
CALL example
#echo off
set checkVar=abcd
FOR %%Y IN (blepabcdnnnn.pdf) DO (
SET "meu=%%Y"
CALL SET "meuWithoutChar=%%meu:%checkVar%=%%"
CALL ECHO meuWithoutChar=%%meuWithoutChar%%
)
pause
Delayed Expansion
#echo off
setlocal Enabledelayedexpansion
set checkVar=abcd
FOR %%Y IN (blepabcdnnnn.pdf) DO (
SET "meu=%%Y"
SET "meuWithoutChar=!meu:%checkVar%=!"
ECHO meuWithoutChar=!meuWithoutChar!
)
pause
As a supplement/extension to Squashmans answer.Only cycles through necessary files and ignores the file's extension.
Without delayed expansion:
#Echo Off
SetLocal DisableDelayedExpansion
Set "strChr=abcd"
For %%A In ("*%strChr%*.pdf") Do (Set "objFileName=%%~nA"
Call Set "objNewFile=%%objFileName:%strChr%=%%%%~xA"
Call Echo %%%%objNewFile%%%%=%%objNewFile%%)
Pause
With full script delayed expansion, (will have issues with filenames containing !'s):
#Echo Off
SetLocal EnableDelayedExpansion
Set "strChr=abcd"
For %%A In ("*%strChr%*.pdf") Do (Set "objFileName=%%~nA"
Set "objNewFile=!objFileName:%strChr%=!%%~xA"
Echo %%objNewFile%%=!objNewFile!)
Pause
With toggled delayed expansion, (protects filenames containing !'s):
#Echo Off
SetLocal DisableDelayedExpansion
Set "strChr=abcd"
For %%A In ("*%strChr%*.pdf") Do (Set "objFileName=%%~nA"
SetLocal EnableDelayedExpansion
Set "objNewFile=!objFileName:%strChr%=!%%~xA"
Echo %%objNewFile%%=!objNewFile!
EndLocal)
Pause

Escape PIPE character in a function call windows Batch script

I am writing a function to execute shell commands and capture its output in a batch script.
:runShellCmd
setlocal EnableDelayedExpansion
SET lf=-
FOR /F "delims=" %%i IN ('%~1') DO if "%out%" == "" (set out=%%i) else (set out=!out!%lf%%%i)
echo "Cmd output: %out%"
SET "funOut=%out%"
ENDLOCAL & IF "%~1" NEQ "" SET %~2=%out%
goto :EOF
I have been successful in passing simple commands and getting output. But for calls like
CALL :runShellCmd "echo Jatin Kumar | find /c /i "jatin"" it fails with error unexpected | character.
I know we need to escape | with ^ in for but if I try to pass ^| in the function argument string, it changes it to ^^| which again throws error.
Am I missing something?
This is an effect of the CALL command.
The CALL command doubles all carets in one of the batch parser phases.
Normally you wouldn't see this, as the carets will be used as an escape charater directly after the doubling.
See this
call echo ^^^^
call call echo ^^^^
call call call echo ^^^^
call echo "^^^^"
call call echo "^^^^"
call call call echo "^^^^"
Output
^^
^^
^^
"^^^^^^^^"
"^^^^^^^^^^^^^^^^"
"^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"
But how do you can escape your pipe character?
You can't!
But you can add a caret remover in your function.
:runShellCmd
setlocal EnableDelayedExpansion
set "param=%~1"
set param
set "param=!param:^^=^!"
for .... ('!param!')
Or you could use an escaping trick when calling your function.
set "caret=^"
CALL :runShellCmd "echo Jatin Kumar %%caret%%| find /c /i "
This works, as the %%caret%% will be expanded after the CALL caret doubling phase.
#echo off
setlocal enableextensions disabledelayedexpansion
call :test "echo(this|find "t""
exit /b
:test
set "x=%~1"
for /f "delims=" %%f in ('%x:|=^|%') do echo [%%f]
I think i'm missing something, because this works for me.
EDITED - This should be a more general solution. Not bulletproof but a skeleton.
#echo off
setlocal enableextensions disabledelayedexpansion
call :test "(echo(this&echo(that)|find "t" 2>nul|sort&echo end"
exit /b
:test
set "x=%~1"
set "x=%x:|=^|%"
set "x=%x:>=^>%"
set "x=%x:<=^<%"
set "x=%x:&=^&%"
set "x=%x:)=^)%"
for /f "delims=" %%f in ('%x%') do echo [%%f]

Escaping ampersands in Windows batch files

I realise that you can escape ampersands in batch files using the hat character
e.g.
echo a ^& b
a & b
But I'm using the command
for /f "tokens=*" %%A IN ('DIR /B /A-D /S .acl') DO ProcessACL.cmd "%%A"
which is finding all the files named '.acl' in the current directory, or a subdirectory of the current directory.
The problem is, I'm finding path names that include the '&' character (and no, they can't be renamed), and I need a way of automatically escaping the ampersands and calling the second batch file with the escaped path as the parameter.
rem ProcessACL.cmd
echo %1
The problem is not the escaping, it seems to be in the second script.
If there is a line like
echo %1
Then it is expands and fails:
echo You & me.acl
Better to use delayed expansion like
setlocal EnableDelayedExpansion
set "var=%~1"
echo !var!
To avoid also problems with exclamation points ! in the parameter, the first set should be used in a DisableDelayedExpansion context.
setlocal DisableDelayedExpansion
set "var=%~1"
setlocal EnableDelayedExpansion
echo !var!
Your for line should be (note the *.acl)
for /f "tokens=*" %%A IN ('DIR /B /A-D /S *.acl') DO ProcessACL.cmd "%%A"
ProcessACL.cmd can access the path passed to it with %1.
// ProcessACL.cmd
ECHO %1
Whatever is contained by the variable %1 is fully contained. There is no need for escapes. Escapes are for the batch processor to interpret the characters it is parsing.

Resources