Creatively stupid ways to rename files with spaces to underscores

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Squashman
Expert
Posts: 4465
Joined: 23 Dec 2011 13:59

Creatively stupid ways to rename files with spaces to underscores

#1 Post by Squashman » 20 Jun 2018 15:44

This is really just for shits and giggles. By no means would I ever use this in a production environment. My initial intent was to do this with no set or goto commands. My original flawed logic made me think that renaming any file with spaces to underscores would loop the file back into the `FOR` commands index but that didn't seem to be the case. So I cheated and used a GOTO.

Code: Select all

@echo off

:loop
FOR %%G IN ("* *.txt") DO (
	FOR /F "tokens=1* delims= " %%H IN ("%%~G") DO (
		IF NOT "%%~I"=="" (
			rename "%%~G" "%%~H_%%~I"
			goto loop
		)
	)
)

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Creatively stupid ways to rename files with spaces to underscores

#2 Post by dbenham » 21 Jun 2018 13:16

Interesting challenge.

I've succeeded in surpassing your original requirements :)

Here is a command line one liner that does not use any true environment variable, though it does use the dynamic pseudo environment variable %cmdcmdline%. Since it is command line, it obviously cannot use GOTO.

Code: Select all

cmd /c "@(call &for %F in ("* *.txt") do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"=="" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"
There are a couple odd restrictions with the above that I do not understand:

1) If the command does not rename any files, then the following unfortunate error message is generated, though I haven't seen any negative impact beyond the message

Code: Select all

'%cmdcmdline%' is not recognized as an internal or external command,
operable program or batch file.
2) The command does not loop properly if the file mask is"*"or "*.*"
It only works properly if the file mask has some restriction. For example, "*.txt" works, as does "* *"

Obviously the code could be adapted for use in a batch file.

renSpaceTo_.bat

Code: Select all

@cmd /c "(call &for %%F in (%*) do @for /f "tokens=1* delims= " %%A in ("%%F") do @if not "%%B"=="" (ren "%%F" "%%A_%%B"&call))&if not errorlevel 1 %%cmdcmdline%%"
Now the code is parametized, and %* allows you to specify multiple file masks.
For example, the following works perfectly without error as long as at least one file is renamed

Code: Select all

renSpaceTo_ *.bat *.exe
Next I thought, why not make a DOSKEY macro variant.

I first tried the obvious:

Code: Select all

doskey renSpaceTo_=cmd /c "@(call &for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"=="" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"
But there is a mysterious quoting anomaly that drops a quote from the first IF comparison.

Code: Select all

C:\test>doskey /macros
renSpaceTo_=cmd /c "@(call &for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"==" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"
I managed to find a solution, substitute 3 quotes for the 1 quote that was getting dropped. But I have no idea why this works.

Code: Select all

doskey renSpaceTo_=cmd /c "@(call &for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"=="""" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"
$* works just like the batch %*, so the macro also supports multiple file masks.

Of course both the batch file and macro have the same restrictions as the original command one liner.

I'm curious if anyone can figure out the reason behind the restrictions, or why the macro requires the 2 extra quotes.


Dave Benham

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: Creatively stupid ways to rename files with spaces to underscores

#3 Post by aGerman » 21 Jun 2018 13:29

Squashman wrote:
20 Jun 2018 15:44
made me think that renaming any file with spaces to underscores would loop the file back into the `FOR` commands index
It's quite likely that the FOR loop was implemented using FindFirstFile and FindNextFile. These functions work with file handles and won't process the same file twice. Also the order of the found files isn't necessarily alphabetic. It's rather depending on the file system. E.g. on FAT file systems the files are enumerated in the order they have been added to the FAT file table.

Steffen

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Creatively stupid ways to rename files with spaces to underscores

#4 Post by dbenham » 21 Jun 2018 13:44

Actually the simple FOR statement uses some form of buffering. If all of the matching files fit within the buffer, then newly appearing file names will never be processed. But if there are a lot of files, then the first set of files that fit in the buffer are processed. Then when the next set of files is loaded into the buffer, newly appearing file names may be included, depending on the sort order.

Dave Benham

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: Creatively stupid ways to rename files with spaces to underscores

#5 Post by aGerman » 21 Jun 2018 13:57

Interesting! I tried this code

Code: Select all

@echo off &setlocal
for /l %%i in (1 1 5000) do >"%%i.txt" type nul
for %%i in (*.txt) do (
  echo ren "%%i" "X%%i"
  ren "%%i" "X%%i"
)
pause
on an NTFS formatted drive and it doesn't stop to rename the files repeatedly.
I'll try if the API functions already behave the same ...

Steffen

Squashman
Expert
Posts: 4465
Joined: 23 Dec 2011 13:59

Re: Creatively stupid ways to rename files with spaces to underscores

#6 Post by Squashman » 21 Jun 2018 14:24

dbenham wrote:
21 Jun 2018 13:44
Actually the simple FOR statement uses some form of buffering. If all of the matching files fit within the buffer, then newly appearing file names will never be processed. But if there are a lot of files, then the first set of files that fit in the buffer are processed. Then when the next set of files is loaded into the buffer, newly appearing file names may be included, depending on the sort order.

Dave Benham
I was originally just testing with two files that were named the same with multiple spaces except for a 1 and 2 at the end of the base file name. The file1 would get two of the spaces changed but file2 would only get one space changed.

Squashman
Expert
Posts: 4465
Joined: 23 Dec 2011 13:59

Re: Creatively stupid ways to rename files with spaces to underscores

#7 Post by Squashman » 21 Jun 2018 14:38

I gave the doskey macro a try.

This was my results.

Code: Select all

C:\Users\Squashman\spaces>dir /b
fu bar 1.txt
s p aces.txt

C:\Users\Squashman\spaces>renSpaceTo_ *.txt

C:\Users\Squashman\spaces>dir /b
fu_bar_1.txt
s_p aces.txt

C:\Users\Squashman\spaces>doskey /macros
renSpaceTo_=cmd /c "@(call &for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"=="" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: Creatively stupid ways to rename files with spaces to underscores

#8 Post by aGerman » 21 Jun 2018 14:48

This C code behaves the same as my second loop above.

Code: Select all

// addx.c

#include <stdio.h>
#include <string.h>
#include <windows.h>

int main(void)
{
  wchar_t buf[MAX_PATH] = {'X'};
  WIN32_FIND_DATAW finddata = {0};
  HANDLE f = FindFirstFileW(L"*.txt", &finddata);
  if (f == INVALID_HANDLE_VALUE)
    return 1;

  do
  {
    wcsncpy(&buf[1], finddata.cFileName, MAX_PATH - 1);
    wprintf(L"ren %s %s\n", finddata.cFileName, buf);
    MoveFileExW(finddata.cFileName, buf, MOVEFILE_REPLACE_EXISTING);
  } while (FindNextFileW(f, &finddata) != FALSE);

  FindClose(f);
  return 0;
}
Obviously I was wrong with my assumption. There is no buffer for several file names in this code (both buffers finddata.cFileName and buf are repeatedly overwritten with the found name and the new name). This means that the buffering of the list will already be done somewhere in the implementation of the API.

Steffen

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Creatively stupid ways to rename files with spaces to underscores

#9 Post by dbenham » 21 Jun 2018 14:56

Squashman wrote:
21 Jun 2018 14:38
I gave the doskey macro a try.

This was my results.

Code: Select all

C:\Users\Squashman\spaces>dir /b
fu bar 1.txt
s p aces.txt

C:\Users\Squashman\spaces>renSpaceTo_ *.txt

C:\Users\Squashman\spaces>dir /b
fu_bar_1.txt
s_p aces.txt

C:\Users\Squashman\spaces>doskey /macros
renSpaceTo_=cmd /c "@(call &for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%F") do @if not "%B"=="" (ren "%F" "%A_%B"&call))&if not errorlevel 1 %^cmdcmdline%"
:shock: That is weird :?
I set up the same test, and it works fine for me. I only get your result if I use renSpaceTo_ *

But I have improved versions that use only one level of CMD /C, without need of %CMDCMDLINE%. These versions eliminate the restrictions, and I suspect they will work for you as well.

renSpaceTo_.bat

Code: Select all

@cmd /c "for /l %%. in () do @(call&for %%F in (%*) do @for /f "tokens=1* delims= " %%A in ("%%~F") do @if not "%%B"=="" (ren "%%~F" "%%A_%%B"&&call ))&if errorlevel 1 exit 0"
macro version

Code: Select all

doskey renSpaceTo_=cmd /c "for /l %. in () do @(call&for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%~F") do @if not "%B"=="""" (ren "%~F" "%A_%B"&&call ))&if errorlevel 1 exit 0"
EDIT
I forgot that REN always clears the ERRORLEVEL upon success. So the code can be simplified as follows:

renSpaceTo_.bat

Code: Select all

@cmd /c "for /l %%. in () do @(call&for %%F in (%*) do @for /f "tokens=1* delims= " %%A in ("%%~F") do @if not "%%B"=="" ren "%%~F" "%%A_%%B")&if errorlevel 1 exit 0"
macro version

Code: Select all

doskey renSpaceTo_=cmd /c "for /l %. in () do @(call&for %F in ($*) do @for /f "tokens=1* delims= " %A in ("%~F") do @if not "%B"=="""" ren "%~F" "%A_%B")&if errorlevel 1 exit 0"

Dave Benham

penpen
Expert
Posts: 1991
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Creatively stupid ways to rename files with spaces to underscores

#10 Post by penpen » 21 Jun 2018 17:39

aGerman wrote:
21 Jun 2018 14:48
This means that the buffering of the list will already be done somewhere in the implementation of the API.
This is right.
Typically the content of a directory is stored in a B-tree. When accessing an existing file the node containig this file is associated with the file handle. This copied B-tree node doesn't change even if the orginal B-tree node changes. So you could get all unexpected behaviour:
- a file (out of the copy of the actual node) is moved to the actual node, and never will be processed by the for loop (or your c-program),
- a file within the copy of the actual node is moved to another unprocessed node, and will be processed twice
- ...

penpen

pieh-ejdsch
Posts: 239
Joined: 04 Mar 2014 11:14
Location: germany

Re: Creatively stupid ways to rename files with spaces to underscores

#11 Post by pieh-ejdsch » 22 Jun 2018 02:27

I also tinkered a bit.
I did not want to think about why I should make a goto out of the for loop.
That makes no sense - but that's exactly why it should turn here.
And well I admit it, I screwed up the loop with the delayed variable itself. - I was lazy -
and had typed this snippet on the smart phone - so not tested.
But somehow you also have to come across something like that.

I also assumed that the file pointer in the loop is updated. But apparently it succeeds and sometimes not. see for yourself:

Code: Select all

@echo off
setlocal
call :set.printErrorLine
ren . .
%ID-1:##==1%
set prompt=$g$s
set "X= "
set "Y=_"
set hide= @
set /a tests=1
md testdirRen
pushd testdirRen
for %%i in ("fil e 2 1 4 and so on with xx"
 "fi xy zu drxzbu z543 6gg hg feeev lll juz tre ghj lk 0"
 "test 3 4 5 6"
 "test 4 4 5 6"
 "test 5 4 5 6"
) do >%%i.txt echo
call :test1
popD
rd /s /q testdirRen
pause
exit /b

:test1
set /a loop#=0
%ID-1:##==2%
set "M="
call :rename1 %hide% rem rem rem rem rem rem
echo(
2>nul set /a Htest=1/(tests%%2) || set "hide=rem %hide%"
set /a tests+=1
( set "X=%Y%"
set "Y=%X%" )
if %tests% leq 14 goto :test1

:test2
set /a loop#=0
echo --- with Variable
call :rename2
set /a tests+=1
( set "X=%Y%"
set "Y=%X%" )
if %tests% leq 16 goto :test2

:test3
set /a loop#=0
echo --- with Parameter
call :rename3 "*%X%*.txt"
set /a tests+=1
( set "X=%Y%"
set "Y=%X%" )
if %tests% leq 18 goto :test3
exit /b

:rename3
set /a loop#+=1
if NOT exist "%~1" exit /b
for /f "tokens=1*delims=%X%" %%i in ("%~nx1") do ( echo loop# %loop#%  ren ^>^>^> "%%i%Y%%%j"
 ren "%~f1" "%%i%Y%%%j"
)
goto %0
exit /b


:rename1
set /a loop#+=1
%M% %1 echo --- in for /f -if exist "File*%X%*name" goto %0
%M% %2 echo --- in for /f -if exist "File*%X%*name" call %0 stackError "_" ^> " "
%M% %3 echo --- in for /f -call %0
%M% %4 echo --- in for loop -if exist "*%X%*" goto %0
%M% %5 echo --- in for loop -if exist "*%X%*" call %0
%M% %6 echo --- in for loop -call %0
%M% %7 echo --- behind for loop if exist "*%X%*" goto %0
set "M=:: "
(
 for %%i in ("*%X%*.txt") do ( 
  for /f "tokens=1*delims=%X%" %%j in ("%%~i") do (
   if "%%k" neq "" ( echo loop# %loop#% ^>^>^> %%j^%Y%%%k
    ren "%%i" "%%j%Y%%%k"
    if errorlevel 1 echo ERROR -- %%i  	%== when NOT leave the for stack this will happen ==%
    %1 if exist "%%j%Y%*%X%*%%~xk" goto %0	%== exit every loop ==%
    %2 if exist "%%j%Y%*%X%*%%~xk" call %0 %*	%== leave NOT the for stack ==%
    %3 call %0 %*					%== leave the FOR stack ==%
  ))
  %4 if exist "*%X%*.txt" goto %0
  %5 if exist "*%X%*" call %0 %*
  %6 call %0 %*
 )
 echo END Loop# %loop#%
)
%7 if exist "*%X%*.txt" goto %0 %== get as rounds as max rename sign in filename ==%
exit /b

:rename2
setlocal enabledelayedexpansion
for %%i in ("*%X%*.txt") do ( set "filename=%%i"
 echo ren ^>^>^> "!filename:%X%=%Y%!"
 ren "%%i" "!filename:%X%=%Y%!"
)
exit /b


:set.printErrorLine
 rem Read the fullline BEFORE into Variables with LineNumber; do something ... 
 rem  usage: command 1 line before
 rem         %%ID-1:##==uniqueString%% [& command %%L ... %%M ...]
set ID-1= @ if errorlevel 1 for /f "usebackQtokens=1*delims=:" %%L in (^
 `cmd /v /c ^"findstr /nrc:"^!CLString^!ID-1:.#^=^##%%" "%~f0"^"`) do ^>^&2 echo BatchLine: %%L -- %%M 
for %%L in (^"^
%== create CR LF searchstring ==%
) do for /f %%C in ('copy /z "%~f0" nul') do set "CLString=..*%%C*%%~L.*"
exit /b
Now the question arises why with the one call and the other not.

Phil

Post Reply