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

88 Views Asked by At

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.
2

There are 2 best solutions below

2
On

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.

0
On

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:

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.

To understand the commands used and how they work, open a command prompt window, execute there the following commands, and read the displayed help pages for each command, entirely and carefully.

  • choice /?
  • cls /?
  • echo /?
  • endlocal /?
  • for /?
  • goto /?
  • if /?
  • mkdir /?
  • pause /?
  • set /?
  • setlocal /?