0

I can add a prefix to a series of text files using:

:: rename files
for %%a in (*.txt) do (
ren "%%a" "Seekret file %%a"
:: ECHO %%a Seekret file %%a
)

which will turn

 a.txt
 b.txt
 c.txt

into

 Seekret file a.txt
 Seekret file b.txt
 Seekret file c.txt

However, the above code seems to rename the first file twice with the prefix. I end up with

Seekret file Seekret file a.txt

and I have no idea why. Any ideas?

aschipfl
  • 33,626
  • 12
  • 54
  • 99
Ghoul Fool
  • 6,249
  • 10
  • 67
  • 125
  • Do not use invalid labels like `::` as comments within parenthesised blocks of code as they might lead to strange unexpected results; use `rem` instead... – aschipfl Jul 25 '19 at 22:48

3 Answers3

4

Use

for /f "delims=" %%a in ('dir /b /a-d *.txt') do (

What is happening is that the version you are using sees the renamed-file as a new file. The dir version builds a list of the filenames and then executes the for on each line, so the list is already built and static and cmd isn't trying to operate on a moving target.

Also - use rem, not :: within a code-block (parenthesised sequence of instructions) as this form of comment is in fact a broken label and labels are not allowed in a code block.

Magoo
  • 77,302
  • 8
  • 62
  • 84
  • 1
    to take this a step further: if you're using a constant prefix and this is not the first time you've run your prefix-adding loop - you'll need to check for the existence of the prefix on your existing files, or you'll be running into the same issue you were having before. this can be done by comparing your parameter to a variable substring and only renaming those that don't match (to give one example). https://ss64.com/nt/syntax-substring.html – mael' Jul 25 '19 at 19:36
3

Yes, this can happen, especially on FAT32 and exFAT drives because of these file systems do not return the list of directory entries matched by a wildcard pattern to calling executable in an alphabetic order. for processes the directory entries matching *.txt one after the other and the command ren results in changing the directory entries, i.e. the file names list is modified while iterating over it.

The solution is using:

for /F "eol=| delims=" %%I in ('dir *.txt /A-D /B 2^>nul') do ren "%%I" "Seekret file %%I"

FOR runs in this case in background %ComSpec% /c with the command line specified between ' which means with Windows installed into directory C:\Windows:

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

So one more command process is started in background which executes DIR which

  • searches in current directory
  • just for files because of option /A-D (attribute not directory)
  • including files with hidden attribute set (use /A-D-H to exclude hidden files)
  • matching the wildcard pattern *.txt
  • and outputs in bare format just the file names because of option /B.

An error message output by DIR to handle STDERR in case of not finding any directory entry matching these criteria is suppressed by redirecting it to device NUL.

Read the Microsoft article 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 file names without path are output by DIR to handle STDOUT of background command process. This output is captured by FOR respectively the command process executing the batch file.

After started command process terminated itself, FOR processes the captured list of file names. All changes done on directory during the loop iterations do not matter anymore for that reason. The file names list does not change anymore.

The options eol=| delims= are needed to get the complete file names assigned one after the other to loop variable I even on starting with ; or containing a space character. eol=| redefines default end of line character ; to a vertical bar which no file name can contain. delims= defines an empty list of delimiters to disable default line splitting behavior on normal spaces and horizontal tabs.

Note: :: is an invalid label and not a comment. Labels inside a command block are not allowed and usually result in undefined behavior on execution of the command block. Use command REM (remark) for a comment.

Even better would be:

for /F "eol=| delims=" %%I in ('dir *.txt /A-D /B 2^>nul ^| %SystemRoot%\System32\findstr.exe /B /I /L /V /C:"Seekret file "') do ren "%%I" "Seekret file %%I"

FINDSTR is used here to output from list of file names output by DIR and redirected to STDIN of FINDSTR all file names which

  • do not because of /V (inverted result)
  • begin because of option /B
  • case-insensitive because of option /I
  • with the literally interpreted because of option /L (redundant to /C:)
  • string Seekret file .

Option /C: is needed to specify the search string containing two spaces as using just "Seekret file" would result in searching literally and case-insensitive for either Seekret OR file at begin of a line. In a search string specified with just "..." each space is interpreted by FINDSTR as an OR expression like | in a Perl regular expression string.

A search string specified with /C: is interpreted implicitly as literal string, but with using /R (instead of /L) it would be possible to get this string interpreted as regular expression string on which a space is interpreted as space and not as OR expression. It is possible to specify multiple search strings using multiple times /C:.

My recommendation on using FINDSTR: Use always either /L or /R to make it clear for FINDSTR and for every reader of the command line how FINDSTR should interpret the search string(s) specified with "..." or with /C:"...".

Mofi
  • 46,139
  • 17
  • 80
  • 143
1

I guess I'll throw my hat in too, since I'm not really a fan of looping through dir output and no one else is currently accounting for this script already having been run:

@echo off

set "dir=C:\Your\Root\Directory"
set "pfx=Seekret file "

setlocal enabledelayedexpansion
for /r "%dir%" %%A in (*.txt) do (
    set "txt=%%~nA"
    if not "!txt:~0,13!"=="%pfx%" ren "%%A" "%pfx%%%~nxA"
)

pause

for /r will loop recursively through all .txt files, set each one as parameter %%A (per iteration), set a variable txt as parameter %%A reduced to just its name (%%~nA), and then it compares the first 13 characters of the text file to your example prefix (which is 13 characters long when you include the space: Seekret file) - if they match the loop does nothing; if they do not match, the loop will rename %%A to include the prefix at the beginning. If you don't want it to be recursive, you can use for %%A in ("%dir%"\*.txt) do ( instead. Other than that, you'll just change !txt:~0,13! depending on what your prefix is or how many letters into a filename you want to check. You also don't have to set your directory and prefix variables, I just prefer to do so because it makes the block look cleaner - and it's easier to go back and change one value as opposed to every place that value occurs in a script.

Reference: for /r, ren, variable substrings

mael'
  • 453
  • 3
  • 7
  • Good solution with one minor limitation: *.txt files with one or more exclamation marks in file name are not correct processed because of enabled delayed environment variable expansion. But `!` in file names are very rare and so this code should work in almost all use cases. A case-insensitive string comparison would be perhaps also a good idea if a text file was not renamed by this batch code, but manually by a user who did not exactly type `Seekret file` on editing file name. – Mofi Jul 25 '19 at 20:10