0

I want to create a batch file able to apply some processing on each JPG file in a folder hierarchy. The following script file works very well for that case (here I only echo the name of each file, but this should be replaced by some more complex statements in the real application):

:VERSION 1
@echo off
set "basefolder=C:\Base"
for /r %basefolder% %%f in (*.jpg) do echo %%f

Actually, I don't want to explore all the folder hierarchy under %basefolder%, but only a given list of subfolders. This modified script is able to deal with that case :

:VERSION 2
@echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
  pushd %basefolder%\%%~s"
  for /r %%f in (*.jpg) do echo %%f
  popd
)

Is there a solution to remove the pushd/popd pair of statements, to get something closer to the initial script. I thought that one of the following scripts would do the job:

:VERSION 3
@echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
  for /r %basefolder%\%%~s" %%f in (*.jpg) do echo %%f
)

or, using delayed expansion:

:VERSION 4
@echo off
setlocal enabledelayedexpansion
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
  set "folder=%basefolder%\%%~s"
  echo !folder!
  for /r !folder! %%f in (*.jpg) do echo %%f
)

but none of them is working. When running the second one, the echo !folder! command in the external loop shows C:\Base\A, C:\Base\B and C:\Base\C as expected, but the inner loop doesn't echo any JPG file, so I guess that the recursive for /r command does not run correctly.

What am I doing wrong ?


Final edit after answers :

Thanks to @aschipfl who provided a link to the answer posted by @jeb on another question, quoted below:

The options of FOR, IF and REM are only parsed up to the special character phase. Or better the commands are detected in the special character phase and a different parser is activated then. Therefore it's neither possible to use delayed expansion nor FOR meta-variables in these options.

In other words, my versions 3 and 4 do not work because when defining the root folder of the FOR /R command, neither the %%~s nor the !folder! are correctly expanded by the expression parser. There is no way to change that, as this is a parser limitation. As I said in a comment below: the root folder option in the FOR /R command is basically only syntactic sugar to avoid the use of pushd/popd before and after the command. As this syntactic sugar is incomplete, we have to stick to the original syntax for some specific use cases, as the one presented here. The alternatives proposed by @Gerhard (using a subroutine CALL) or by @Mofi (parsing the result of a DIR command) are working, but they are neither more readable nor more efficient than the simple pushd/popd version I proposed initially.

sciroccorics
  • 2,357
  • 1
  • 8
  • 21
  • What about a simple one liner, `@For %%S In ("A","B","C") Do @Dir /B /S /A:-D "%BaseFolder%\%%~S\*.jpg"`? – Compo Nov 03 '20 at 02:26
  • @Compo: Well, the 'echo %%f' in my post was simply a placeholder to show a short usecase. In my **real** application, I have to apply several processing on each JPG file, so a simple 'dir' is not sufficient. I agree that this wasn't clear in the initial post, so I've edited it on that point. Anyway, the pushd/podp version solves all my needs, so my question is more : why does the "for /s folder %%f ..." syntax not work for the inner loop, while it works for the outer loop in the first version. – sciroccorics Nov 03 '20 at 07:19
  • 1
    You cannot use a `for` meta-variable to define the root for `for /R`, neither can you use a variable with delayed expansion – take a look at these related threads: [Nested FOR with variables](https://stackoverflow.com/q/38465855) and [Why is escaping exclamation marks not necessary in parameter of `for /F` or `for /R`?](https://stackoverflow.com/q/39649641) – aschipfl Nov 03 '20 at 21:12
  • 1
    @aschipfl : Thanks for the links you provided, especially the [answer posted by @jeb](https://stackoverflow.com/a/39653402/6770842) which explains why there is no solution to use meta-variable or delayed expansion for the root folder of a `FOR /R` statement. So I guess I will stay on my pushd/popd version, which I find less cumbersome than the `CALL` based workaround posted by Gerhard, or the `FOR /F` one proposed by Mofi. Changing the root folder in `FOR /R` is basically only syntactic sugar to avoid pushd/popd, but if syntactic sugar is less powerful, there is no good reason to employ it. – sciroccorics Nov 03 '20 at 22:56

2 Answers2

0

It is in general not advisable to assign the value of a loop variable to an environment variable and next use the environment variable unmodified without or with concatenation with other strings being coded in batch file or defined already above the FOR loop within body of a FOR loop. That causes just problems as it requires the usage of delayed expansion which results in files and folders with one or more ! are not correct processed anymore inside body of the FOR loop caused by double parsing of the command line before execution, or command call is used on some command lines, or a subroutine is used called with call which makes the processing of the batch file much slower.

I recommend to use this batch file for the task:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "basefolder=C:\Base"
set "subfolders=A B C "Subfolder D" SubfolderE"
for %%I in (%subfolders%) do for /F "delims=" %%J in ('dir "%basefolder%\%%~I\*.jpg" /A-D /B /S 2^>nul') do echo %%J
endlocal

The inner FOR loop starts for each subfolder defined in subfolders in background one more command process with %ComSpec% /c and the DIR command line appended as additional arguments. So executed is with Windows installed to C:\Windows for example for the first subfolder:

C:\Windows\System32\cmd.exe /c dir "C:\Base\A\*.jpg" /A-D /B /S 2>nul

The command DIR searches

  • in specified directory C:\Base\A and all it subdirectories because of option /S
  • for files because of option /A-D (attribute not directory) including those with hidden attribute set
  • matching the pattern *.jpg in long or short file name
  • and outputs to handle STDOUT of background command process just the matching file names because of option /B (bare format)
  • with full path because of option /S.

The error message output by DIR on nothing found matching these criteria is redirecting from handle STDERR to device NUL to suppress it.

Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.

The output to handle STDOUT of background command process is captured by FOR respectively the command process which is processing the batch file. FOR processes the captured output line by line after started cmd.exe terminated itself. This is very often very important. The list of files to process is already in memory of command process before processing the first file name. This is not the case on using for /R as this results in accessing file system, getting first file name of a non-hidden file matching the wildcard pattern, run all commands in body of FOR and accessing the file system once again to get next file name. The for /R approach is problematic if the commands in body of FOR change a file to process like deleting, moving, modifying, copying it in same folder, or renaming a found file because of the entries in file system changes while for /R is iterating over these entries. That can easily result in some files are skipped or some files are processed more than once and it could result also an endless running loop, especially on FAT file system like FAT32 or exFAT. It is never good to iterate over a list of files on which the list changes on each iteration.

Command FOR on usage of /F ignores empty lines which do not occur here. A non-empty line is split up into substrings using a normal space and a horizontal tab as string delimiters by default. This line splitting behavior is not wanted here as there could be full qualified file names containing anywhere inside full name one or more spaces. For that reason delims= is used to define an empty list of delimiters which disables the line splitting behavior.

FOR with option /F would also ignore lines on which first substring starts with ; which is the default end of line character. This is no problem here because of command DIR was used with option /S and so each file name is output with full path which makes it impossible that any file name starts with ;. So the default eol=; can be kept.

FOR with option /F assigns by default just first substring to specified loop variable as tokens=1 is the default. This default can be kept here as splitting the lines (full file names) into substrings is disabled already with delims= and so there is always the full file name assigned to the loop variable.

This example uses just echo %%I to output the file names with full path. But it is now safe to replace this single command by a command block which does more with the JPEG files because of the list of JPEG files for each specified subfolder tree in base folder is always already completely in memory of command process processing the batch file.

Mofi
  • 46,139
  • 17
  • 80
  • 143
  • @@Mofi: Thanks for your very detailed answer. Three remarks, however: (1) On the performance point of view, I guess that a pushd/popd pair would be more efficient than launching a full cmd.exe process for each subfolder just to perform a 'dir' command, (2) On the readability point of view, again the pushd/popd version seems easier to understand, (3) For me the most obvious/efficient/readable version would have been my third one, using a FOR /R starting each time with an updated root folder. After having read your answer, I still don't understand why this is not working. – sciroccorics Nov 03 '20 at 09:33
  • @sciroccorics From a performance point of you the pushd/popd solution would be better. Readability is opinion based as it does not make a difference for me as advanced batch file coder with a very good syntax highlighting. For the __reliability__ point of view regarding to really processing the JPEG files instead of just echoing them, the solution posted by me is highly recommended. However, as long as the `*.jpg` files are just read and nothing else is done with them, you can use `for /R`. In all other cases, `for /F` with `dir` is highly recommended as `for /R` is unreliable in other cases. – Mofi Nov 03 '20 at 15:32
0

My Approach for this would be really straight forward:

@echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do for /R "%basedir%" %%a in ("%%~i\*.jpg") do echo %%~fa

The double quotes inside of the subfolders variable is important here, it will ensure that folder names with whitespace are not seen as separators for the folder names. For instance:

set "subfolders="Folder A","Folder B","Folder C""

Edit

@echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do call :work "%%~i"
goto :eof
:work
for /R "%basedir%\%~1" %%a in (*.jpg) do echo %%~fa
Gerhard
  • 22,678
  • 7
  • 27
  • 43
  • Nice, that's a clever workaround indeed ! Didn't thought about it. But it makes the failure of my third version even more strange : why can we use %%~i as a prefix for *.jpg (as you did) but not as a suffix for %basedir% (as I did). I know that cmd sucks as a scripting language, but still, how can this weird behavior be explained from a parser point of view? That's just a matter of variable evaluation and string concatenation, nothing different between the two versions. – sciroccorics Nov 03 '20 at 13:09
  • Oops, I was a bit too optimistic. Your solution does not work when the JPG files are not directly located in the provided subfolders but somewhere deeper in the folder hierarchy. – sciroccorics Nov 03 '20 at 13:22
  • I was under the impression that only files in the root of the cild dir was to be used, anyway, see edit. Keep in mind the reason for the behaviour is because `for /R` does not expect the metavariable in the particular place. – Gerhard Nov 03 '20 at 14:32