Home > other >  How to process a multi-line command inside of a for /f loop?
How to process a multi-line command inside of a for /f loop?

Time:01-08

Context

I'm trying to develop a script that unifies my currently working scripts to parse some video files. The first one is very simple, just makes sure all video files are on mkv containers. The next few each call mkvmerge -i to check the file for subtitles, tags, attachments and other undesired extras to strip (which is done sometimes using find, sometimes using findstr, for the RegEx).

This is what the output of mkvmerge -i looks like:

File 'test.mkv': container: Matroska
Track ID 0: video (AVC/H.264/MPEG-4p10)
Track ID 1: audio (Opus)
Track ID 2: subtitles (SubRip/SRT)
Attachment ID 1: type 'image/jpeg', size 30184 bytes, file name 'test.jpg'
Attachment ID 2: type 'image/jpeg', size 30184 bytes, file name 'test2.jpg'
Attachment ID 3: type 'image/jpeg', size 30184 bytes, file name 'test3.jpg'
Chapters: 9 entries

Currently, I just pipe it to find or findstr and search for words such as "subtitles", "bytes" or : [0-9].

My goal is, for each processed file, to have mkvmerge -i called just once per file instead of once per script it goes through. This is an old project I'm picking up again and my previous try can be seen in this question.

The question

Following this answer, I managed to append the output of mkvmerge -i to a variable (%mkvmergeinfo%) and now all I have to do is to pipe this variable a few times and have the rest of the code act accordingly. This is what it looks like currently:

for /f "usebackq delims= eol=$" %%c in (`echo !mkvmergeinfo! ^| find /c /i "subtitles"`) do (
    if [%%c]==[0] (
        ...

If I change echo !mkvmergeinfo! to mkvmerge -i, the rest of the code works correctly, but as now I'm trying to pass a multiline command, when I add echo %%c after do, it just displays the first line of echo !mkvmergeinfo! (which I already checked and it does contain all the lines).

I tried to solve this replacing the whole command inside the parenthesis to a call to a label, which would do the echo and pipe, but that didn't work.

Is there another way around the issue without just writing the output to a file and instead parsing that file?

CodePudding user response:

A for /F command with a set to process the output of a command line results in starting in background one more Windows command processor with %SystemRoot%\System32\cmd.exe /c and the command line appended as additional arguments. Therefore delayed variable expansion is not enabled for this command process running in background as it would be necessary for echo !mkvmergeinfo!. It would be necessary to run the background cmd.exe with option /V:ON left to option /c and the command line to execute. But that is not possible, except running two additional cmd.exe, the first one with /c and a command line which starts with %ComSpec% /D /V:ON /C ... a second cmd.exe as suggested by aschipfl in his comment above.

I suggest to run mkvmerge.exe -i once for each *.mkv file and process the output using just internal commands of cmd.exe like FOR and IF without using find.exe and findstr.exe at all.

Example:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
for %%G in (*.mkv) do (
    echo/
    echo Processing file "%%G" ...
    for /F "delims=" %%H in ('mkvmerge.exe -i "%%G"') do (
        echo/
        echo Processing info line: %%H
        for /F "tokens=1,3,4* delims=:( " %%I in ("%%H") do (
            if "%%I" == "Track" (
                for /F "delims=)" %%M in ("%%L") do (
                    if "%%K" == "video" (
                        echo ... Track %%J is a video track with video codec %%M
                    ) else if "%%K" == "audio" (
                        echo ... Track %%J is an audio track with audio codec %%M
                    ) else if "%%K" == "subtitles" (
                        echo ... Track %%J is a subtitles track with subtitles codec %%M
                    )
                )
            ) else if "%%I" == "Attachment" (
                echo ... Attachment %%J is %%K %%L
            )
        )
    )
)
endlocal

How each line output by mkvmerge.exe -i "%%G" is processed further is up to you. This is just a demonstration which results in following output for the posted information of file test.mkv.

Processing file "test.mkv" ...

Processing info line: File 'test.mkv': container: Matroska

Processing info line: Track ID 0: video (AVC/H.264/MPEG-4p10)
... Track 0 is a video track with video codec AVC/H.264/MPEG-4p10

Processing info line: Track ID 1: audio (Opus)
... Track 1 is an audio track with audio codec Opus

Processing info line: Track ID 2: subtitles (SubRip/SRT)
... Track 2 is a subtitles track with subtitles codec SubRip/SRT

Processing info line: Attachment ID 1: type 'image/jpeg', size 30184 bytes, file name 'test.jpg'
... Attachment 1 is type 'image/jpeg', size 30184 bytes, file name 'test.jpg'

Processing info line: Attachment ID 2: type 'image/jpeg', size 30184 bytes, file name 'test2.jpg'
... Attachment 2 is type 'image/jpeg', size 30184 bytes, file name 'test2.jpg'

Processing info line: Attachment ID 3: type 'image/jpeg', size 30184 bytes, file name 'test3.jpg'
... Attachment 3 is type 'image/jpeg', size 30184 bytes, file name 'test3.jpg'

Processing info line: Chapters: 9 entries

So the first and the last line of information output are not really processed while there is a further processing on the lines with first substring (token) being case-sensitive Track or Attachment.

The second FOR command line results in starting in background with Windows installed into C:\Windows one more Windows command processor with the following command line on current file being test.mkv:

C:\Windows\System32\cmd.exe /c mkvmerge.exe -i "test.mkv"

The Windows command processor has to search first for the file mkvmerge.exe by using the environment variables PATHEXT and PATH. So if the batch file has to process 100 .mkv files, there must be executed most likely several thousand of file system accesses in total to find this executable again and again. The usage of the fully qualified file name, i.e. mkvmerge.exe with its full path, would help to reduce the number of file system accesses to exactly 100 for 100 .mkv files for 100 executions of mkvmerge.exe.

Once mkvmerge.exe is found, the second cmd.exe started in background calls this executable with its full path and passes the two arguments to it. mkvmerge.exe outputs the data to handle STDOUT of the background command process.

The output to handle STDOUT of the background command process is captured by cmd.exe processing the batch file and FOR processes the lines one after the other after started cmd.exe closed itself after mkvmerge.exe finished.

FOR with option /F ignores always empty lines. That is no problem here.

Non-empty lines would be split up by default into substrings (tokens) using normal space and horizontal tab as string delimiters, then the first substring would be checked on beginning with a semicolon which is the default end of line character in which case the line would be ignored also for further processing and finally the first space/tab delimited substring is assigned to the specified loop variable H. But that default line processing behavior is not wanted in this case. For that reason delims= is used to defined an empty list of delimiters which results in getting the entire captured line assigned to the loop variable H, except the line would start with ; which can be excluded here because of mkvmerge.exe does not output a line with a semicolon at the beginning.

The entire line is output by the demonstration code to the console window so that it can be seen how the line looks like which is processed further.

The second for /F loop makes now the real job of processing the data of the line. The line assigned to loop variable H is specified in double quotes inside the round brackets resulting in FOR interpreting the line as string to process like a line captured from a command process executed in background or a line read from a text file.

This time the string delimiters are defined with a colon, an opening round bracket and a normal space using delims=:( . There is additionally specified with tokens=1,3,4* that of interest is not only the first substring, but the first, the third, the fourth and the rest of the line not further split up into substrings using the delimiters.

For example, let us look on what happens now with the following line:

Track ID 0: video (AVC/H.264/MPEG-4p10)

This line is split up to:

  1. Track being assigned to the specified loop variable I.
  2. ID which is not further processed at all.
  3. 0 which is assigned to next but one loop variable J according to the ASCII table.
    This is the reason for loop variables being case-sensitive.
  4. video which is assigned to loop variable K according to the ASCII table.
  5. AVC/H.264/MPEG-4p10) which is the rest of the line being assigned to the loop variable L.

The strings assigned to the loop variables I to L are processed further with the IF conditions whereby the round bracket at end of the codec string of a line beginning with Track is removed using one more for /F command with delims=).

It would be also possible to directly split up the captured lines into substrings as demonstrated with the following code which additionally ignores all lines beginning with Attachment on having before processed lines beginning with Track.

@echo off
setlocal EnableExtensions DisableDelayedExpansion
for %%G in (*.mkv) do (
    set "Tracks="
    echo/
    echo Processing file "%%G" ...
    for /F "tokens=1,3,4* delims=:( " %%H in ('mkvmerge.exe -i "%%G"') do (
        if "%%H" == "Track" (
            set "Tracks=1"
            for /F "delims=)" %%L in ("%%K") do (
                if "%%J" == "video" (
                    echo ... Track %%I is a video track with video codec %%L
                ) else if "%%J" == "audio" (
                    echo ... Track %%I is an audio track with audio codec %%L
                ) else if "%%J" == "subtitles" (
                    echo ... Track %%I is a subtitles track with subtitles codec %%L
                )
            )
        ) else if not defined Tracks (
            if "%%H" == "Attachment" echo ... Attachment %%I is %%J %%K
        )
    )
)
endlocal

The output of this demonstration code for file test.mkv is:

Processing file "test.mkv" ...
... Track 0 is a video track with video codec AVC/H.264/MPEG-4p10
... Track 1 is an audio track with audio codec Opus
... Track 2 is a subtitles track with subtitles codec SubRip/SRT

There is undefined first with set "Tracks=" the environment variable Tracks for each file before processing the lines output by mkvmerge.exe. If there is a line beginning with Track, the environment variable Tracks is defined with value 1 whereby the value does not matter.

On all other lines there is checked first if the environment variable Tracks is still not defined. If this condition is not true because of at least one line beginning with Track was processed before, the line is completely ignored. Otherwise there were no track information processed before and for that reason lines beginning with Attachment are processed next with printing information about the attachment. The first line with File and the last line with Chapters are still completely ignored in any case.

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.

  • echo /?
  • endlocal /?
  • for /?
  • if /?
  • setlocal /?

See also my answer on Symbol equivalent to NEQ, LSS, GTR, etc. in Windows batch files to get knowledge why it makes sense to enclose both strings to compare in " while it does not make sense to enclose them in [ and ] because of the square brackets have no special meaning for the Windows command processor.

  •  Tags:  
  • Related