简体   繁体   中英

Workaround for problems with “!” literals in a For loop with delayed variable expansion

How do I make a workaround for the FOR Loop? The problem is that DelayedExpansion fails for filenames with "!" in them

The code (this is part of a bigger script)

:FileScan
::Sets the filenames(in alphabetical order) to variables
SETLOCAL EnableDelayedExpansion
SET "EpCount=0"
FOR /f "Tokens=* Delims=" %%x IN ('Dir /b /o:n %~dp0*.mkv* *.mp4* *.avi* *.flv* 2^>nul') DO (
    SET /a "EpCount+=1"
    SET Ep!EpCount!=%%x
)
IF %EpCount%==0 ( Echo. & Echo: No Episodes Found & GOTO :Err )

:FolderScan
::Sets the foldernames(in alphabetical order) to variables
SET "FolderCount=0"
FOR /f "Tokens=* Delims=" %%x IN ('Dir /b /o:n /a:d "%~dp0Put_Your_Files_Here" 2^>nul') DO (
    SET /a "FolderCount+=1"
    SET Folder!FolderCount!=%%x
)

If not possible in batch how do I do it in PowerShell to something that can be called by the original batch like:

:FileScan
Call %~dp0FileScanScript.ps1
IF %EpCount%==0 ( Echo. & Echo: No Episodes Found & GOTO :Err )
:FolderScan
Call %~dp0FolderScanScript.ps1

EDIT: CALL SET fixed the original code avoiding DelayedExpansion altogether

SET "EpCount=0"
FOR /f "Tokens=* Delims=" %%x IN ('Dir /b /o:n %~dp0*.mkv* *.mp4* *.avi* *.flv* 2^>nul') DO (
    SET /a "EpCount+=1"
    CALL SET "Ep%%EpCount%%=%%x"
)

The easiest solution is indeed to use call set to introduce another parsing phase:

setlocal DisableDelayedExpansion
pushd "%~dp0."
set /A "EpCount=0"
for /F "delims=" %%x in ('
    dir /B /A:-D /O:N "*.mkv*" "*.mp4*" "*.avi*" "*.flv*" 2^> nul
') do (
    set /A "EpCount+=1"
    call set "Ep%%EpCount%%=%%x"
)
popd
endlocal

However, this causes problems as soon as carets ^ appear in the strings or file names, because call doubles them when they appear quoted.

To solve this, you need to make sure that the strings are expanded in the second parsing phase too, which can be achieved by using an interim variable, like this:

setlocal DisableDelayedExpansion
pushd "%~dp0."
set /A "EpCount=0"
for /F "delims=" %%x in ('
    dir /B /A:-D /O:N "*.mkv*" "*.mp4*" "*.avi*" "*.flv*" 2^> nul
') do (
    set "Episode=%%x"
    set /A "EpCount+=1"
    call set "Ep%%EpCount%%=%%Episode%%"
)
popd
set Ep
endlocal

In addition, I added the filter option /A:-D to dir to ensure that no directories are returned. Furthermore, I used pushd and popd to change to the parent directory of the script temporarily. The dir command line as you wrote it, searched files *.mkv* in the parent directory of the script, but all the other ones in the current working directory, which is probably not what you wanted.


Another option is to toggle delayed expansion, so that the for variable reference %%x becomes expanded when delayed expansion is disabled, and the assignment to the Ep array-style variables is done when it is enabled. But you need to implement measures to transport variable values beyond the endlocal barrier then, like in the following example:

setlocal DisableDelayedExpansion
pushd "%~dp0."
set /A "EpCount=0"
for /F "delims=" %%x in ('
    dir /B /A:-D /O:N "*.mkv*" "*.mp4*" "*.avi*" "*.flv*" 2^> nul
') do (
    rem /* Delayed expansion is disabled at this point, so expanding
    rem    the `for` variable reference `%%x` is safe here: */
    set "Episode=%%x"
    set /A "EpCount+=1"
    rem // Enable delayed expansion here:
    setlocal EnableDelayedExpansion
    rem /* Use a `for /F` loop that iterates once only over the assignment string,
    rem    which cannot be empty, so the loop always iterates; the expanded value
    rem    is stored in `%%z`, which must be assigned after `endlocal` in order
    rem    to have it available afterwards and not to lose exclamation marks: */
    for /F "delims=" %%z in ("Ep!EpCount!=!Episode!") do (
        endlocal
        set "%%z"
    )
)
popd
set "Ep"
endlocal
CALL SET Ep%%EpCount%%=%%x

would set your variables appropriately, as would

FOR /f "tokens=1*delims=[]" %%x IN (
 'Dir /b /o:n %~dp0*.mkv* *.mp4* *.avi* *.flv* 2^>nul ^|find /v /n ""'
 ) DO (
 SET /a epcount=%%x
 CALL SET ep%%x=%%y
)

(prefix each name with [num] then use for to extract num to %%x and name to %%y (assuming there are no names that start [ or ] ))

Note: The OP had originally tagged the question in addition to , but later removed that tag (since restored).

The equivalent of the following batch-file ( cmd ) loop:

SETLOCAL EnableDelayedExpansion
FOR /f "Tokens=* Delims=" %%x IN ('Dir /b /o:n %~dp0*.mkv* *.mp4* *.avi* *.flv* 2^>nul') DO (
    SET /a "EpCount+=1"
    SET Ep!EpCount!=%%x
)

in PowerShell (PSv3+) is:

# Collect all episode paths in array $Eps
$Eps = (Get-ChildItem -Recurse $PSScriptRoot -Include *.mkv, *.mp4, *.avi, *.flv).FullName
# Count the number of paths collected.
$EpCount = $Eps.Count

The above will work with paths that have embedded ! instances too.
$Eps will be an array of the full filenames.


To then process the matching files one by one :

foreach ($ep in $Eps) { # Loop over all files (episodes)
  # Work with $ep
}

Using a single array rather than distinct $Ep1 , $Ep2 , ... variables makes for much simpler and more efficient processing.


The above is the fastest way to process the already-collected-in-memory filenames one by one.
A more memory-efficient (though slightly slower), pipeline-based approach would be:

Get-ChildItem -Recurse $PSScriptRoot -Include *.mkv,*.mp4,*.avi,*.flv | ForEach-Object {
  # Work with $_.FullName - automatic variable $_ represents the input object at hand.
}

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