Cannot parse file names with ! in them but double quoted? - windows

I've probably pushed myself into a corner here but for some reason this batch will handle pretty much any name except one with a ! in it. I cannot figure out why for the life of me what I've done wrong.
If the file has a ! in the name it ignores it and creates a new one with no ! which gets moved into the batch directory.
Actually every instance of that file name ends up missing the ! even the quote in the output MGL:
setlocal enabledelayedexpansion
set choice=
set /p choice= Type selection and press enter:
if '%choice%'=='1' set console=SNES
if '%choice%'=='1' set settings=delay="2" type="f" index="0"
if '%choice%'=='2' set console=PSX
if '%choice%'=='2' set settings=delay="1" type="s" index="0"
if '%choice%'=='3' set console=gameboy
if '%choice%'=='3' set settings=delay="1" type="f" index="1"
if '%choice%'=='4' set console=C64
if '%choice%'=='4' set settings=delay="1" type="f" index="1"
mkdir MGL_!console! > nul 2>&1
for /R %%a in (*.32x,*.a26,*.a78,*.abs,*.bin,*.bs,*.chd,*.cof,*.col,*.fds,*.gb,*.gbc,*.gba,*.gg,*.j64,*.jag,*.lnx,*.md,*.neo,*.nes,*.o,*.pce,*.rom,*.sc,*.sfc,*.sg,*.smc,*.smd,*.sms,"*.vec",*.wsc,*.ws) do (
set "filepath=%%a"
set "filepath=!filepath:%CD%=!"
set "filepath=!filepath:\=/!"
set "filepath=!filepath:~1!"
echo ^<mistergamedescription^> > "%%~pna.mgl"
echo ^<rbf^>_console/!console!^</rbf^> >> "%%~pna.mgl"
echo ^<file !settings! path="!filepath!"/^> >> "%%~pna.mgl"
echo ^</mistergamedescription^> >> "%%~pna.mgl"
>nul move "%%~pna.mgl" "MGL_%console%\%%~na.mgl"
)
The output seems fine the whole way though?
echo </mistergamedescription> 1>>"\mISTER\Done\Vectrex\1 World - A-Z\Blitz! - Action Football (USA, Europe) (0F11CE0C).mgl"
move "\mISTER\Done\Vectrex\1 World - A-Z\Blitz! - Action Football (USA, Europe) (0F11CE0C).mgl" "MGL_!console!\Blitz! - Action Football (USA, Europe) (0F11CE0C).mgl" 1>nul
)
The system cannot find the file specified.

I believe the issue is caused by the fact that delayed expansion is enabled during expansion of %%a, because there is an exclamation mark in the affected path (\Blitz! - *), which becomes lost due to delayed expansion. Therefore, you need to toggle delayed expansion, so it is only enabled where actually needed. Here is an improved approach, also featuring several other improvements:
rem // Initially disable delayed expansion:
setlocal DisableDelayedExpansion
:USER_PROMPT
rem // Use quoted `set` syntax in general:
set "choice="
set /P choice="Type select and press enter: "
rem /* Use quotation marks rather than apostrophes (single quotes);
rem then combine multiple duplicate conditions using the `&` operator;
rem unwanted trailing spaces are prevented by the quoted `set` syntax: */
if "%choice%"=="1" set "console=SNES" & set settings=delay="2" type="f" index="0"
if "%choice%"=="2" set "console=PSX" & set settings=delay="1" type="s" index="0"
if "%choice%"=="3" set "console=gameboy" & set settings=delay="1" type="f" index="1"
if "%choice%"=="4" set "console=C64" & set settings=delay="1" type="f" index="1"
rem // Retry if user entered something else:
goto :USER_PROMPT
::rem /* Alternative user prompt approach -- the `choice` command;
::rem the `ErrorLevel` variable reflects the POSITION of the choices;
::rem it is just coincidental here that its value equals the choice: */
::choice /C 1234 /M "Type selection: " /N
::if %ErrorLevel% equ 1 set "console=SNES" & set settings=delay="2" type="f" index="0"
::if %ErrorLevel% equ 2 set "console=PSX" & set settings=delay="1" type="s" index="0"
::if %ErrorLevel% equ 3 set "console=gameboy" & set settings=delay="1" type="f" index="1"
::if %ErrorLevel% equ 4 set "console=C64" & set settings=delay="1" type="f" index="1"
::goto :EOF
mkdir "MGL_%console%" > nul 2>&1
for /R %%a in (*.32x,*.a26,*.a78,*.abs,*.bin,*.bs,*.chd,*.cof,*.col,*.fds,*.gb,*.gbc,*.gba,*.gg,*.j64,*.jag,*.lnx,*.md,*.neo,*.nes,*.o,*.pce,*.rom,*.sc,*.sfc,*.sg,*.smc,*.smd,*.sms,"*.vec",*.wsc,*.ws) do (
rem // Delayed expansion is still disabled at this point, so it does not interfere with expansion of `%%a`:
set "filepath=%%~a"
rem // Immediately set target file, hence no more move is necessary later:
set "targetfile=MGL_%console%\%%~na.mgl"
rem // Now toggle delayed expansion:
setlocal EnableDelayedExpansion
set "filepath=!filepath:*%CD%\=!"
::rem // Improved variant to remove root path, avoiding immediate (`%`-)expansion:
::for /F "delims=" %%b in ("!CD!") do set "filepath=!filepath:*%%b\=!"
set "filepath=!filepath:\=/!"
rem // Redirect to the target file only once (also avoiding trailing spaces):
> "!targetfile!" (
echo ^<mistergamedescription^>
echo ^<rbf^>_console/!console!^</rbf^>
echo ^<file !settings! path="!filepath!"/^>
echo ^</mistergamedescription^>
)
endlocal
)
endlocal
Anyway, I still do not get if you really want to overwrite the *.mgl file in every for /R loop iteration.

The problem with file names containing one or more ! is caused by enabled delayed expansion on execution of the command line set "filepath=%%a". This command line is parsed a second time because of enabled delayed expansion resulting in interpreting all exclamation marks in file name assigned to the loop variable a as beginning/end of a delayed expanded variable reference. Therefore a single exclamation mark is removed from the file name string before assigning the remaining string to the environment variable filepath.
Here is the code rewritten to avoid this and several other possible problems.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
cls
echo/
echo Select a system to create MGL files for:
echo/
echo 1. SNES
echo 2. PSX
echo 3. Gameboy
echo 4. C64
echo/
:Console0
%SystemRoot%\System32\choice.exe /C 1234 /N /M "Please make your selection:"
goto Console%ErrorLevel%
:Console1
set "console=SNES"
set "settings=delay="2" type="f" index="0""
goto CreateFolder
:Console2
set "console=PSX"
set "settings=delay="1" type="s" index="0""
goto CreateFolder
:Console3
set "console=gameboy"
set "settings=delay="1" type="f" index="1""
goto CreateFolder
:Console4
set "console=C64"
set "settings=delay="1" type="f" index="1""
:CreateFolder
mkdir "MLG_%console%" 2>nul
if exist "MLG_%console%\" goto GetBasePathLength
for %%I in (".\MLG_%console%") do echo ERROR: Failed to create the directory: "%%~fI"
echo/
#setlocal EnableExtensions EnableDelayedExpansion & for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do #endlocal & if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" pause
goto EndBatch
:GetBasePathLength
set "BasePath=_%CD%"
if not "%BasePath:~-1%" == "\" set "BasePath=%BasePath%\"
setlocal EnableDelayedExpansion
set "BasePathLength=0"
for /L %%I in (12,-1,0) do (
set /A "BasePathLength|=1<<%%I"
for %%J in (!BasePathLength!) do if "!BasePath:~%%J,1!" == "" set /A "BasePathLength&=~1<<%%I"
)
endlocal & set "BasePathLength=%BasePathLength%"
for /R %%I in (*.32x,*.a26,*.a78,*.abs,*.bin,*.bs,*.chd,*.cof,*.col,*.fds,*.gb,*.gbc,*.gba,*.gg,*.j64,*.jag,*.lnx,*.md,*.neo,*.nes,*.o,*.pce,*.rom,*.sc,*.sfc,*.sg,*.smc,*.smd,*.sms,"*.vec",*.wsc,*.ws) do (
set "FileName=%%I"
set "OutputFile=MLG_%console%\%%~nI.mgl"
setlocal EnableDelayedExpansion
set "FileName=!FileName:~%BasePathLength%!"
set "FileName=!FileName:\=/!"
(
echo ^<mistergamedescription^>
echo ^<rbf^>_console/!console!^</rbf^>
echo ^<file !settings! path="!FileName!"/^>
echo ^</mistergamedescription^>
)>"!OutputFile!"
endlocal
)
:EndBatch
endlocal
I recommend to read first:
How to stop Windows command interpreter from quitting batch file execution on an incorrect user input? It explains the reasons for the usage of command CHOICE instead of set /P.
Why is no string output with 'echo %var%' after using 'set var = text' on command line?
It describes the reasons for using the syntax set "variable=value" for the definition of an environment variable.
DosTips forum topic: ECHO. FAILS to give text or blank line - Instead use ECHO/
DosTips string function strLen of which code is used in the batch file in an adapted form to get the length of the base folder path to later remove that path from each fully qualified file name.
The strange looking FOR command line with command pause at end is for running PAUSE only if the Windows Command Processor instance processing the batch file was started with first argument being case-insensitive /C as done on double clicking on the batch file. PAUSE is not executed if there is first opened a command prompt window and next executed the batch file from within the command prompt window and the error occurs on creation of output directory in current directory as in this case the error message can be read in the command prompt window without using pause.
The main FOR loop assigns first the fully qualified file name to the environment variable FileName while delayed expansion is disabled to handle correct also file names with one or more question marks in its path/name.
The output file name with path relative to current directory is assigned also to the environment variable OutputFile while disabled delayed expansion to work also for a file name like Blitz! - Action Football (USA, Europe) (0F11CE0C).mgl.
Next delayed expansion is enabled which causes additional actions in the background as described in this answer with all the details about the commands SETLOCAL and ENDLOCAL.
Then the base path is removed from the file name and all \ are replaced by / using delayed expansion.
Next the output file is created and opened with writing the four lines output with ECHO using delayed expansion into the file. Finally the output file is closed and the previous execution environment is restored using the command ENDLOCAL.
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.
choice /?
cls /?
echo /?
endlocal /?
for /?
goto /?
if /?
mkdir /?
pause /?
set /?
setlocal /?

Related

How to process pathnames with ! within an for /F loop?

In a complex batch file I want to read in files with paths, among other things, to read them into a variable one after the other separated by spaces.
This works with the following code so far quite well - but only if the path does not contain an exclamation mark.
Even using the setlocal command (enabledelayedexpansion / disabledelayedexpansion) I did not succeed in processing exclamation marks.
Does anyone here have a clever idea to the problem?
The following example batch creates a text file in the current directory and then reads it in a for /F loop.
At the end all three paths from the text file should be in the variable %Output%. But with the exclamation mark.
#echo off
setlocal enabledelayedexpansion
echo This is an example^^! > "textfile.txt"
echo This is a second example^^! >> "textfile.txt"
echo And this line have an ^^! exclamation mark in the middle >> "textfile.txt"
for /F "usebackq tokens=* delims=" %%a in (textfile.txt) do (
set "Record=%%a"
set "Output=!Output!!Record! - "
)
)
echo %Output%
echo !Output!
endlocal
The Output is like this:
This is an example - This is a second example - And this line have an exclamation mark in the middle
But should be like this:
This is an example! - This is a second example! - And this line have an ! exclamation mark in the middle
It is advisable not using delayed variable expansion on processing files and directories, lines in a text file, strings not defined by the batch file itself, or output captured from the execution of a program or a command line. If it is for some reasons necessary to make use of delayed variable expansion inside a FOR loop, there should be first assigned the file/directory name, the line, or the string to process to an environment variable while delayed expansion is disabled and then enable delayed expansion temporary inside the FOR loop.
Here is a batch file demo which can be simply run from within a command prompt window or by double clicking on the batch file. It creates several files for demonstration in the directory for temporary files, but deletes them all before exiting.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
echo This is an example!> "%TEMP%\TextFile.tmp"
echo This is a second example!>> "%TEMP%\TextFile.tmp"
echo And this line has an exclamation mark ! in the middle.>> "%TEMP%\TextFile.tmp"
set "Output="
(for /F usebackq^ delims^=^ eol^= %%I in ("%TEMP%\TextFile.tmp") do set "Line=%%I" & call :ConcatenateLines) & goto ContinueDemo
:ConcatenateLines
set "Output=%Output% - %Line%" & goto :EOF
:ContinueDemo
cls
echo/
echo All lines concatenated are:
echo/
echo %Output:~3%
set "Output="
del "%TEMP%\TextFile.tmp"
echo File with name ".Linux hidden file!">"%TEMP%\.Linux hidden file!"
echo File with name "A simple test!">"%TEMP%\A simple test!"
echo File with name " 100%% Development & 'Test' (!).tmp">"%TEMP%\ 100%% Development & 'Test(!)'.tmp"
echo/
echo Files with ! are:
echo/
for /F "eol=| tokens=* delims=" %%I in ('dir "%TEMP%\*!*" /A-D /B /ON 2^>nul') do (
set "NameFile=%%I"
set "FileName=%%~nI"
set "FileExtension=%%~xI"
set "FullName=%TEMP%\%%I"
setlocal EnableDelayedExpansion
if defined FileName (
if defined FileExtension (
echo File with ext. !FileExtension:~1!: !NameFile!
) else (
echo Extensionless file: !NameFile!
)
) else echo Extensionless file: !NameFile!
del "!FullName!"
endlocal
)
endlocal
echo/
#setlocal EnableExtensions EnableDelayedExpansion & for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do #endlocal & if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" set /P "=Press any key to exit the demo . . . "<nul & pause >nul
The output of this batch file is:
All lines concatenated are:
This is an example! - This is a second example! - And this line has an exclamation mark ! in the middle.
Files with ! are:
File with ext. tmp: 100% Development & 'Test(!)'.tmp
Extensionless file: .Linux hidden file!
Extensionless file: A simple test!
The text file example with concatenating lines makes use of a subroutine called from within the FOR loop processing the lines in the text file. The syntax used here is for maximum performance by getting the subroutine as near as possible to the FOR command line. That is important if the FOR loop has to process hundreds or even thousands of items.
The example processing file names enables and disables delayed expansion inside the FOR loop after having assigned all parts of the currently processed file to environment variables. It could be useful to reduce the list of environment variables before processing thousands of files for a better performance on using this method.
Another method is shown in Magoo´s answer using the command CALL to get a command line with referenced environment variables (re)defined inside the loop parsed a second time. I used that method also in the past quite often, but don't that anymore as it is not fail-safe and not efficient. call set results in searching by cmd.exe in current directory and next in all directories of environment variable PATH for a file with name set and a file extension of environment variable PATHEXT. So it results in lots of file system accesses in the background on each iteration of the FOR loop and if there is by chance a file set.exe, set.bat, set.cmd, etc. found by cmd.exe somewhere, the batch file does not work anymore as expected because of running the executable or calling the batch file instead of the (re)definition of the environment variable.
The following answers written by me could be also helpful:
How to read and print contents of text file line by line?
It explains in full details how to process all lines of a text file.
How to pass environment variables as parameters by reference to another batch file?
It explains in full details what the commands SETLOCAL and ENDLOCAL do.
How to pass a command that may contain special characters (such as % or !) inside a variable to a for /f loop?
This is an example of a batch file designed to process video files with any valid file name on any Windows computer very efficient, safe and secure with full explanation.
Well, the main trick is to enable delayed expansion only when it is actually needed and to disable it otherwise. Since you are accumulating multiple strings in a single variable inside of a loop, it becomes a bit more difficult, because you should have delayed expansion disabled during expansion of for meta-variables (like %%a), but enabled when joining the string, leading to setlocal and endlocal statements inside of the loop. The major purpose of these commands is environment localisation, hence any variable changes become lost past endlocal, so a method of tansfering the value beyond endlocal is required, which is incorporated in the following code:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem /* At this point delayed expansion is disabled, so there is no need to
rem escape exclamation marks; moreover a redirected block prevents
rem superfluous file close/reopen operations, and there is no more
rem trailing space written to the file (as in your original approach): */
> "textfile.txt" (
echo This is an example!
echo This is a second example!
echo And this line have an ! exclamation mark in the middle
)
rem // Let us initialise the output variable:
set "Output= - "
rem // Using `usebackq` only makes sense when you want to quote a file path:
for /F "usebackq tokens=* delims=" %%a in ("textfile.txt") do (
rem // Remember that delayed expansion is still disabled at this point:
set "Record=%%a"
rem // For concatenation we need delayed expansion to be enabled:
setlocal EnableDelayedExpansion
set "Output=!Output!!Record! - "
rem /* We need to terminate the environment localisation of `setlocal`
rem inside of the loop, but we would lose any changes in `Output`;
rem therefore let us (mis-)use `for /F`, which is iterated once: */
for /F "delims=" %%b in ("!Output!") do endlocal & set "Output=%%b"
rem /* An often used method to transfer a variable beyond `endlocal` is
rem the line `endlocal & set "Output=%Output%`, but this only works
rem outside of a parenthesised block because of percent expansion. */
)
rem /* Echo out text with delayed expansion enabled is the only safe way;
rem surrounding separators ` - ` are going to be removed; since `Output`
rem was initialised with something non-empty, we do not even need to skip
rem sub-string expansion for the problematic case of an empty string: */
setlocal EnableDelayedExpansion
echo(!Output:~3,-3!
endlocal
endlocal
exit /B
Pew. I finally got it to work.
It works via a workaround using a second text file.
Not pretty, not performant, but it works and is sufficient for my purposes.
#Magoo, thanks for your post.
This is my solution:
#echo off
setlocal enableextensions enabledelayedexpansion
echo This is an example^^!> "textfile.txt"
echo This is a second example^^!>> "textfile.txt"
echo And this line have an ^^! exclamation mark in the middle>> "textfile.txt"
echo.
echo Content of the textfile:
type "textfile.txt"
set output=
del "textfile2.txt" 1> nul 2>&1
setlocal disabledelayedexpansion
for /f "usebackq tokens=* delims=" %%a IN ("textfile.txt") do (
rem Write each line without a newline character into a new text file
echo|set /p "dummy=%%a, ">>"textfile2.txt"
)
endlocal
rem Loading the content of the new text file into the variable
set /p output=<"textfile2.txt"
del "textfile2.txt" 1> nul 2>&1
echo.
echo --------------------------------------------
echo Content of the variable:
set out
endlocal
The output looks like this:
Content of the textfile:
This is an example!
This is a second example!
And this line have an ! exclamation mark in the middle
--------------------------------------------
Content of the variable:
output=This is an example!, This is a second example!, And this line have an ! exclamation mark in the middle,
It's delayedexpansion mode that appears to raise this problem.
#ECHO OFF
setlocal enabledelayedexpansion
echo This is an example^^^! > "textfile.txt"
echo This is a second example^^^! >> "textfile.txt"
echo And this line have an ^^^! exclamation mark in the middle >> "textfile.txt"
TYPE "textfile.txt"
SETLOCAL disabledelayedexpansion
for /F "usebackq tokens=* delims=" %%a in (textfile.txt) do (
set "Record=%%a"
CALL set "Output2=%%Output2%%%%record%% - "
CALL set "Output=%%Output%%%%a - "
SET out
)
)
endlocal&SET "output=%output%"
echo %Output%
echo !Output!
SET out
I've no doubt that with delayedexpansion off, there would be the same problem with %. Just special characters, I suppose.
Note that with endlocal&SET "output=%output%", the set is executed in delayedexpansion mode.

Enable/Disable Delayed Expansion in FOR /f loop (batch)

I am still struggling to properly understand the behaviour of Disable/EnableDelayedExpansion...
I want to parse input arguments when calling something like command -a -b -c file such to finally have options=-a -b -c and filename=file.
To do so I use the FOR /f loop:
set "count=0"
set "opts="
set "fl="
set tmpv=
:argloop
for /f tokens^=1^,^*^ delims^= %%a in ("%1") do (
echo.
echo Chosen option is %1
set /a count+=1
echo.
echo Reading %count% is %%a..
set "tmpv=%%a"
rem setlocal enabledelayedexpansion
echo Tmp is %tmpv% after set equal %%variable.
rem endlocal
rem setlocal disabledelayedexpansion
set "tmpv=%tmpv:-=%"
rem setlocal enabledelayedexpansion
echo After removing it writes !tmpv!
rem endlocal
rem setlocal disabledelayedexpansion
if "%tmpv%"=="%%a" (
echo Input does not contain "-"
set "fl=%tmpv%"
echo %fl%
) else (
echo/Options before are %opts%
echo.
if "%opts%"=="" (
echo Options are empty.
set opts=%%a
) else (
set "opts=%opts% %%a"
)
)
if not "%2"=="" (shift & goto:argloop)
)
echo.
echo Finally options are %opts%
set opts=%opts:-=/%
echo Finally options are %opts%
echo File name %fl%
set tmpv=
set count=
goto:end
Output writes:
Chosen option is -a
Reading 1 is -a..
Tmp is after set equal %variable.
After removing it writes
Options before are
Options are empty.
Chosen option is -b
Reading 2 is -b..
Tmp is -= after set equal %variable.
After removing it writes -=
Options before are -a
Chosen option is -c
Reading 3 is -c..
Tmp is = after set equal %variable.
After removing it writes =
Options before are -a -b
Chosen option is flfl
Reading 4 is flfl..
Tmp is = after set equal %variable.
After removing it writes =
Options before are -a -b -c
Finally options are -a -b -c flfl
Finally options are /a /b /c flfl
File name
I had made it working with EnableDelayedExpansion, but not capable of storing final %fl% variable.
But why does it not work this way (without using delayed expansions)??
I will sincerely appreciate whom will try to clarify it in all extents.
The rules really aren't too hard.
You are aware that %var% is resolved to the value of var.
When a loop is parsed, every %var% within that loop is replaced by the THEN-current value of var. This includes pseudovariables like %cd%, %errorlevel% and %random%.
If delayedexpansion is in effect (and it is "in effect" from the setlocal enabledelayedexpansion instruction [start-of-setlocal-bracket] until an endlocal or end-of-file [end-of-setlocal-bracket] is reached) then !var! is resolved to the contents of var at the time that particular instruction (set, echo, etc) is executed.
If delayedexpansion is NOT in effect then !var! is simply that - the literal string !var!.
And one small kink. Any change made to the environment (addition, deletion or variation of variable values) is discarded when a setlocal bracket ends.
So, in all probability, you could display the difference by echoing %var% alongside !var!
and %%x (a metavariable) is always resolved to its current value, regardless of setlocal status.
[After responses]
Since all setlocal enabledelayedexpansion/endlocal brackets are remmed-out in the published code, I'm not surprised at the results.
However, running the published code does not yield the published results - for me, the response was "Reading 0"..."Reading 3".
So, looking at the for loop, I believe it's equivalent to
for /f "tokens=1,* delims=" %%a in ("%1") do (
which in turn is the same as
for /f "delims=" %%a in ("%1") do (
since there are no delimiters, an this is effectively the same as
for %%a in (%1) do (
which does nothing beyond assigning %1 to %%a and making the entire loop one block statement.
So therefore, this code should do the same job - without the kinkiness afforded by setlocal...
#ECHO OFF
SETLOCAL
set "count=0"
set "opts="
set "fl="
set "tmpv="
:argloopn
SET "arg=%1"
IF NOT DEFINED arg GOTO report
SET /a count+=1
ECHO arg %count% is %1 IN variable ARG = "%arg%"
SET "tmpv=%arg%"
rem remove "-"
set "tmpv=%tmpv:-=%"
IF "%tmpv%"=="%arg%" (
echo Input does not contain "-"
set "fl=%tmpv%"
) ELSE (
ECHO Input contains "-" so is option
SET "opts=%opts% %arg%"
)
SHIFT
GOTO argloopn
:report
rem fix-up options since 1st char, if it exists must be a space as [space]newoption is added each time
IF DEFINED options SET "options=%options:~1%"
echo Finally options are %opts%
set opts=%opts:-=/%
echo Finally options are %opts%
echo File name %fl%
echo.
GOTO :EOF
I've assumed that a proper batch-debug environment has been established; hence goto :eof to terminate the batch and an inintial setlocal to preserve the original environment.
When you use the point-click-and-giggle method of executing a batch, the batch window will often close if a syntax-error is found. You should instead open a 'command prompt' and run your batch from there so that the window remains open and any error message will be displayed.
--- BUT ---
In testing, I tried this:
#ECHO OFF
:parasite
setlocal
set "opts="
:loopp
FOR /f "delims=" %%a IN ("%1") DO (
SET "opts=%opts% %%a"
SETLOCAL ENABLEDELAYEDEXPANSION
ECHO OPTS was "%opts%" is now "!opts!"
ENDLOCAL
ECHO %%opts%% is "%opts%" and !opts! is "!opts!"
)
SHIFT
IF "%1"=="" GOTO :EOF
GOTO loopp
Which didn't do what I expected it to do - that is, report
%opts% is " -a -b -c file" and !opts! is "!opts!"
Instead, it reported
%opts% is " -a -b -c file" and -a -b -c file is " -a -b -c file"
Which I find puzzling as the !var! is outside the setlocal enabledelayedexpansion/endlocal command-bracket and hence should not have been replaced, in my view.
Seems like an #jeb problem to me... so I'll see whether he's got an explanation...

Windows batch script "The system cannot find the file specified." with exclamation in filenames?

I am trying to write a batch script that does the following:
When a folder is drag-and-dropped onto the batch script, it processes every file in that folder.
I am running into a problem with a certain filenames that contain exclamation marks. e.g.:
!.txt or !!!.txt
For now, I am simply trying to rename the file to demonstrate the issue:
#echo off
SetLocal EnableDelayedExpansion
set folder=%~1
set count=0
for /r "%folder%" %%G in (*) do (
set fullpath=%%G
set fileExtension=%%~xG
call :processFile
)
goto end
:processFile
echo "fullpath = %fullpath%"
echo "fileExtension = %fileExtension%"
rename "%fullpath%" "temporary_filename_500%fileExtension%"
set /a count+=1
echo.
goto :eof
:end
echo "%count% files processed."
pause
It gives me the error "The system cannot find the file specified." However, it works if I change the filename to something simple like "test.webm" How can I make the script more robust?
I don't see that you are using somewhere delayed expansion. So, either disable it with setlocal DisableDelayedExpansion in the start of your batch file or just remove it by removing line setlocal EnableDelayedExpansion.
However, if you want to keep it, do:
#echo off
SetLocal EnableDelayedExpansion
rem Code above (^^) if exists.
Setlocal DisableDelayedExpansion
set "folder=%~1"
set "count=0"
for /R "%folder%" %%G in (*) do (
set "fullpath=%%~fG"
set "fileExtension=%%~xG"
call :processFile
)
goto end
:processFile
echo "fullpath = %fullpath%"
echo "fileExtension = %fileExtension%"
ren "%fullpath%" "temporary_filename_500%fileExtension%"
set /a "count+=1"
echo/
goto :eof
:end
echo "%count% files processed."
pause
setlocal EnableDelayedExpansion
rem Your code below with active delayed expansion:
Note that:
You should always quote the variable name and the value in the set command like set "var=value" and in set /a like set /a "var+=1", etc.; see set /? for more information.
To find the full path of a file/folder for sure in a for loop, use the f modifier, like %%~fG.
Mentioned by Mofi here: don't use echo.; use echo/ for better practice.
See also the Phase 5 (Delayed Expansion) of this answer about how batch files are interpreted.

usebackq without delayedexpansion

The following creates a numbered list of links from a remote directory like
1 Link!1
2 Link!2
3 Link!3
4 Link!4
5 Link!5
.
#echo off
setlocal ENABLEDELAYEDEXPANSION
megals --reload /Root/
set /p var1="enter folder name: " & megals /Root/var1
set /a c=0
FOR /F "tokens=1 usebackq" %%i in (`megals -n -e /Root/%%var1%%`) do (
set /a c=c+1
echo !c! %%i
set string[!c!]=%%i
)
set /P number=Enter number:
echo !string[%number%]!
pause
First Problem: All the links contain a ! character which gets removed by delayedexpansion, rendering the link useless. The links require the ! as it is part of the link.
Second Problem: I'm trying to integrate this into a program, and I can't use findstr because it will list the link and filename on the same line, and when the filenames contain parentheses the program crashes. So I have to use usebackq because it lets me get just the link, without needing to deal with the filenames.
Findstr will list Link!1 Filename (the whole line)
Usebackq lets me just get Link!1
I can't use Findstr because when filenames contain parentheses the program will crash, which can only be solved by delayedexpansion.
This is a follow-up post from here, which I got stuck on: (Shows the Program)
https://stackoverflow.com/questions/49564553/create-a-numbered-list-based-on-a-given-list-of-strings#=
You can see the findstr method there, and how it causes crashes when filenames contain parentheses, which can be fixed with delayedexpansion, but that removes the ! character which is essential as it is part of the link.
Edit: Seems to be working now, thanks
Working Code
#echo off
:start:
megals --reload /Root/
set /p var1="dir? " & megals /Root/%%var1%%
for /f "tokens=1,* delims=:" %%A in ('megals -n /Root/%%var1%% ^|findstr
/n "." ') do (
set Link[%%A]=%%B
Echo %%A %%B
)
setlocal DisABLEDELAYEDEXPANSION
set /a c=0
FOR /F "tokens=1 usebackq" %%i in (`megals -n -e /Root/%%var1%%`) do (
set /a c+=1
call set "string[%%c%%]=%%i"
)
set /P number="Enter number: "
FOR /F "tokens=*" %%g IN ('call echo %%string[%number%]%%') do (SET VAR2=%%g)
echo %Var2%
echo.
Megadl %VAR2% & echo. && goto :start:
pause
https://megatools.megous.com/man/megals.html#_megatools
You really should double quote your whole set commands.
Using the alternate delayed expansion type with a call:
#echo off
setlocal DisABLEDELAYEDEXPANSION
megals --reload /Root/
set /p var1="enter folder name: " & megals /Root/var1
set /a c=0
FOR /F "tokens=1 usebackq" %%i in (`megals -n -e /Root/%%var1%%`) do (
set /a c+=1
call echo %%c%% %%i
call set "string[%%c%%]=%%i"
)
set /P number=Enter number:
call echo %%string[%number%]%%
pause
The simple solution is not using delayed environment variable expansion, for example by using command CALL.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
megals.exe --reload /Root/
rem Prompt user for folder name and verify that the user has really entered
rem a folder name and remove double quotes to prevent an exit of batch file
rem execution on further processing because of an invalid command line syntax.
:EnterFolder
set "FolderName="
set /P "FolderName=Enter folder name: "
rem Has the user entered anything at all?
if not defined FolderName goto EnterFolder
rem Remove all double quotes from folder name string?
set "FolderName=%FolderName:"=%"
rem Is anything left from folder name after removing double quotes?
if not defined FolderName goto EnterFolder
megals.exe "/Root/%FolderName%"
rem Get first space/tab separated string of each line output by megals
rem assigned to an environment variable which form an array of strings.
rem Redefine end of line character from semicolon to vertical bar as it
rem is impossible that a line starts with a vertical bar. Command CALL
rem is used to double process the SET command line by Windows command
rem processor which is the alternate solution for delayed expansion.
echo/
set "Count=0"
for /F "eol=|" %%I in ('megals.exe -n -e "/Root/%FolderName%"') do (
set /A Count+=1
call echo %%Count%% %%~I
call set "string[%%Count%%]=%%~I"
)
if %Count% == 0 echo There is nothing! & goto EndBatch
echo/
rem Prompt user for number name and verify that the user has really entered
rem a decimal interpreted number which must be in range of 1-%Count%.
:EnterNumber
set "Number="
set /P "Number=Enter number (1-%Count%): "
rem Has the user entered anything at all?
if not defined Number goto EnterNumber
rem Remove all double quotes from number string?
set "Number=%Number:"=%"
rem Is anything left from number string?
if not defined Number goto EnterNumber
rem Contains the number string any non digit character?
for /F "delims=0123456789" %%I in ("%Number%") do goto EnterNumber
rem Remove all leading zeros from number string to avoid interpreting
rem the entered number as octal number on the two IF comparisons below.
:LeadingZeros
if not %Number:~0,1% == 0 goto CheckNumber
set "Number=%Number:~1%"
if defined Number goto LeadingZeros
rem The number was 0 which is less than 1.
goto EnterNumber
rem Check number in range of 1 to %Count% which requires to convert the
rem number strings on both sides of the comparison operators to signed
rem 32-bit integers by Windows command processor in background.
:CheckNumber
if %Number% GTR %Count% goto EnterNumber
if %Number% LSS 1 goto EnterNumber
rem Output the string according to entered number by forcing Windows command
rem processor again to double processing the command line because of CALL.
call echo %%string[%number%]%%
rem Restore previous environment with discarding all the
rem environment variables defined by this batch file.
:EndBatch
endlocal
pause
Another solution is using a subroutine like OutputAndSet.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
megals.exe --reload /Root/
rem Prompt user for folder name and verify that the user has really entered
rem a folder name and remove double quotes to prevent an exit of batch file
rem execution on further processing because of an invalid command line syntax.
:EnterFolder
set "FolderName="
set /P "FolderName=Enter folder name: "
rem Has the user entered anything at all?
if not defined FolderName goto EnterFolder
rem Remove all double quotes from folder name string?
set "FolderName=%FolderName:"=%"
rem Is anything left from folder name after removing double quotes?
if not defined FolderName goto EnterFolder
megals.exe "/Root/%FolderName%"
rem Get first space/tab separated string of each line output by megals
rem assigned to an environment variable which form an array of strings.
rem Redefine end of line character from semicolon to vertical bar as it
rem is impossible that a line starts with a vertical bar. Command CALL
rem is used to double process the SET command line by Windows command
rem processor which is the alternate solution for delayed expansion.
echo/
set "Count=0"
for /F "eol=|" %%I in ('megals.exe -n -e "/Root/%FolderName%"') do call :OutputAndSet "%%~I"
if %Count% == 0 echo There is nothing! & goto EndBatch
echo/
rem Prompt user for number name and verify that the user has really entered
rem a decimal interpreted number which must be in range of 1-%Count%.
:EnterNumber
set "Number="
set /P "Number=Enter number (1-%Count%): "
rem Has the user entered anything at all?
if not defined Number goto EnterNumber
rem Remove all double quotes from number string?
set "Number=%Number:"=%"
rem Is anything left from number string?
if not defined Number goto EnterNumber
rem Contains the number string any non digit character?
for /F "delims=0123456789" %%I in ("%Number%") do goto EnterNumber
rem Remove all leading zeros from number string to avoid interpreting
rem the entered number as octal number on the two IF comparisons below.
:LeadingZeros
if not %Number:~0,1% == 0 goto CheckNumber
set "Number=%Number:~1%"
if defined Number goto LeadingZeros
rem The number was 0 which is less than 1.
goto EnterNumber
:OutputAndSet
set /A Count+=1
echo %Count% %~1
set "string[%Count%]=%~1"
goto :EOF
rem Check number in range of 1 to %Count% which requires to convert the
rem number strings on both sides of the comparison operators to signed
rem 32-bit integers by Windows command processor in background.
:CheckNumber
if %Number% GTR %Count% goto EnterNumber
if %Number% LSS 1 goto EnterNumber
rem Output the string according to entered number by forcing Windows command
rem processor again to double processing the command line because of CALL.
call echo %%string[%number%]%%
rem Restore previous environment with discarding all the
rem environment variables defined by this batch file.
:EndBatch
endlocal
pause
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.
call /?
echo /?
endlocal /?
for /?
goto /?
if /?
pause /?
rem /?
set /?
setlocal /?
See also:
How does the Windows Command Interpreter (CMD.EXE) parse scripts?
Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files
How to set environment variables with spaces?
Where does GOTO :EOF return to?

Batch File: List Directory & File names to individual variables and display as selection menu

I use RDP on many different windows machines, and sometimes have to RDP into one, then rdp from there onto another.
I'd like to know if it is possible to create a Batch File that can read the Names of all Directories within a set path, then display them as numbered variables like a menu.
After i input my selection, it would do the same for all .rdp files in the selected directory.
Below is an example of how i could manually hardcode it for each file...but I need something that will adapt to dropping an new rdp file into a directory rather than having to manually add it each time inside the batch file, as the number of sites/pcs and names can change regularly.
:site
ECHO Location List
ECHO.
ECHO 1 NSW
ECHO 2 QLD
ECHO.
SET /p site=Enter Selection:
IF "%site%"=="1" GOTO NSW
IF "%site%"=="2" GOTO QLD
:NSW
SET dirname=C:\Machine\NSW\
ECHO Machine List
ECHO.
ECHO 1 Client01.rdp
ECHO 2 Server01.rdp
ECHO 3 Server02.rdp
ECHO.
SET /p machine0=Enter Selection:
IF "%machine0%"=="1" SET machine1=%dirname%Client01.rdp
IF "%machine0%"=="2" SET machine1=%dirname%Server01.rdp
IF "%machine0%"=="3" SET machine1=%dirname%Server02.rdp
GOTO connection
:connection
mstsc %machine1% /console
I've found several questions similar to this (such as here and here) but they all seem to be about just displaying a list and not getting them into a menu like option, also i still do not fully understand how the FOR command works.
Example of the directory structure.
C:\Batchfile.bat
C:\Machines\NSW\Client01.rdp
C:\Machines\NSW\Server01.rdp
C:\Machines\NSW\Server02.rdp
C:\Machines\QLD\Client01.rdp
C:\Machines\QLD\Client02.rdp
C:\Machines\QLD\Server01.rdp
The base directory would be set to C:\Machines then batch would store each sub-directory name to a numbered variable and echo them to the screen and prompt for selection.
Location List
1 NSW
2 QLD
Enter Selection:_
If user input 1 then then it would store each .RDP filename inside the QLD sub-directory to a numbered variable and echo them to the screen and prompt for selection.
Machine List for NSW
1 Client01.rdp
2 Server01.rdp
3 Server02.rdp
Enter Selection:_
After the user makes a selection at this point i would like to use the selected .rdp file with the mstsc command to launch an rdp session to the selected computer then loop back the beginning to allow to open a second connection at the same time.
I'd appreciate any help you could give.
#ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /f "delims==" %%i IN ('SET s_ 2^>nul') DO SET "%%i="
SET "sourcedir=c:\sourcedir"
FOR /f "delims=" %%a IN ('dir/s/b/a-d "%sourcedir%\*.rdp"') DO (
SET s_=%%~dpa
FOR /f %%b IN ("!s_:~0,-1!") DO SET s_#%%~nb=Y&SET s_#%%~nb_%%~na=Y
)
CALL :showmenu "Location List" #
IF /i "%s_%"=="q" GOTO :EOF
SET s_site=%s_%
CALL :showmenu "Machine List for %s_site%" # %s_site%
IF /i "%s_%"=="q" GOTO :EOF
SET s_machine=%s_%
ECHO(==============
ECHO site=%s_site% machine=%s_machine%
GOTO :EOF
:showmenu
SET s_items=1
CLS
ECHO(%~1
ECHO(
FOR /f "tokens=2,3delims=_%2=" %%i IN ('set s_%2') DO (
IF "%3"=="" (
CALL :showline %%i
) ELSE (
IF "%3"=="%%i" CALL :showline %%j
)
)
ECHO(
SET "s_="
SET /p "s_=Enter Selection : "
IF NOT DEFINED s_ SET s_=Q
IF /i "%s_%"=="q" GOTO :EOF
IF DEFINED s_%s_% CALL SET s_=%%s_%s_%%%&GOTO :EOF
GOTO showmenu
:showline
SET "s_= %s_items%. "
ECHO %s_:~-4%%1
SET s_%s_items%=%1
SET /a s_items+=1
SET "s_%s_items%="
GOTO :eof
This way is self-adjusting. Unfortunately it also uses a few hieroglyphics...
The first step is to ensure that all variables with names starting s_ are removed from the environment. There's no special significance to s_ - it's just what I chose. The output of set s_ will be of the form s_whatever=something if an s_... variable exists. If none exist, the 2>nul suppresses the error message, but the > needs to be escaped by a caret (^) to tell cmd that the redirection is part of the command to be executed, not the for command. If s_whatever=something then the for /f will parse that line using = as a delimiter, hence assigning s_whatever to the nominated metavariable, %%i.
The next step is to find all of the .RDP filenames starting at the source directory. dir /s/b/a-d produces bare lines (no headers or footers) of the full filenames, suppressing any directorynames that happen to match the specified mask.
The entire filename is assigned to %%a because delims="" that is, there are no delimiters. S_ is used as a general-purpose variable and is assigned the drive and path parts of the filename in %%a. The last character of s_ is then removed (it will be a \) and the for /f %%b interprets the resultant string as though it was a filename. The variables s_#site and s_#site_machine are then set (to Y, but they could have been set to anything)
Note the use of !s_:~0,-1! which specifies to take character 0..last-1 of the run-time value of s_ - the ! specifies run-time value when SETLOCAL ENABLEDELAYEDEXPANSION is active.
The remainder of the main routine simply calls SHOWMENU twice with various parameters and assigns the value returned in s_ to the appropriate variable.
SHOWMENU sets the number of items (s_items) to 1 more than the count of items available, clears the screen and shows the menu title from the first subroutine parameter (%1) - but with the quotes suppressed (%~1) - which allows the parameter to contain spaces.
The following FOR/F tokenises the SET list for s_# (the SITE names) or s_# (the SITE+MACHINE names). Using delimiters of _ and = as well as the # or # means that for a line like s_#NSW=Y will assign NSW to %%i and a line like s_#NSW_Server01=Y assigns NSW to %%i and Server01 to %%j
The appropriate part is selected and passed to the SHOWLINE routine.
s_ is then used for user input. Forcing it to be cleared means that if the user presses just ENTER then it will remain unset - otherwise it would retain its original value.
I've arbitrarily assigned an input of Q to quit, and if there is no user input, then that quits, too. Otherwise, if the variable s_choicemade is set, then s_ is set to that value and reaching EOF returns from the subroutine. If an invalid choice is made, then s_invalidchoice will NOT be set, so the menu is re-displayed.
The SHOWLINE procedure sets s_ to spacethelinenumber.space and then the paraeeter is displayed (%1) preceded by the last 4 characters of that string. This means that if the item number exceeds 9 then the leading space wil be lopped off and the dots will be aligned. The item number is then incremented ready for the next choice.
Here is one way:
#echo off
setlocal enabledelayedexpansion
:Start
ECHO Location List
ECHO.
ECHO NSW
ECHO QLD
ECHO.
SET /p site=Enter Selection:
for /f %%a in ('dir /b/s "c:\Temp\%site%\*.rdp"') do (
set /a i+=1
echo !i! - %%~nxa
set mach[!i!]=%%~nxa
)
set /p m0=Enter Selection:
echo mstsc !mach[%m0%]! /console
set /p sel=Would you like to launch another [y/n]?
if /i "%sel%" EQU "y" Goto :start
Or
#echo off
setlocal enabledelayedexpansion
:Start
ECHO Location List
ECHO.
ECHO 1 - NSW
ECHO 2 - QLD
ECHO.
SET /p site=Enter Selection:
set i=0 & set a=0
set site[1]=NSW
set site[2]=QLD
for /f %%a in ('dir /b/s "c:\Temp\!site[%site%]!\*.rdp"') do (
set /a i+=1
echo !i! - %%~nxa
set mach[!i!]=%%~nxa
)
set /p m0=Enter Selection:
echo mstsc !mach[%m0%]! /console
set /p sel=Would you like to launch another [y/n]?
if /i "%sel%" EQU "y" Goto :start
Also, this is assuming all of your servers are 2003. Starting with 2008, I believe /console has been deprecated in lieu of /admin. If that's the case, it's easy enough to add a little more logic depending on which version of Server you're connecting to.

Resources