简体   繁体   中英

How to pass a command that may contain special characters (such as % or !) inside a variable to a for /f loop?

I have a few nested loops in my code and in some point, they're divided by a call to a label like this:

@echo off
chcp 65001
for /r %%a in (*.mkv *.mp4 *.avi *.mov) do (
    echo Processing "%%~a"
    call :innerloop "%%a" "%%~fa"
)
:: Instead of goto :eof, I chose cmd /k because I want the command prompt to still be open after the script is done, not sure if this is correct though
cmd /k

:innerloop
setlocal EnableExtensions EnableDelayedExpansion
for /f "delims=" %%l in ('mkvmerge.exe -i "%~1"') do (
:: Probably this would be a safer place for setlocal, but I believe that would mean that I wouldn't get to keep a single, different !propeditcmd! per processed file
    echo Processing line "%%~l"
    for /f "tokens=1,4 delims=: " %%t in ("%%l") do (
:: This section checks for mkv attachments. There are similar checks for chapters and global tags, all of those are handled by mkvpropedit.exe
        if /i "%%t" == "Attachment" (
            if not defined attachments (
                set /a "attachments=1"
            ) else (
                set /a "attachments+=1"
            )
            if not defined propeditcmd (
                set "propeditcmd= --delete-attachment !attachments!"
            ) else (
                set "propeditcmd=!propeditcmd! --delete-attachment !attachments!"
            )
        )
    )
)
:: Since !propeditcmd! (which contains the parameters to be used with the executable) is called after all lines are processed, I figured setlocal must be before the first loop in this label
if defined propeditcmd (
    mkvpropedit.exe "%~f1" !propeditcmd!
)
endlocal
goto :eof

The script works for most files and is divided like that to allow breaking the inner loop without breaking the outer when a pass is reached. While it works for most files, I noticed it can't handle filenames containing parenthesis % in their names, likely due to EnableDelayedExtensions .

Normally, I know I would have to escape these characters with a caret ( ^ ), but I don't know how I can do it if the special characters are inside a variable ( %~1 ).

Is there a way to do it?

Update: I've been working a way to separate the section that needs delayed expansion from the one that needs it off just find in the end of my code the line mkvpropedit.exe "%~f1" !propeditcmd! , which both needs it off and on due to "%~f1" and !propeditcmd! respectively. I think this means there's no way around the question and escaping will be necessary.

Continuing my research, this answer seem to suggest this could be achieved with something like set filename="%~1:!=^^!" . Nevertheless, this doesn't seem to be the proper syntax according to SS64 . I'm also unsure if this will replace all occurrences of ! with ^! and I'm also concerned this kind of substitution could create an infinite loop and if wouldn't it be more adequate to perform this by first replacing ! with, say, ¬ before replacing it ^! .

While I intend to do testing soon to determine all of this, I'm worried I may not cover it all, so more input would definitely be appreciated.


PS: full code (88 lines) is available here if more context is needed, although I'll edit the snippet in this question as it may be requested!

There is not really a need for a subroutine. Delayed variable expansion is needed finally, but it is possible to first assign the fully qualified file name to an environment variable like FileName to avoid troubles with file names containing an exclamation mark.

The rewritten code according to the code posted in the question:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"

rem Find out if the batch file was started with a double click which means
rem with starting cmd.exe with option /C and the batch file name appended
rem as argument. In this case start one more Windows command processor
rem with the option /K and the batch file name to keep the Windows command
rem processor running after finishing the processing of this batch file
rem and exit the current command processor processing this batch file.
rem This code does nothing if the batch file is executed from within a
rem command prompt window or it was restarted with the two options /D /K.

setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
    if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
        endlocal
        start %SystemRoot%\System32\cmd.exe /D /K %0
        if not errorlevel 1 exit /B
        setlocal EnableDelayedExpansion
    )
)

rem Set the console window title to the batch file name.
title !WindowTitle!
endlocal
set "WindowTitle="

rem Get the number of the current code page and change the code page
rem to 65001 (UTF-8). The initial code page is restored at end.
for /F "tokens=*" %%G in ('%SystemRoot%\System32\chcp.com') do for %%H in (%%G) do set "CodePage=%%~nH"
%SystemRoot%\System32\chcp.com 65001 >nul 2>&1

for /R %%G in (*.mkv *.mp4 *.avi *.mov) do (
    echo(
    echo Processing "%%~G"
    set "Attachments="
    for /F "delims=" %%L in ('mkvmerge.exe -i "%%G"') do (
        rem echo Processing line "%%L"
        for /F "delims=: " %%I in ("%%L") do if /I "%%I" == "Attachment" set /A Attachments+=1
    )
    if defined Attachments (
        set "FileName=%%G"
        setlocal EnableDelayedExpansion
        set "propeditcmd=--delete-attachment 1"
        for /L %%I in (2,1,!Attachments!) do set "propeditcmd=!propeditcmd! --delete-attachment %%I"
        mkvpropedit.exe "!FileName!" !propeditcmd!
        endlocal
    )
)

rem Restore the initial code page.
%SystemRoot%\System32\chcp.com %CodePage% >nul
endlocal

See DosTips forum topic [Info] Saving current codepage , especially the post by Compo , for an explanation about getting current code page number assigned to an environment variable which is used at end of the batch file to restore the initial code page.

The most outer FOR assigns the name of the found file always with full path without surrounding " to the specified loop variable G because of using option /R . For that reason just "%%G" is used instead of "%%~G" wherever the fully qualified file name must be referenced to speed up the processing of the file names.

echo( outputs an empty line, see the DosTips forum topic ECHO. FAILS to give text or blank line - Instead use ECHO/

If an undefined environment variable like Attachments is referenced in an arithmetic expression evaluated by SET , the value 0 is used as explained by the usage help output on running set /? in a command prompt window. For that reason set /A Attachments+=1 can be used to either define the variable with 1 on first execution or increment the value of environment variable Attachments by one on all further executions for the current file.

The final value of environment variable Attachments is evaluated after processing all lines output by mkvmerge . If there are attachments, the file name is assigned to the environment variable FileName with still disabled delayed variable expansion and for that reason ! are interpreted as literal character. Next is the environment variable propeditcmd created dynamically according to the number of attachments.

I have installed neither mkvmerge.exe nor mkvpropedit , but I looked also on the referenced full code. Here is a rewritten version of your full code which I could not really test.

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"

rem Find out if the batch file was started with a double click which means
rem with starting cmd.exe with option /C and the batch file name appended
rem as argument. In this case start one more Windows command processor
rem with the option /K and the batch file name to keep the Windows command
rem processor running after finishing the processing of this batch file
rem and exit the current command processor processing this batch file.
rem This code does nothing if the batch file is executed from within a
rem command prompt window or it was restarted with the two options /D /K.

setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
    if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
        endlocal
        start %SystemRoot%\System32\cmd.exe /D /K %0
        if not errorlevel 1 exit /B
        setlocal EnableDelayedExpansion
    )
)

rem Set the console window title to the batch file name which works even
rem with an ampersand in the batch file name although that would be really
rem very unusual. Then delete the longer needed environment variable.
title !WindowTitle!
endlocal
set "WindowTitle="

rem Get the number of the current code page and change the code page
rem to 65001 for UTF-8. The initial code page is restored at the end.
for /F "tokens=*" %%G in ('%SystemRoot%\System32\chcp.com') do for %%H in (%%G) do set "CodePage=%%~nH"
%SystemRoot%\System32\chcp.com 65001 >nul 2>&1

for /R %%G in (*.mkv *.mp4 *.avi *.mov) do (
    echo(
    echo Processing "%%G"
    if /i not "%%~xG" == ".mkv" (
        mkvmerge.exe -o "%%~dpnG.mkv" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
        if errorlevel 1 (
            echo Warnings/errors generated during remuxing, original file not deleted.
            mkvmerge.exe -i --ui-language en "%%G" >> Errors.txt
            del "%%~dpnG.mkv"
        ) else (
            echo Deleting old file ...
            del /F "%%G"
        )
    ) else (
        set "FileName=%%G"
        set "AudioTracks="
        set "Attachments="
        set "Chapters="
        set "Skipfile="
        set "TagsAll="
        for /F "delims=" %%L in ('mkvmerge.exe -i "%%G"') do if not defined SkipFile (
            rem echo Processing line "%%L"
            for /F "tokens=1 delims=: " %%I in ("%%L") do (
                if /I "%%I" == "audio" (
                    set /A AudioTracks+=1
                    setlocal EnableDelayedExpansion
                    if !AudioTracks! == 2 echo !FileName!>> ExtraTracksList.txt
                    endlocal
                ) else  if /I "%%I" == "subtitles" (
                    echo ###
                    echo "%%G" has subtitles
                    mkvmerge.exe -o "%%~dpnG.nosubs%%~xG" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
                    if errorlevel 1 (
                        echo ###
                        echo Warnings/errors generated during remuxing, original file not deleted, check errors.txt
                        mkvmerge.exe -i --ui-language en "%%G" >> Errors.txt
                        del "%%~dpnG.nosubs%%~xG"
                    ) else (
                        echo Deleting old file ...
                        del /F "%%G"
                        echo Renaming new file ...
                        ren "%%~dpnG.nosubs%%~xG" "%%~nxG"
                    )
                    set "SkipFile="
                ) else if /I "%%I" == "Attachment"  (
                    set /A Attachments+=1
                ) else if /I "%%I" == "Global" (
                    set "TagsAll=--tags all:"
                ) else if /I "%%I" == "Chapters" (
                    set "Chapters=--chapters """
                )
            )
        )
        if not defined SkipFile (
            setlocal EnableDelayedExpansion
            set "propeditcmd="
            if defined Attachments (
                set "propeditcmd= --delete-attachment 1"
                for /L %%I in (2,1,!Attachments!) do set "propeditcmd=!propeditcmd! --delete-attachment %%I"
            )
            if defined TagsAll set "propeditcmd=!propeditcmd! !TagsAll!"
            if defined Chapters set "propeditcmd=!propeditcmd! !Chapters!"
            if defined propeditcmd (
                echo ###
                echo !FileName! has extras
                mkvpropedit.exe "!FileName!"!propeditcmd!
            )
            endlocal
        )
    )
)

rem Restore the initial code page.
%SystemRoot%\System32\chcp.com %CodePage% >nul
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.

  • chcp /?
  • cmd /?
  • del /?
  • echo /?
  • endlocal /?
  • exit /?
  • for /?
  • if /?
  • rem /?
  • set /?
  • setlocal /?
  • start /?
  • title /?

See also: Issue 7: Usage of letters ADFNPSTXZadfnpstxz as loop variable

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM