Escaping ampersands in Windows batch files - windows

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.

Related

Batch renaming files incrementally with special characters

I am trying to rename the episodes in a directory in an incremental way, but there are exclamation marks in some of the episodes. It will skip those files. I tried doing delayed expansion, but it didn't work.
#echo off
setlocal DisableDelayedExpansion
set /a num=0
for %%a in (*.mkv) do (
set filename=%%a
setlocal EnableDelayedExpansion
ren "!filename!" "Soul Eater Episode 0!num!.mkv"
set /a num=!num!+1
)
pause
endlocal
Try this:
#echo off
setlocal EnableDelayedExpansion
set /a num=0
for %%a in (*.mkv) do (
set filename=%%a
ren "!filename!" "Soul Eater Episode 0!num!.mkv"
set /a num=!num!+1
)
endlocal
pause
The following batch file code could be used to rename the files containing one or more ! in file name.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "num=0"
for /F "eol=| delims=" %%I in ('dir *.mkv /A-D /B /ON 2^>nul ^| %SystemRoot%\System32\findstr.exe /B /I /L /V /C:"Soul Eater Episode"') do (
set "filename=%%I"
setlocal EnableDelayedExpansion
ren "!filename!" "Soul Eater Episode 0!num!.mkv"
endlocal
set /A num+=1
)
pause
endlocal
There is no need to use an arithmetic expression to define the environment variable num with the value 0.
It is very advisable on running renames on a list of file names in a directory using a wildcard pattern like *.mkv to get first the list of file names loaded into memory of Windows command processor and then rename one file after the other as done by this code using a for /F loop. Otherwise the result of the file renames is unpredictable as depending on file system (NTFS, FAT32, exFAT) and current names of the files matched by the wildcard pattern.
The additional FINDSTR is used to filter out all file names beginning already case-insensitive with the string Soul Eater Episode although the batch file would most likely fail to rename some files if there are already files with a file name matched by Soul Eater Episode 0*.mkv in the current directory on execution of the batch file.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul and |. The redirection operators > and | must be escaped with caret character ^ on FOR command line to be interpreted as literal characters when Windows command interpreter processes this command line before executing command FOR which executes the embedded command line with dir and findstr with using a separate command process started in background with %ComSpec% /c and the specified command line appended as additional arguments.
The file name is first assigned as output by DIR filtered by FINDSTR to the environment variable filename with delayed expansion disabled as otherwise the double processing of this command line on enabled delayed expansion would result in interpreting ! in file name assigned to loop variable I as beginning/end of a delayed expanded environment variable reference.
Then delayed expansion is enabled to be able to do the rename with referencing the environment variable num using delayed expansion and of course also the file name assigned to environment variable filename.
Next delayed expansion is disabled again before an arithmetic expression is used using the preferred syntax to increment the value of an environment variable by one which always works independent on disabled or enabled delayed expansion.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
dir /?
echo /?
endlocal /?
findstr /?
for /?
pause /?
ren /?
set /?
setlocal /?
Please read this answer with details on what happens in background on every execution of SETLOCAL and ENDLOCAL.

%%I in windows batch doesn't manage name with spaces

In windows 10, I need to make a batch with a loop on the subfolder names in a folder, I did the following, but the problem is the %%I doesn't manage the folder name with spaces, it takes only the first part:
#echo off
FOR /F %%I IN ('dir /b C:\Users\Thomas\Music') DO (
ECHO %%I)
If the folder "Music" contains the folder "My music", then echo %%I will print only "My".
FOR /F "delims=" %%I IN ('dir /b /ad C:\Users\Thomas\Music') DO (
... and use "%%I" where you want to use the name-containing-spaces (ie. quote the constructed string) - a principle that applies wherever batch uses strings containing separators like Space
The /ad selects directorynames instead of filenames.
Adding a further switch, /s will scan the entire subdirectory-tree.
Assignment of string values to variables is best done with
set "var=%variablefrom%"
or in the case of a metavariable (eg the loop-control variable %%I in your code) you need
set "var=%%I"
BUT you should investigate the topic of delayed expansion (many items here) if you want to use the value of the variable assigned (var) within the loop.
My best-practice concept shown in a commented .bat script:
#ECHO OFF
SETLOCAL EnableExtensions DisableDelayedExpansion
FOR /F "delims=" %%I IN ('dir /b /AD "%UserProfile%\Music" 2^>NUL') DO (
rem process a FOR-loop variable directly
ECHO For_variable "%%~I"
rem set a FOR-loop variable as an environment variable value
set "_myVar=%%~I" see tilde which would strip double quotes
rem process an environment variable INside a FOR-loop body
SETLOCAL EnableDelayedExpansion
echo Env_variable "!_myVar!"
ENDLOCAL
rem process an environment variable OUTside a FOR-loop body
call :ProcessMyVar
)
ENDLOCAL
goto :eof
:ProcessMyVar
rem process an environment variable OUTside a FOR-loop body
echo Env_var_Call "%_myVar%"
goto :eof
Output shows that even names with cmd-poisonous characters like percent or exclamation (etc. etc.) are processed properly:
==> tree "%UserProfile%\Music"
Folder PATH listing
Volume serial number is 0000005F F2DF:F89D
C:\USERS\USER\MUSIC
├───100% My Tunes!
└───My Songs!
==> D:\bat\SO\39697872.bat
For_variable "100% My Tunes!"
Env_variable "100% My Tunes!"
Env_var_Call "100% My Tunes!"
For_variable "My Songs!"
Env_variable "My Songs!"
Env_var_Call "My Songs!"
==>
Resources (required reading, incomplete):
(command reference) An A-Z Index of the Windows CMD command line
(helpful particularities) Windows CMD Shell Command Line Syntax
(%I, %~I etc. special page) Command Line arguments (Parameters)
(special page) EnableDelayedExpansion
(>, & etc. special page) Redirection
(%% doubled percent sign, ^ caret, double quotes) Escape Characters, Delimiters and Quotes

The curious case of the missing exclamation mark in CMD files

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.

How to handle space of Filename in Batch For Loop

#echo off
echo processing please wait...
setlocal enabledelayedexpansion
set txtfile=%~dp0mysql\my.ini.bak
set newfile=%~dp0mysql\my.ini
if exist "%newfile%" del /f /q "%newfile%"
for /f "tokens=*" %%a in (%txtfile%) do (
set newline=%%a
echo !newline! >> %newfile%
)
Now my.ini.bak file is in D:\Program Files\my.ini.bak
Error : The system cannot find the file Files\mysql\my.ini.bak.
How to make this code work, so it copy each line from my.ini.bak to my.ini
The space in the path is indeed preventing FOR /F from opening the file successfully. You need quotes around the file name, but then you also need the FOR /F "USEBACKQ" option so that the quoted name is treated as a file name instead of a text string.
Using "TOKENS=*" is almost, but not quite the same as "DELIMS="
"DELIMS=" preserves the entire line
"TOKENS=*" preserves the remainder of the line after first stripping any leading spaces and/or tabs
I generally prefer "DELIMS=" unless I have a reason to strip leading spaces.
If there is a chance that the .INI file can contain ! character then you will want to toggle delayed expansion on and off within the loop. The value of %%a will be corrupted if it contains ! and delayed expansion is enabled.
It is more efficient to enclose the entire FOR loop within another set of parens and redirect the output just once instead of once for each iteration. It also eliminates the need to first delete the file if it already exists.
#echo off
echo processing please wait...
setlocal disableDelayedExpansion
set "txtfile=%~dp0mysql\my.ini.bak"
set "newfile=%~dp0mysql\my.ini"
>"%newfile%" (
for /f "usebackq delims=" %%a in ("%txtfile%") do (
set newline=%%a
setlocal enableDelayedExpansion
rem Presumably more processing goes here
echo !newline!
endlocal
)
)
I don't think you need to quote the paths, you can just use the short names in the expanded path. So use %~dps instead of %~dp
#echo off
echo processing please wait...
setlocal enabledelayedexpansion
set txtfile=%~dps0mysql\my.ini.bak
set newfile=%~dps0mysql\my.ini
if exist "%newfile%" del /f /q "%newfile%"
for /f "tokens=*" %%a in (%txtfile%) do (
set newline=%%a
echo !newline! >> %newfile%
)
By the way, the error I get with your original code is slightly different from your error, not sure why.
The system cannot find the file C:\Program.
The problem is the space in the file paths. I can't test this right now, but believe quoting the paths will fix it:
set txtfile="%~dp0mysql\my.ini.bak"
set newfile="%~dp0mysql\my.ini"
Here is the full code I tested with. For testing I added a space in "my sql" and created a folder by this name.
#echo off
echo processing please wait...
setlocal enabledelayedexpansion
set txtfile="%~dp0my sql\my.ini.bak"
echo %txtfile%
set newfile="%~dp0my sql\my.ini"
echo %newfile%
if exist "%newfile%" del /f /q "%newfile%"
for /f "tokens=*" %%a in (%txtfile%) do (
set newline=%%a
echo !newline! >> %newfile%
)

How do I get the equivalent of dirname() in a batch file?

I'd like to get the parent directory of a file from within a .bat file. So, given a variable set to "C:\MyDir\MyFile.txt", I'd like to get "C:\MyDir". In other words, the equivalent of dirname() functionality in a typical UNIX environment. Is this possible?
for %%F in (%filename%) do set dirname=%%~dpF
This will set %dirname% to the drive and directory of the file name stored in %filename%.
Careful with filenames containing spaces, though. Either they have to be set with surrounding quotes:
set filename="C:\MyDir\MyFile with space.txt"
or you have to put the quotes around the argument in the for loop:
for %%F in ("%filename%") do set dirname=%%~dpF
Either method will work, both at the same time won't :-)
If for whatever reason you can't use FOR (no Command Extensions etc) you might be able to get away with the ..\ hack:
set file=c:\dir\file.txt
set dir=%file%\..\
The problem with the for loop is that it leaves the trailing \ at the end of the string. This causes problems if you want to get the dirname multiple times. Perhaps you need to get the name of the directory that is the grandparent of the directory containing the file instead of just the parent directory. Simply using the for loop technique a second time will remove the \, and will not get the grandparent directory.
That is you cannot simply do the following.
set filename=c:\1\2\3\t.txt
for %%F in ("%filename%") do set dirname=%%~dpF
for %%F in ("%dirname%") do set dirname=%%~dpF
This will set dirname to "c:\1\2\3", not "c:\1\2".
The following function solves that problem by also removing the trailing \.
:dirname file varName
setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET _dir=%~dp1
SET _dir=%_dir:~0,-1%
endlocal & set %2=%_dir%
GOTO :EOF
It is called as follows.
set filename=c:\1\2\3\t.txt
call :dirname "%filename%" _dirname
call :dirname "%_dirname%" _dirname
To get rid of the trailing \ just pass the result %~dpA+. into the next for and get the full path %~fB:
C:\> #for /f "delims=" %A in ("c:\1\2\3\t.txt") do #for /f "delims=" %B in ("%~dpA.") do #echo %~fB
c:\1\2\3
C:\> #for /f "delims=" %A in ("c:\1\2\3\t 1.txt") do #for /f "delims=" %B in ("%~dpA.") do #echo %~fB
c:\1\2\3
C:\> #for /f "delims=" %A in ("c:\1\2\3 3\t 1.txt") do #for /f "delims=" %B in ("%~dpA.") do #echo %~fB
c:\1\2\3 3

Resources