问题
I have been scratching my head for the last two days trying to put together a code that reads a folder structure and the strips off the base path of the listing.
What I have so far is this:
@ECHO OFF
CLS
SETLOCAL EnableExtensions EnableDelayedExpansion
SET "StartTime=%TIME: =0%"
SET "Folder=%~1"
IF "%Folder:~-1%"=="\" SET "Folder=%Folder:~0,-1%"
FOR %%G IN ("%Folder%") DO SET "Archive=%%~nxG"
ECHO Processing %Folder%...
ECHO.
PUSHD "%Folder%"
FOR /F "DELIMS=" %%G IN ('DIR /A:-D /B /O:N /S') DO (
SET "FullPath=%%G"
ECHO !FullPath:%Folder%\=! >> "%Archive%.$$$"
)
pause
endlocal
goto :eof
This works fine as long the directory does not have ( or ) (e.g. C:\Program Files (x86)\AMD), then throws me an error message and halts.
Cannot find any solution to bypass this limitation. Any input is greatly appreciated. Thanks!
回答1:
The simple solution is to insert the following command line below SET "FullPath=%%G"
:
SET "RelativePath=!FullPath:%Folder%\=!"
A closing parenthesis inside a double quoted argument string is interpreted as literal character and not as end of a command block on being found inside a command block.
Then the next line is changed to:
IF DEFINED RelativePath ECHO !RelativePath!>>"%Archive%.$$$"
This line writes the relative path into the file without a trailing space as there is no space anymore left to redirection operator >>
which would be output also by ECHO.
So the batch file would be:
@ECHO OFF
CLS
SETLOCAL EnableExtensions EnableDelayedExpansion
IF "%~1" == "" GOTO :EOF
SET "Folder=%~1"
IF "%Folder:~-1%" == "\" SET "Folder=%Folder:~0,-1%"
FOR %%G IN ("%Folder%") DO SET "Archive=%%~nxG"
DEL "%Archive%.$$$" 2>NUL
ECHO Processing %Folder%...
ECHO/
FOR /F "DELIMS=" %%G IN ('DIR "%Folder%" /A:-D /B /O:N /S') DO (
SET "FullPath=%%G"
SET "RelativePath=!FullPath:%Folder%\=!"
IF DEFINED RelativePath ECHO !RelativePath!>>"%Archive%.$$$"
)
ENDLOCAL
PAUSE
But there are several problems remaining with this solution.
- If the folder path assigned to environment variable
Folder
contains an equal sign, the string substitution inside last FOR loop does not work as expected. - A folder path assigned to environment variable
Folder
with one or more exclamation marks is not correct processed because of enabled delayed expansion. - A full qualified file name output by DIR in separate command process started in background with one or more exclamation marks is not correct processed because of enabled delayed expansion.
- The batch file does not work as expected on being called with a root folder path of a drive like
D:\
or just\
to reference the root of current drive. - The batch file does not work as expected on being called with a relative path.
- Batch file processing is exited with a syntax error message on being called with
"""
as this results in the syntactically incorrect command lineIF """ == "" GOTO :EOF
during batch file execution. - An invalid folder path like
C:\Temp\*
orC:\Temp:
passed to the batch file is in some cases not detected by the batch file and results in not expected behavior.
See the Microsoft documentation page Naming Files, Paths, and Namespaces with details about relative paths.
The commented batch file below works for even those unusual use cases.
@echo off
cls
setlocal EnableExtensions DisableDelayedExpansion
rem Do nothing if batch file called without a folder path
rem or with just one or more double quotes.
set "Folder=%~1"
if defined Folder set "Folder=%Folder:"=%"
if not defined Folder (
echo Error: %~nx0 must be called with a folder path.
goto EndBatch
)
rem Make sure the folder path contains backslashes and not forward slashes
rem and does not contain wildcard characters or redirection operators or a
rem horizontal tab character after removing all double quotes.
set "Folder=%Folder:/=\%"
for /F "delims=*?|<> " %%I in ("%Folder%") do if not "%Folder%" == "%%I" (
echo Error: %~nx0 must be called with a valid folder path.
echo "%~1" is not a valid folder path.
goto EndBatch
)
rem Get full folder path in case of the folder was specified with a relative
rem path. If the folder path references the root of a drive like on using
rem "C:\" or just "\", redefine the folder path with full path for root of
rem the (current) drive. Determine also the name for the archive file to
rem create by this batch file in the current directory depending on name of
rem the folder to process respectively the drive in case of a root folder.
for %%I in ("%Folder%") do (
set "Folder=%%~fI"
set "ArchiveFile=%%~nxI.$$$"
)
if not "%Folder:~-1%" == "\" (
set "Folder=%Folder%\"
) else (
if "%Folder:~1,3%" == ":\" (
set "ArchiveFile=Drive%Folder:~0,1%.$$$"
) else (
for %%I in ("%Folder%.") do set "ArchiveFile=%%~nxI.$$$"
)
)
rem Verify the existence of the folder. The code above processed also
rem folder paths of folders not existing at all and also invalid folder
rem paths containing for example a colon not (only) after drive letter.
if not exist "%Folder%" (
echo Error: Folder "%Folder%" does not exist.
goto EndBatch
)
rem Determine the number of folders in folder path.
set "FolderCount=0"
set "FolderRemain=%Folder%"
:CountFolders
set /A FolderCount+=1
for /F "tokens=1* delims=\" %%I in ("%FolderRemain%") do if not "%%J" == "" set "FolderRemain=%%J" & goto CountFolders
rem Write all files with path relative to base folder into the archive file.
echo Processing "%Folder%" ...
(for /F "tokens=%FolderCount%* delims=\" %%I in ('dir "%Folder%" /A:-D /B /O:N /S 2^>nul') do echo %%J)>"%ArchiveFile%"
rem Delete the archive file on being an empty file because of no file found.
for %%I in ("%ArchiveFile%") do if %%~zI == 0 (
del "%ArchiveFile%"
echo/
echo Info: There are no files in folder: "%Folder%"
)
:EndBatch
endlocal
echo/
pause
ATTENTION: There should be a horizontal tab character after the delimiters *?|<>
in the batch file.
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.
call /?
... for an explanation of%~nx0
(name of batch file without path) and%~1
(argument 1 without surrounding double quotes)cls /?
del /?
dir /?
echo /?
endlocal /?
for /?
goto /?
if /?
pause /?
rem /?
set /?
setlocal /?
See also:
- Microsoft article about Using command redirection operators
- Single line with multiple commands using Windows batch file
- How does the Windows Command Interpreter (CMD.EXE) parse scripts?
- DosTips forum topic: ECHO. FAILS to give text or blank line - Instead use ECHO/
Here is the enhanced batch file once again in a more compact form without comments:
@echo off & cls & setlocal EnableExtensions DisableDelayedExpansion
set "Folder=%~1"
if defined Folder set "Folder=%Folder:"=%"
if not defined Folder echo Error: %~nx0 must be called with a folder path.& goto EndBatch
set "Folder=%Folder:/=\%"
for /F "delims=*?|<> " %%I in ("%Folder%") do if not "%Folder%" == "%%I" (
echo Error: %~nx0 must be called with a valid folder path.
echo "%~1" is not a valid folder path.
goto EndBatch
)
for %%I in ("%Folder%") do set "Folder=%%~fI" & set "ArchiveFile=%%~nxI.$$$"
if not "%Folder:~-1%" == "\" (set "Folder=%Folder%\") else if "%Folder:~1,3%" == ":\" (set "ArchiveFile=Drive%Folder:~0,1%.$$$") else for %%I in ("%Folder%.") do set "ArchiveFile=%%~nxI.$$$"
if not exist "%Folder%" echo Error: Folder "%Folder%" does not exist.& goto EndBatch
set "FolderCount=0"
set "FolderRemain=%Folder%"
:CountFolders
set /A FolderCount+=1
for /F "tokens=1* delims=\" %%I in ("%FolderRemain%") do if not "%%J" == "" set "FolderRemain=%%J" & goto CountFolders
echo Processing "%Folder%" ...
(for /F "tokens=%FolderCount%* delims=\" %%I in ('dir "%Folder%" /A:-D /B /O:N /S 2^>nul') do echo %%J)>"%ArchiveFile%"
for %%I in ("%ArchiveFile%") do if %%~zI == 0 del "%ArchiveFile%"& echo/& echo Info: There are no files in folder: "%Folder%"
:EndBatch
endlocal & echo/& pause
回答2:
Great explanations and solutions from Mofi
Here is a bit shorter and more direct way to do achieve a reliable solution that still relies on Mofi's fundamental technique of using the FOR /F
tokens
and delims
to strip out the root path.
My code allows no specified path, in which case it defaults to the current directory. The code to normalize, validate, and extract the needed values from the root path is greatly simplified.
I also use a totally different method for counting the number of tokens in the root path. I substitute a newline for the \
character and then count the number of lines with FIND /C
when I echo the value. It is a bit tricky, but it is very efficient and works without resorting to a GOTO
loop.
I also preserve the error message if no files are found, and don't bother deleting the resultant empty file.
@echo off
setlocal disableDelayedExpansion
::==== Get normalized values for supplied path in %1
::==== Default to current directory if not specified
for %%F in ("%~f1.") do ( %=== The trailing dot allows paths with/without trailing \ to work %
set "folder=%%~fF" ==== The normalized path for the root folder
set "archive=%%~nxF" ==== The base name of the output file
set "att=-%%~aF" ==== List of attributes for the root folder
)
::==== Validate path by checking for d in attributes
if %att:d=% == %att% (
>&2 echo Supplied path is invalid or not a folder
exit /b 1
)
::==== Special handling if drive root
if not defined archive (
set "archive=drive %folder:~0,1% root"
set cnt=1
goto start
)
::==== Normal processing for all other paths
setlocal enableDelayedExpansion
for %%L in (^"^
%==== Puts quoted newline in FOR variable L %
^") do set "folder2=!folder:\=%%~L!" ==== Substitutes newline for all \ in folder
::==== Count the number of tokens (lines) in folder2
setlocal disableDelayedExpansion
for /f %%N in ('"(cmd /v:on /c echo !folder2!)|find /c /v """') do set cnt=%%N
:start ==== Finally get the sorted list of files without root path
>"%archive%.$$$" (
for /f "delims=\ tokens=%cnt%*" %%A in ('dir "%folder%" /a:-d /b /o:n /s') do echo %%B
)
The code is quite terse without the comments and extra blank lines
@echo off
setlocal disableDelayedExpansion
for %%F in ("%~f1.") do (
set "folder=%%~fF"
set "archive=%%~nxF"
set "att=-%%~aF"
)
if %att:d=% == %att% (
>&2 echo Supplied path is invalid or not a folder
exit /b 1
)
if not defined archive (
set "archive=drive %folder:~0,1% root"
set cnt=1
goto start
)
setlocal enableDelayedExpansion
for %%L in (^"^
^") do set "folder2=!folder:\=%%~L!"
setlocal disableDelayedExpansion
for /f %%N in ('"(cmd /v:on /c echo !folder2!)|find /c /v """') do set cnt=%%N
:start
>"%archive%.$$$" (
for /f "delims=\ tokens=%cnt%*" %%A in ('dir "%folder%" /a:-d /b /o:n /s') do echo %%B
)
Update in response to Mofi's comment
My original answer did not consider the possibility of wildcards in the input. Wildcards can be excluded by adding the following after the initial SETLOCAL
for /f "delims=*?<> tokens=2" %%A in (".%~1.") do (
>&2 echo Wildcards * ? ^< and ^> not supported
exit /b 1
)
Note that "<"
and ">"
are poorly documented wildcards (only when they are quoted within a path)!
But Mofi described an interesting behavior I had never seen before. At first I could not understand how C:\test\*
was yielding C:\
.
That is simply the result of %~f1
seeing the .
directory entry as the first listed file/folder in the test folder, and then when my code appends its own dot, it becomes C:\test\..
, which of course normalizes to C:\
.
For a while I was thinking that there was special processing going on when a path node is nothing but wildcards, but I posted the behavior on DosTips and a responder reminded me of the .
and ..
directory entries on all but the drive root.
来源:https://stackoverflow.com/questions/61436029/how-to-replace-a-string-with-a-substring-when-there-are-parentheses-in-the-strin