Search code examples
batch-filecmdwindows-10

How to copy/rename all files in a directory in Windows batch?


My question is similar to Rename all files in a directory with a Windows batch script, but I would like to give the new files on copy an odd number.

It should work in a cmd.exe window under Windows 10.

In a directory I have:

a.txt
b.txt
c.txt

...and would like to copy these files to:

_1.txt
_3.txt
_5.txt

I have tried this, but without success:

@echo off                       
setlocal enableDelayedExpansion 
set COUNTER=-1                   
for %%F in (*.txt) do (         
    set /A C = %COUNTER% + 2      
    rem echo %C%                      
    copy %%F _%C%.txt             
    set COUNTER = %C%             
)                               

Can someone tell my why this do not work? The %C% is always empty.


Solution

  • There should be opened a command prompt window, ran set /? and read the output usage help. It explains the syntax to (re)define environment variables. There are no spaces around =. See also: Why is no string output with 'echo %var%' after using 'set var = text' on command line? There is also explained when and how to use delayed variable expansion on an IF and a FOR example. It is also described that the current values of environment variables should be referenced within an arithmetic expression with using just the variable names without using % or ! around the variable names. An arithmetic expression is the argument string after option /A of command SET.

    There can be also executed in the command prompt window for /? for getting output the usage help of command FOR which is obviously needed too.

    There are some important facts not described in the helps of command FOR and SET which are important on copying (or renaming or deleting or modifying) files in a directory with file extension kept and so matched also by the wildcard pattern.

    1. FOR processes the list of matching file system entries which changes on each iteration of the loop because of the file copy operation which is very problematic here. For more details see: At which point does `for` or `for /R` enumerate the directory (tree)? The solution is getting first loaded all the wildcard pattern matching file names into the memory of cmd.exe processing the batch file before starting the copy operation loop.
    2. Enabled delayed variable expansion results in parsing a command line a second time by cmd.exe after loop variable references like %%F are replaced by the string assigned to the loop variable. An exclamation mark in a file name is therefore interpreted as beginning/end of a delayed expanded variable reference. An exclamation mark is removed on being the last ! on odd number of ! in file name and everything between two ! in a file name is replaced by the value of the referenced environment variable respectively is removed from file name on no such environment variable exists at all. The result of this unwanted file name change caused by enabled delayed expansion is a not found file or a by mistake wrong selected file for the copy operation. See also: How does the Windows Command Interpreter (CMD.EXE) parse scripts?

    Here is a batch file code which copies all non-hidden files with file extension .txt even on containing on or more ! in file name.

    @echo off
    setlocal EnableExtensions DisableDelayedExpansion
    set "Counter=-1"
    for /F "eol=| delims=" %%I in ('dir *.txt /A-D-H /B /ON 2^>nul') do (
        set "FileName=%%I"
        set /A Counter+=2
        setlocal EnableDelayedExpansion
        copy /B /Y "!FileName!" "_!Counter!.txt" >nul
        endlocal
    )
    endlocal
    

    There is started by cmd.exe processing the batch file one more command process in background with using %ComSpec% /c and the command line within ' specified on the FOR /F command line appended as additional arguments. There is executed for that reason with Windows installed into C:\Windows:

    C:\Windows\System32\cmd.exe /c dir *.txt /A-D-H /B /ON 2>nul
    

    The internal command DIR of in background started cmd.exe searches now

    • in the current directory as defined by the process starting cmd.exe for processing the batch file which can be any directory and not necessarily the directory containing the batch file
    • for just non-hidden files or non-hidden links to files because of option /A-D-H (attribute not directory and not hidden) including files with read-only attribute set
    • matching the wildcard pattern *.txt in long or short 8.3 file name
    • and outputs in bare format just the file names because of option /B
    • in alphabetical order by name because of option /ON.

    An error message output by DIR to handle STDERR (standard error) on no matching file system entry found is suppressed by redirecting it to device NUL.

    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 Windows Command Processor processing the batch file captures everything written to handle STDOUT (standard output) of the background command process. Once DIR finished resulting in close of the background cmd.exe because of option /c, the internal command FOR of the batch file processing cmd.exe starts the processing of the list of file names which is loaded now into the memory of cmd.exe processing the batch file. The file copy operations in same directory do not change anymore the file names list processed by FOR.

    The FOR /F option delims= defines an empty list of delimiters to turn off the default line splitting behavior on normal spaces and horizontal tabs to get the full file name in a line assigned to the specified loop variable I and not just the first space/tab separated string as by default.

    The FOR /F option eol=| defines the vertical bar as end of line character replacing the default ; which prevents ignoring a file name beginning with a semicolon. The Microsoft documentation about Naming Files, Paths, and Namespaces describes which characters are not allowed in file names on Windows file systems like NTFS, FAT32 or exFAT. One of those characters should be used as end of line character.

    It is necessary to assign first the file name with file extension to an environment variable and increment the value of the environment variable Counter by two. Then delayed variable expansion is enabled for copying the file with referencing the file name assigned to the environment variable FileName and the value of the Counter environment variable. The previous environment must be restored next with the command ENDLOCAL as otherwise a stack overflow could occur during the copy operations. Read this answer for details about the commands SETLOCAL and ENDLOCAL with a full description what happens on each execution of SETLOCAL and ENDLOCAL in memory of cmd.exe processing the batch file.

    NOTE:

    It would be a very good idea to copy the files into a different directory than the current directory like the parent directory of the current directory by using the command line:

    copy /B /Y "!FileName!" "..\_!Counter!!FileExt!" >nul
    

    The code above is still a problem if the current directory contains a.txt, b.txt, c.txt, ... but also already _1.txt, _3.txt, _5.txt, ... from a previous execution of the batch file. Copying the files into a different directory would avoid that problem as well as the usage of the following code:

    @echo off
    setlocal EnableExtensions DisableDelayedExpansion
    set "Counter=-1"
    for /F "eol=| delims=" %%I in ('dir *.txt /A-D-H /B /ON 2^>nul ^| %SystemRoot%\System32\findstr.exe /I /V /X "_[0123456789][0123456789]*\.txt"') do (
        set "FileName=%%I"
        set /A Counter+=2
        setlocal EnableDelayedExpansion
        copy /B /Y "!FileName!" "_!Counter!.txt" >nul
        endlocal
    )
    endlocal
    

    The command FINDSTR is used for filtering out all file names of which entire name is matched case-insensitive by the regular expression which searches for an underscore followed by one or more digits and next a literally interpreted dot and the string txt.

    To understand the commands used and how they work, open a command prompt window, execute there the following commands, and read the displayed help pages for each command, entirely and carefully.

    • cmd /?
    • copy /?
    • dir /?
    • echo /?
    • endlocal /?
    • findstr /?
    • for /?
    • set /?
    • setlocal /?