Windows batch - nested variables and delayed expansion - windows

newbie here who would appreciate some help. I'm trying to generate to generate random file names (i.e. for each file in the source directory, a different random alphanumeric figure should be generated)
The function that generates the random alphanumeric name works but when the value is returned to the main routine, it remains the same across all the files. Not sure where the problem is but I suspect it has something to do with delayed expansion.
#Echo Off
Setlocal EnableDelayedExpansion
dir "D:/Source" /b > List.txt
FOR /F %%i in (List.txt) DO (
CALL:Alpha Numb
echo !Numb!
md D:\Output\%%~ni
rar a -v50M -hpabc123 -m0 -ep "D:\Output\%%~ni\!Numb!.rar" "D:\Source\%%i"
)
goto:eof
:Alpha
Setlocal EnableDelayedExpansion
Set _RNDLength=40
Set _Alphanumeric=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
Set _Str=%_Alphanumeric%987654321
:_LenLoop
IF NOT "%_Str:~18%"=="" SET _Str=%_Str:~9%& SET /A _Len+=9& GOTO :_LenLoop
SET _tmp=%_Str:~9,1%
SET /A _Len=_Len+_tmp
Set _count=0
SET _RndAlphaNum=
:_loop
Set /a _count+=1
SET _RND=%Random%
Set /A _RND=_RND%%%_Len%
SET _RndAlphaNum=!_RndAlphaNum!!_Alphanumeric:~%_RND%,1!
If !_count! lss %_RNDLength% goto _loop
REM Echo %_RndAlphaNum%
ENDLOCAL & SET "%~1=%_RndAlphaNum%"
Exit /b

Related

Batch get relative path from absolute path

How can I convert an absolute path to a relative path in batch? I have an absolute path to a directory A and a reference directory B, and I need the path to A relative to B. As example, the following batch script should print ..\other\somedir\.
setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
set relativePath=...
echo %relativePath%
I tried relativePath=!absolutePath:%referencePath%=!, but this yields the absolute path C:\Users\xyz\other\somedir\.
I need something similar to the python function os.path.relpath:
>>> os.path.relpath("C:\\Users\\xyz\\other\\somedir", "C:\\Users\\xyz\\project\\")
"..\\other\\somedir"
I need this because I have a batch file with command line arguments similar to the above file names. This batch file creates another batch file startup.bat which sets some environment variables and starts an application. The startup.bat may be called over network, so I have to use relative paths. With absolute paths, the environment variables would point to the files on the wrong machine.
Here is a quick'n'dirty hack:
#echo off
setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
set relativePath=
:LOOP
for /F "tokens=1 delims=\" %%a in ("%referencePath%") do (set ref=%%a)
for /F "tokens=1 delims=\" %%a in ("%absolutePath%") do (set rel=%%a)
if /i !ref!==!rel! (
set referencePath=!referencePath:%ref%\=!
set absolutePath=!absolutePath:%rel%\=!
goto LOOP
)
:RELLOOP
for /F "tokens=1 delims=\" %%a in ("%absolutePath%") do (
set absolutePath=!absolutePath:%%a\=!
set relativePath=!relativePath!..\
)
if not "%absolutePath%"=="" goto RELLOOP
set complRelPath=%relativePath%%referencePath%
echo !complRelPath!
This won't give you propper output if the folders are on different drives so you'll have to handle this special case yourself.
EDIT (comment): Well, this can't be that hard that you couldn't figure it out yourself. If / and \ are mixed (which is a bad idea - we are on Windows! Windows means \ in paths, UNIX etc. means / in paths) you should replace / by :
SET referencePath=%referencePath:/=\%
SET absolutePath=%absolutePath:/=\%
If the paths are equal, you have nothing to do so:
IF %referencePath%==%absolutePath% (
SET complRelPath=.\
GOTO WHATEVER
)
#ECHO OFF
setlocal enabledelayedexpansion
set referencePath=C:\Users\xyz\project\
set absolutePath=C:\Users\xyz\other\somedir\
FOR %%a IN ("%absolutepath%") DO FOR %%r IN ("%referencepath%.") DO (
SET "abspath=%%~pa"
SET "relativepath=!abspath:%%~pr=..\!"
)
echo %relativePath%
GOTO :EOF
It would be of assistance if you were to tell us what your desired output is. Telling us what the output of your current code is, and that implicitly that's not what you expect, and then how to obtain something using some other platform is not particularly helpful.
The problem is that you are attempting to replace a string containing a colon within a string contining a colon. cmd` gets confused as it doesn't know which colon of the three is which.
This solution is resticted, since it assumes that the part of the path to be removed is exactly the parent directory of referencepath. In the absence of more information, it's as far as I'm prepared to guess...
…and an example which leverages PowerShell:
#Echo Off
Set "referencePath=C:\Users\xyz\project"
Set "absolutePath=C:\Users\xyz\other\somedir"
Set "relativePath="
Set "_="
If /I Not "%CD%"=="%referencePath%" (Set "_=T"
PushD "%referencePath%" 2>Nul || Exit /B)
For /F "Delims=" %%A In ('
PowerShell -C "Resolve-Path -LiteralPath '%absolutePath%' -Relative"
') Do Set "relativePath=%%A"
If Defined _ PopD
If Defined relativePath Echo %relativePath%
Pause
This obviously only works with actual existing paths
Well, usually I strongly recommend not to do string manipulation on file or directory paths, because it is quite prone to failures. But for a task like this, which does not rely on existing paths, there appears no way around.
Anyway, for doing so in a reliable fashion, the following issues must be considered:
Windows uses case-insensitive paths, so do all path comparisons in such manner as well!
Avoid sub-string substitution, because it is troublesome with =-signs and a few other characters!
Ensure to properly resolve the provided paths, using the ~f-modifier of for-loop meta-variables! This ensures that the input paths are really absolute, they do not contain doubled separators (\\), and there are no sequences with . and .. (like abc\..\.\def, which is equivalent to def), that make comparison difficult.
Regard that paths may be provided in an ugly way, like with trailing \ or \., bad quotation (like abc\"def ghi"\jkl), or using wrong path separators (/ instead of \, which is the Windows standard).
Alright, so let us turn to a script that I wrote for deriving the common path and the relative path between two absolute paths, regarding all of the said items (see the rem comments):
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "referencePath=C:\Users\"xyz"\dummy\..\.\project"
set "absolutePath=C:\Users\"xyz"\other\\somedir\"
rem /* At first resolve input paths, including correction of bad quotes and wrong separators,
rem and avoidance of trailing separators (`\`) and also other unwanted suffixes (`\.`): */
set "referencePath=%referencePath:"=%"
set "absolutePath=%absolutePath:"=%"
for %%P in ("%referencePath:/=\%") do for %%Q in ("%%~fP.") do set "referencePath=%%~fQ"
for %%P in ("%absolutePath:/=\%") do for %%Q in ("%%~fP.") do set "absolutePath=%%~fQ"
rem // Initially clean up (pseudo-)array variables:
for /F "delims==" %%V in ('2^> nul ^(set "$ref[" ^& set "$abs["^)') do set "%%V="
rem // Split paths into their elements and store them in arrays:
set /A "#ref=0" & for %%J in ("%referencePath:\=" "%") do if not "%%~J"=="" (
set /A "#ref+=1" & setlocal EnableDelayedExpansion
for %%I in (!#ref!) do endlocal & set "$ref[%%I]=%%~J"
)
set /A "#abs=0" & for %%J in ("%absolutePath:\=" "%") do if not "%%~J"=="" (
set /A "#abs+=1" & setlocal EnableDelayedExpansion
for %%I in (!#abs!) do endlocal & set "$abs[%%I]=%%~J"
)
rem /* Determine the common root path by comparing and rejoining the array elements;
rem also build the relative path herein: */
set "commonPath=\" & set "relativePath=." & set "flag=#" & set /A "#cmn=#ref+1"
for /L %%I in (1,1,%#abs%) do (
if defined flag (
set "flag=" & if defined $ref[%%I] (
setlocal EnableDelayedExpansion
if /I "!$abs[%%I]!"=="!$ref[%%I]!" for %%J in ("!commonPath!\!$ref[%%I]!") do (
endlocal & set "commonPath=%%~J" & set "flag=#" & set /A "#cmn=%%I+1"
)
)
)
if not defined flag (
setlocal EnableDelayedExpansion & for %%J in ("!relativePath!\!$abs[%%I]!") do (
endlocal & set "relativePath=%%~J"
)
)
)
rem // Complete the relative path by preceding enough level-up (`..`) items:
for /L %%I in (%#cmn%,1,%#ref%) do (
setlocal EnableDelayedExpansion & for %%J in (".\.!relativePath!") do (
endlocal & set "relativePath=%%~J"
)
)
set "relativePath=%relativePath:*\=%" & if "%commonPath:~-1%"==":" set "commonPath=%commonPath%\"
set "commonPath=%commonPath:~2%" & if not defined commonPath set "relativePath=%absolutePath%"
rem // Eventually return results:
set "referencePath"
set "absolutePath"
set "commonPath" 2> nul || echo commonPath=
set "relativePath"
endlocal
exit /B
Note that this approach does not support UNC paths.
Though I understand exactly what you want to do, I do not like this method.
Instead, why not use your environment path, where we will search for the relevant file in path.
#echo off
for %%g in ("bin\startup-batch.cmd") do #set "pathTobat=%%~$PATH:g"
echo "%pathTobat%"
The solution of #MichaelS is good, but has problems:
If subdirectories have same or similar names as their parents, it doesn't work.
setlocal without endlocal
Same-path condition not in script
No command-line-interface (CLI)
Improved version follows: (save as setComRelPath.bat) (you need strlen.cmd from https://ss64.com/nt/syntax-strlen.html)
#echo off
rem Usage: call setCompRelPath.bat C:\A\B C:\i\am\here
rem echo "%compRelPath%"
rem
rem Alternernatively,
rem set position_target=D:\44_Projekte\imagine\
rem set position_now=%CD%\
rem call setCompRelPath.bat
rem echo "%compRelPath%"
if not [%1]==[] set position_target=%1
if not [%2]==[] set position_now=%2
set __absolutePath=%position_now%
set __referencePath=%position_target%
if %__referencePath%==%__absolutePath% (
set complRelPath=.\
exit /b
)
rem echo __referencePath=%__referencePath%
rem echo __absolutePath=%__absolutePath%
set relativePath=
:LOOP
for /F "tokens=1 delims=\" %%a in ("%__referencePath%") do (set ref=%%a)
for /F "tokens=1 delims=\" %%a in ("%__absolutePath%") do (set rel=%%a)
if /i not %ref%==%rel% goto RELLOOP
call strlen.cmd "x%ref%" _strlen
call set __referencePath=%%__referencePath:~%_strlen%%%
call set __absolutePath=%%__absolutePath:~%_strlen%%%
rem echo abs^> %__absolutePath%
goto LOOP
:RELLOOP
for /F "tokens=1 delims=\" %%a in ("%__absolutePath%") do call :SUB_relpath %%a
if not "%__absolutePath%"=="" goto RELLOOP
goto FIN
:SUB_relpath
set ARG=%1
call strlen.cmd "x%ARG%" _strlen
rem echo abs: %__absolutePath% // ARG=%ARG% // rel: %relativePath%
call set __absolutePath=%%__absolutePath:~%_strlen%%%
set relativePath=%relativePath%..\
exit /b
:FIN
set compRelPath=%relativePath%%__referencePath%
If you want to see it functioning, uncomment the echo lines

Second latest folder in a Directory

I want a batch file which will find out which is the second latest folder created/modified in a directory.
I found this article but no matter how much i tried i could not understand how it works
#echo off
set "root_dir=c:\somewhere"
pushd "%root_dir%"
set "bl1="
set "bl2="
setlocal enableDelayedExpansion
for /f "tokens=* delims=" %%# in ('dir /b /a:-d /o:d') do (
set "bl2=!bl1!"
set "bl1=%%#"
)
echo %bl2%
endlocal
If i use it as it is then i can get the second latest folder but this script is supposedly able to get which ever latest folder you need , be it 1st or nth.
Could someone please tell me what modifications need to be done to the script to accomplish that. Also how exactly this script works
In your approach, the latest folder is already available in variable bl1; add echo %bl1% at the end before endlocal to display it. Retrieving the nth folder is simply not possible in a flexible way with that script as you would need to define another variable (say bl3, bl4,..., bln) within the loop.
However, you could reverse the sort order of the output of the dir command by changing the /O option, so it returns the latest (most recent) item first. Then let an index number count the iterations of the loop, and if that index equals the predefined number n, store the currently iterated item:
#echo off
setlocal EnableDelayedExpansion
rem // Define N here to get Nth-latest folder:
set /A LATEST=2
set /A INDEX=0
for /F "eol=| delims=" %%# in ('dir /B /A:D /O:-D "C:\somewhere"') do (
set /A INDEX+=1
if !INDEX! EQU !LATEST! (
set "ITEM=%%#"
)
)
if defined ITEM echo %LATEST%th-latest folder: %ITEM%
endlocal
exit /B
Update
Here is a modified script with the following improvements:
Exclamation marks ! in folder names are no longer lost due to toggling delayed expansion;
the target directory can be provided as the first command line argument; if omitted, the current directory is used;
the number n can be given as the second command line argument; if omitted, the user is prompted for it (this addresses elzooilogico's comment); n defaults to 1 for empty input;
the display output is improved to avoid something weird like 1th-latest, 2th-latest and 3th-latest; instead, The latest, 2nd-latest and 3rd-latest is returned, respectively;
So this is the code:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem /* define location path and folder pattern as 1st command line argument;
rem /* Define number N as 2nd command line argument to get Nth-latest folder. */
set "LATEST=%~2"
set /A LATEST+=0
if %LATEST% LEQ 0 (set /P LATEST="Enter N [1]: " & set /A LATEST+=0)
if %LATEST% LEQ 0 set /A LATEST=1
set /A INDEX=0
for /F "eol=| delims=" %%# in ('dir /B /A:D /O:-D "%~1"') do (
set /A INDEX+=1
setlocal EnableDelayedExpansion
if !INDEX! EQU !LATEST! (
endlocal
set "ITEM=%%#"
goto :SKIP & rem // break loop after having retrieved Nth-latest folder;
) else endlocal
)
:SKIP
setlocal EnableDelayedExpansion
if defined ITEM (
if %LATEST% EQU 1 (echo The latest file: !ITEM!) else (
if %LATEST% EQU 2 (echo 2nd-latest file: !ITEM!) else (
if %LATEST% EQU 3 (echo 3rd-latest file: !ITEM!) else (
echo %LATEST%th-latest file: !ITEM!)))
)
endlocal
endlocal
exit /B
To achieve a similar result as with the simple script on top of this answer, you need to call this script by the following command line, supposing it has been saved as Nth-latest.bat:
Nth-latest.bat "C:\somewhere" 2

batch file calling to function fail

been trying to figure how to make the random string generation as a function, then call to function to return new random for each for loop.
but unable to make it work...
blank of idea, batch programming seems lot more complicated than web..
#echo off
GOTO :MAIN
:TestFunc
set orig=%1
set %~2=%random%
goto :eof
:MAIN
for %%a in (C:\folder\*.png) do (
set /a count+=1
set "fname=%%~a"
setlocal enabledelayedexpansion
set param_to_function=LetItBeA
call :TestFunc %param_to_function% return_value
set random=%return_value%
echo !random!
echo !fname!
ren "!fname!" img_%date:~10,4%-%date:~4,2%-%date:~7,2%_%HR%%time:~3,2%-!random!.png
endlocal
)
goto :eof
Original question answered (before edit). Explanation:
Use for /F against dir /b as unlike for /F, for starts parsing files immediately so it could get a renamed file again and again...
File renaming treated in a subroutine with test on file existence before ren.
Note endlocal&set "%1=%_RndAlphaNum%"&goto :eof tricky part how-to return a value to a variable (parameter) passed by reference.
ren command echoed merely for debugging purposes.
Resources (required reading):
An A-Z Index of the Windows CMD command line
Windows CMD Shell Command Line Syntax
The script:
#ECHO OFF
SETLOCAL enableextensions disabledelayedexpansion
Set /A "_RNDLength=6"
Set "_Alphanumeric=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
set "_folder=C:\folder"
for /F "delims=" %%a in ('dir /B "%_folder%\*.png"') do (
set /a count+=1
set "fname=%%~nxa"
call :renameFile
)
ENDLOCAL
goto :eof
:renameFile
call :getRandomString _RndANum
set "newFileName=img_%date:~10,4%-%date:~4,2%-%date:~7,2%_%HR%%time:~3,2%-%_RndANum%.png"
if exist "%_folder%\%newFileName%" goto :renameFile
echo ren "%_folder%\%fname%" "%newFileName%"
goto :eof
:getRandomString
rem usage
rem call :getRandomString varname
rem %1 = a variable name (pass by reference)
Setlocal EnableDelayedExpansion
Set "_Str=%_Alphanumeric%987654321"
:_LenLoop
IF NOT "%_Str:~18%"=="" (
SET _Str=%_Str:~9%
SET /A "_Len+=9"
GOTO :_LenLoop
)
SET _tmp=%_Str:~9,1%
SET /A _Len=_Len+_tmp
Set _count=0
SET _RndAlphaNum=
:_loop
Set /a _count+=1
SET _RND=%Random%
Set /A _RND=_RND%%%_Len%
SET _RndAlphaNum=!_RndAlphaNum!!_Alphanumeric:~%_RND%,1!
If !_count! lss %_RNDLength% goto _loop
rem do not split next line
endlocal&set "%1=%_RndAlphaNum%"&goto :eof

Mass renaming of files?

I have a folder with 46 different text files, from 001.txt to 046.txt and I need to add another file to, say, spot 30. Is there a ways to rename all the files from 030.txt until 046.txt up by one number, so there is an empty spot for the new 030.txt? (Operating on Windows 7)
You can use PowerShell, which is build-in in Windows 7:
46..30|Rename-Item -Path {'{0:000}.txt'-f$_} -NewName {'{0:000}.txt'-f($_+1)}
#ECHO Off
SETLOCAL
SET "sourcedir=U:\sourcedir\t w o"
SET "insertat="
SET /p "insertat=Insert at which number ? "
IF NOT DEFINED insertat GOTO :EOF
SET /a insertat1=1%insertat%
SET /a howmany=1
SET /p "howmany=Insert How many ? [%howmany%]"
IF "%howmany%"=="0" GOTO :EOF
FOR /f "delims=" %%a IN (
'dir /b /o-n /a-d "%sourcedir%\*.txt" '
) DO (
CALL :isnum %%~na
IF NOT DEFINED notnumber SET /a maxnum=1%%~na&GOTO insert
)
ECHO maxnum NOT found
GOTO :eof
:insert
SET /a newnum=maxnum + howmany
IF EXIST "%sourcedir%\%maxnum:~1%.txt" ECHO(REN "%sourcedir%\%maxnum:~1%.txt" %newnum:~1%.txt
SET /a maxnum -=1
IF %maxnum% GEQ 1%insertat% GOTO insert
GOTO :EOF
:: Determine whether %* is purely numeric
:isnum
SET "notnumber=%~2"
IF DEFINED notnumber GOTO :EOF
SET "notnumber=9%~1"
FOR /l %%z IN (0,1,9) DO CALL SET "notnumber=%%notnumber:%%z=%%"
GOTO :eof
I may as well post this regardless.
It automatically locates the highest filenumber and allows any number of slots to be inserted (default of 1 for Enter)
You would need to change the setting of sourcedir to suit your circumstances.
The required REN commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO(REN to REN to actually rename the files.
Here is a simple pure batch solution
#echo off
setlocal enableDelayedExpansion
set increment=1
set "start=030"
for /f %%F in (
'dir /b /a-d /o-n ???.txt^|findstr /rx "[0-9][0-9][0-9]\.txt"'
) do if "%%~nF" geq "%start%" (
set /a new=1%%~nF+increment
ren %%F !new:~1!%%~xF
)

How do I increment a folder name using Windows batch?

I've got a batch script that creates a folder named New_Folder and a few subdirectories and files within. Currently, if I need to create multiple New_Folders I have to rename each New_Folder created by the batch before I can run it again and create a new one. What I'd like to do is have the batch check and see if New_Folder already exists, and if so, to increment New_Folder by a number. So I'd have New_Folder, New_Folder1, New_Folder2, and so on.
How would I go about doing this? The solutions I've seen for incrementing things in batch scripts don't seem to apply to my situation, and I don't know anything about batch scripting beyond what I've copy/pasted for my own code.
Here is a solution that will always work, even if there are gaps in the numbers. The folder number will always be 1 greater than the current max number.
#echo off
setlocal enableDelayedExpansion
set "baseName=New_Folder"
set "n=0"
for /f "delims=" %%F in (
'2^>nul dir /b /ad "%baseName%*."^|findstr /xri "%baseName%[0-9]*"'
) do (
set "name=%%F"
set "name=!name:*%baseName%=!"
if !name! gtr !n! set "n=!name!"
)
set /a n+=1
md "%baseName%%n%"
with that you will be able to count the number of occurence of "New_Folder*" and create one with the next number.
#echo off
set /a count=0
for /d %%d in (New_Folder*) do (
set /a count+=1
)
set /a count+=1
mkdir New_Folder%count%
Note that the best way would be to find the largest number at the end of New_Folder, but Windows Batch is very limitative and I'm a Linux guy!
EDIT : After about one hour of googling and testing :
#echo off
setlocal EnableDelayedExpansion
set max_number=0
for /d %%d in (New_Folder*) do (
set current_directory=%%~nxd
call:StrLength name_length !current_directory!
call:Substring directory_number,!current_directory!,10,!name_length!
if !directory_number! gtr !max_number! (
set max_number=!directory_number!
)
)
set /a max_number+=1
mkdir New_Folder%max_number%
:Substring
::Substring(retVal,string,startIndex,length)
:: extracts the substring from string starting at startIndex for the specified length
SET string=%2%
SET startIndex=%3%
SET length=%4%
if "%4" == "0" goto :noLength
CALL SET _substring=%%string:~%startIndex%,%length%%%
goto :substringResult
:noLength
CALL SET _substring=%%string:~%startIndex%%%
:substringResult
set "%~1=%_substring%"
GOTO :EOF
:StrLength
::StrLength(retVal,string)
::returns the length of the string specified in %2 and stores it in %1
set #=%2%
set length=0
:stringLengthLoop
if defined # (set #=%#:~1%&set /A length += 1&goto stringLengthLoop)
::echo the string is %length% characters long!
set "%~1=%length%"
GOTO :EOF
Note, the command line return me an error "The syntax of the command is incorrect." but everything works so I'll let you find why... New folder is created regardless of the order of directories or if they start at 1 or not :) Hope you'll enjoy!
This solution find the largest numbered name, and create the next one to it:
#echo off
for /d %%d in (New_Folder*) do set lastFolder=%%d
set /A nextFolder=%lastFolder:*New_Folder=% + 1
mkdir New_Folder%nextFolder%
EDIT: Previous solution doesn't correctly get the last numbered folder, but the next one is correct:
#echo off
setlocal EnableDelayedExpansion
set lastFolder=0
for /d %%d in (New_Folder*) do (
set folder=%%d
set folder=!folder:New_Folder=!
if not defined folder set folder=0
if !folder! gtr !lastFolder! set lastFolder=!folder!
)
set /A nextFolder=lastFolder+1
mkdir New_folder%nextFolder%

Resources