19

I am trying to create the path to a temporary file to be used in a batch file.

There are the environment variables %TEMP% and %TMP% to get the temporary directory for the current user. But how to build a file name that does surely not yet exist?

Of course I can use the built-in variable %RANDOM% and create something like bat~%RANDOM%.tmp, but this method does not ensure that the file is currently inexistent (or that it will be created coincidentally by another application, before I first create it on disk and write to it) -- although this all is very unlikely.

I know I could just reduce the probability of such collisions by appending also %DATE%/%TIME%, or by just adding multiple %RANDOM% instances, but this is not what I want...

Note: According to this post, there is a method in .NET (Path.GetTempFileName()) which does exactly what I am asking for (besides the wrong programming language obviously).

Community
  • 1
  • 1
aschipfl
  • 33,626
  • 12
  • 54
  • 99

3 Answers3

16

Try next code snippet:

@echo off
setlocal EnableExtensions

rem get unique file name 
:uniqLoop
set "uniqueFileName=%tmp%\bat~%RANDOM%.tmp"
if exist "%uniqueFileName%" goto :uniqLoop

or create procedures

  • :uniqGet: create a file of a fix filename template (bat~%RANDOM%.tmp in your case);
  • :uniqGetByMask: create a file of a variable filename template. Note quadrupled percent signs of %random% reference in a procedure call: prefix%%%%random%%%%suffix.ext. Also note advanced usage: CALLing internal commands in call set "_uniqueFileName=%~2" inside the procedure.

The code could be as follows:

@ECHO OFF
SETLOCAL enableextensions
call :uniqGet uniqueFile1 "%temp%"
call :uniqGet uniqueFile2 "%tmp%"
call :uniqGet uniqueFile3 d:\test\afolderpath\withoutspaces
call :uniqGet uniqueFile4 "d:\test\a folder path\with spaces"

call :uniqGetByMask uniqueFile7 d:\test\afolder\withoutspaces\prfx%%%%random%%%%sffx.ext
call :uniqGetByMask uniqueFile8 "d:\test\a folder\with spaces\prfx%%%%random%%%%sffx.ext"

set uniqueFile

pause

goto :continuescript
rem get unique file name procedure
rem usage: call :uniqGetByMask VariableName "folderpath\prefix%%%%random%%%%suffix.ext"
rem parameter #1=variable name where the filename save to
rem parameter #2=folder\file mask
:uniqGetByMask
rem in the next line (optional): create the "%~dp2" folder if does not exist
md "%~dp2" 2>NUL
call set "_uniqueFileName=%~2"
if exist "%_uniqueFileName%" goto :uniqGetByMask
set "%~1=%_uniqueFileName%"
rem want to create an empty file? remove the `@rem` word from next line 
@rem type nul > "%_uniqueFileName%"
exit /B

goto :continuescript
@rem get unique file name procedure
@rem usage: call :uniqGet VariableName folder
@rem parameter #1=variable name where the filename save to
@rem parameter #2=folder where the file should be about
:uniqGet
@rem in the next line (optional): create the "%~2" folder if does not exist 
md "%~2" 2>NUL
set "_uniqueFileName=%~2\bat~%RANDOM%.tmp"
if exist "%_uniqueFileName%" goto :uniqGet
set "%~1=%_uniqueFileName%"
@rem want to create empty file? remove the `@rem` word from next line 
@rem type nul > "%_uniqueFileName%"
exit /B

:continueScript

Output:

==>D:\bat\SO\32107998.bat
uniqueFile1=D:\tempUser\me\bat~21536.tmp
uniqueFile2=D:\tempUser\me\bat~15316.tmp
uniqueFile3=d:\test\afolderpath\withoutspaces\bat~12769.tmp
uniqueFile4=d:\test\a folder path\with spaces\bat~14000.tmp
uniqueFile7=d:\test\afolder\withoutspaces\prfx26641sffx.ext
uniqueFile8=d:\test\a folder\with spaces\prfx30321sffx.ext
Press any key to continue . . .
JosefZ
  • 28,460
  • 5
  • 44
  • 83
  • Problem is, %RANDOM% is generated as a function from current time (in seconds), meaning that your code will be in a loop for 1 second when you need a second file. – Sergei Aug 31 '17 at 08:43
  • 2
    @Sergei Nope. Running e.g. `echo %time%&>NUL (for /l %G in (1,1,120) do @call d:\bat\SO\32107998.bat)&cmd /V /C "echo !time!"&dir D:\test\afolderpath\withoutspaces|find "File(s)"` shows that **all** `720` files (= 6 * 120) are created during cca 3.5 seconds, repeatedly (now I have cca 1220 files of given template pattern in each testing directory). Read https://ss64.com/nt/syntax-random.html – JosefZ Aug 31 '17 at 14:31
  • When I'm running same command really fast I get this http://imgur.com/a/6SVmu – Sergei Aug 31 '17 at 15:50
  • @Sergei Sorry, I can't reproduce your results. Please ask a new question providing full context in a [mcve]. – JosefZ Aug 31 '17 at 16:01
  • @JosefZ, Sadly, there's no guarantee that this will always work. There's a chance that if called in a sufficiently tight loop that this will produce duplicates that won't yet have been created. The chances go up when you've got lots of calls to this, and slow disk access. The only way you can ensure that it's good is to include a lock file in the mechanism. It's not a bad first start but no replacement for the unix mktemp. – UKMonkey Nov 16 '17 at 09:56
4

I suggest you one of two methods. The "technical approach" is to use JScript's FileSystemObject.GetTempName method. JScript is a programming language that comes pre-installed in all Windows versions from XP on, and its use in Batch via a "Batch-JScript" hybrid script is very simple:

@if (@CodeSection == @Batch) @then

@echo off
setlocal
for /F "delims=" %%a in ('CScript //nologo //E:JScript "%~F0"') do set "tempName=%%a"
echo Temp name: "%tempName%"
goto :EOF

@end

// JScript section

var fso = new ActiveXObject("Scripting.FileSystemObject");
WScript.Stdout.WriteLine(fso.GetTempName());

However, the simplest approach is to store a number in a data file and every time that you want a new name, get the number, increment it and store it back in the same file. This will work for "just" 2147483647 times!

rem Get next number
set /P "nextNum=" < NextNumber.txt
set /A nextNum+=1
echo %nextNum% > NextNumber.txt
set "tempName=File%nextNum%.txt"
echo Temp name: %tempName%
Aacini
  • 65,180
  • 12
  • 72
  • 108
2

Firstly, using a separate folder will significantly reduce the chances of other programs intruding. So lets store the temp file in a private folder that's really long and specific to prevent any name competition.

When generating the name, you could always use %random% and try generating a name which does not exist, however the more times this operation is use, the more ineffective it becomes. If you plan to use this process 10's of thousands of times as the random function is limited to 32000 (approximately) your program will spend forever generating random attempts.

The next best approach is to start a counter at one and increase it till you have an unused name. That way you can guarantee your program will eventually find a name in a reasonable amount of time, however again your files will pile up (which is never a good experience)

What some people do (and I would recommend for your situation) is combine these to processes to effectively cut the fat in selecting a name, while using a reliable method (the best of both worlds):

@echo off

:: Set Temp Folder Path
set "tp=%temp%\Temporary Name Selection Test"

:: File name will be XXXXXX_YY.txt 
:::: X's are randomly generated
:::: Y's are the incremented response to existing files
set x=%random%
set y=0
set "filename=Unexpected Error"

:loop
set /a y+=1 
set "filename=%tp%\%x%_%y%.txt"
if exist %filename% goto loop

:: At this point, filename is all good 
Echo %filename%
pause
Monacraft
  • 6,510
  • 2
  • 17
  • 29
  • I liked your approach. Simple and effective. I used a modified version to create random directories and worked like a charm – JACH Jan 28 '21 at 23:10