0

I have the following code;

setlocal EnableDelayedExpansion

:splitEncode
::Get the number of Chapters
set "cmd=FINDSTR /R /N "^.*" %~n1.txt | FIND /C ":""
for /F %%a in ('!cmd!') do set numChapters=%%a

::Cycle through this once for every chapter, getting the line and the line after it
for /L %%a in (1,1,%numChapters%) do (
    set "skip="
    if %%a geq 2 (
        set /a skip=%%a-1
        set "skip=skip=!skip!"
    )

    for /F "!skip! tokens=1,2" %%i in ("%~n1.txt") do (
        set startTime=%%i
        set chapterName=%%j
    )

    set "skip=skip=%%a"
    for /F !skip! %%i in ("%~n1.txt") do (
        set endTime=%%i
    )
    echo %startTime% %endTime% %chapterName%
)

First I find out how many lines are in a text file, and set that to the variable numChapters. I then use this to create a for loop that iterates for each chapter. Inside the for loop, there are two further loops. The first reads a line, and the second reads the following line.

The intent of this is to read lines 1+2, 2+3, 3+4, and use those values as part of a command run the same number of times as the number of lines. This means that from a list such as this;

00:00:00 The Meeting Room/The Meeting
00:03:36 Long Distance Runaround
00:07:47 Wonderous Stories

I can end up with a command that includes the start time, end time, and chapter title.

The issue I am facing is that no matter what I do, I cannot get the nested for loops to use the skip variables. I've tried %%a, %skip%, !skip!, and none of them work. The value isn't correctly substituted in any situation.

Does anyone have any way to get this variable used, or a better method of reading a specific line of a text file than a for loop?

Jademalo
  • 323
  • 1
  • 8
  • 1
    The option string of `for /F` (like the root path of `for /R`) requires immediate (`%`-)expansion, though which cannot be used inside the loop body as the variables change. However, move the affected `for /F` loops into sub-routines and use [`call`](https://ss64.com/nt/call.html) to call them, because then `%`-expansion works. *N. B.:* Always quote the option string to protect the `=`-signs… – aschipfl Oct 26 '22 at 11:26
  • Ah! I hadn't considered moving them to subroutines, that's a good thought, thanks. I've not used batch much, so I'm not too familiar with things like that. What do you mean by always quote the option string? – Jademalo Oct 26 '22 at 11:45
  • It's about your last loop `for /F !skip! %%i in …`, which should read `for /F "!skip!" %%i in …`, in order to protect the `=` in the string `skip=…`. *N. B.:* I encountered the same problem several years ago – see [this question](https://stackoverflow.com/q/39649641) of mine… – aschipfl Oct 26 '22 at 13:31
  • PErhaps a better solution could be devised if you were to post what your desired output would be. – Magoo Oct 26 '22 at 21:14
  • Desired output was a start and end time for each chapter to plug into ffmpeg, totally forgot to mention it since my brain was a bit fried. Last chapter doesn't need an end time since simply not specifying it will go to the end of the input video in ffmpeg. – Jademalo Oct 27 '22 at 13:17

1 Answers1

1

The option string of for /F (like the root path of for /R) requires immediate (%-)expansion, because for (besides if and rem) is recognised by the command interpreter even before delayed expansion and also expansion of for meta-variables occur.

A possible solution is to put each for /F loop with the dynamic skip options into a sub-routine, to use call to call it and to apply %-expansion therein (see all the additional rem remarks for explanations):

@echo off
setlocal EnableDelayedExpansion

:splitEncode
rem Get the number of chapters
rem // To determine the number of lines in a file you do not need `findstr`:
for /F %%a in ('^< "%~n1.txt" find /C /V ""') do set "numChapters=%%a"

rem Cycle through this once for every chapter, getting the line and the line after it
for /L %%a in (1,1,%numChapters%) do (
    set /A "skip=%%a-1"
    call :getTwoValues startTime chapterName "%~n1.txt" "!skip!"
    call :getTwoValues endTime dummy "%~n1.txt" "%%a"
    rem /* For the last line, there is of course no next line containing the end time;
    rem    therefore, let us mark that case specifically: */
    if not defined endTime set "endTime=??:??:??"
    rem /* If there is no chapter specified, do not output anything; this might also
    rem    be quite useful in case the last line just contains a time stamp but no
    rem    chapter name just to provide the end time of the last one: */
    if defined chapterName echo !startTime! !endTime! !chapterName!
)
goto :EOF


:getTwoValues  <1st var. name>  <2nd var. name>  <file path/name>  <lines to skip>
rem // Ensure not to return the former output, and set up `skip` option string:
set "%~1=" & set "%~2=" & set /A "skip=0, skip+=%~4" 2> nul
if %skip% gtr 0 (set "skip=skip=%skip%") else set "skip="
rem /* Added `usebackq` in order not to interprete the quoted file path/name as
rem    a literal string; also changed the `tokens` option to return the first
rem    token and then the whole remainder of the line: */
rem /* Remember that `for /F` regards empty lines for its `skip` option, but it
rem    does not iterate through such; hence the first line it iterates over is
rem    actually the first non-empty line after the number of skipped lines: */
for /F "usebackq %skip% tokens=1,*" %%i in ("%~3") do (
    set "%~1=%%i"
    set "%~2=%%j"
    rem // Since we do not want to iterate to the last line, leave the loop here:
    goto :EOF
)
rem /* This is just needed in case `skip` points beyond the end of the file, or
rem    there are no more non-empty lines behind the skipped ones: */
goto :EOF

Based on your sample data, the output should be this:

00:00:00 00:03:36 The Meeting Room/The Meeting
00:03:36 00:07:47 Long Distance Runaround
00:07:47 ??:??:?? Wonderous Stories

However, the entire approach could be heavily simplified, when you avoid the file multiple times and do not read each line twice, by simply reading the file line by line once, but return the chapter information from the previous iteration, together with the end time from the current line:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem // Define constants here:
set "_FILE=%~n1.txt" & rem // (path/name of file to process)
set "_FRMT=??:??:??" & rem // (dummy end time output for last chapter)

rem /* Initialise variables; loop through lines of files, augmented by
rem    an additional line at the end, to alyways output last chapter: */
set "STA=" & for /F "tokens=1,*" %%K in ('
    type "%_FILE%" ^& echo(%_FRMT%
') do (
    rem // Output the chapter from the previous loop iteration:
    set "END=%%K" & if defined STA if defined NAME (
        setlocal EnableDelayedExpansion
        echo(!STA! !END! !NAME!
        endlocal
    )
    rem // Store chapter information for the next loop iteration:
    set "STA=%%K" & set "NAME=%%L"
)

endlocal
exit /B
aschipfl
  • 33,626
  • 12
  • 54
  • 99
  • Oh fantastic, thank you so much for the answer! The intended output was just the time variables to be used as an ffmpeg input, and track names for the filename of ffmpeg's output. The last one doesn't actually need a defined end time since the end of the video is the end. I should've mentioned that, but my brain was a bit fried when I wrote the question. That second example is great, looks to be a much cleaner way of doing what I want. Thanks! – Jademalo Oct 27 '22 at 13:16