Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create unique file name Windows batch

I have seen many posts about creating a unique filename from the naive %TIME% to the plausible (but insufficient) %RANDOM%. Using wmic os get localdatetime is much better, but it can still fail on multiple CPU/core machines. The following script will eventually fail when run in 5+ shells on a multple core machine.

@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION

FOR /L %%i IN (0, 1, 1000) DO (
    FOR /F "usebackq" %%x IN (`wmic os get localdatetime ^| find "."`) do (set MYDATE=%%x)
    ECHO MYDATE is now !MYDATE!
    IF EXIST testuniq_!MYDATE!.txt (
        ECHO FAILED ON !MYDATE!
        GOTO TheEnd
    )
    COPY NUL >testuniq_!MYDATE!.txt
)

:TheEnd
EXIT /B 0

Does anyone have a reliable way to create a unique file name in a shell script?

like image 798
lit Avatar asked Jan 06 '15 16:01

lit


2 Answers

Any system that relies on a random number can theoretically fail, even one using a GUID. But the world seems to have accepted that GUIDs are good enough. I've seen code posted somewhere that uses CSCRIPT JScript to generate a GUID.

There are other ways:

First I will assume that you are trying to create a unique temporary file. Since the file will be deleted once your process ends, all you must do is establish an exclusive lock on a resource whose name is a derivative of your temp file name. (actually I go in reverse - the temp name is derived from the locked resource name).

Redirection establishes an exclusive lock on the output file, so I simply derive a name from the time (with 0.01 second preciscion), and attempt to lock a file with that name in the user's temp folder. If that fails than I loop back and try again until I succeed. Once I have success, I am guaranteed to have sole ownership of that lock, and all derivitives (unless someone intentionally breaks the system).

Once my process terminates, the lock will be released, and a temp file with the same name could be reused later on. But the script normally deletes the temp file upon termination.

@echo off
setlocal

:getTemp
set "lockFile=%temp%\%~nx0_%time::=.%.lock"
set "tempFile=%lockFile%.temp"
9>&2 2>nul (2>&9 8>"%lockFile%" call :start %*) || goto :getTemp

:: Cleanup
2>nul del "%lockFile%" "%tempFile%"
exit /b


:start
:: Your code that needs the temp file goes here. This routine is called with the
:: original parameters that were passed to the script. I'll simply write some
:: data to the temp file and then TYPE the result.
>"%tempFile%" echo The unique tempfile for this process is "%tempfile%"
>>"%tempFile%" echo(%*
type "%tempFile%"
exit /b

Looping due to name collision should be rare unless you are really stressing your system. If so, you can reduce the chance of looping by a factor of 10 if you use WMIC OS GET LOCALDATETIME instead of %TIME%.


If you are looking for a persistent unique name, then the problem is a bit more difficult, since you cannot maintain the lock indefinitely. For this case I recommend the WMIC OS LocalDateTime approach, coupled with two checks for name collision.

The first check simply verifies the file does not already exist. But this is a race condition - two processes could make the check at the same time. The second check creates the file (empty) and establishes a temporary exclusive lock on it. The trick is to make sure that the lock is maintained for a period of time that is longer than it takes for another process to check if the file exists. I'm lazy, so I simply use TIMEOUT to establish a 1 second wait - way more than should be necessary.

The :getUniqueFile routine expects three arguments - a base name, an extension, and the variable name where the result is to be stored. The base name can include drive and path information. Any path information must be valid, otherwise the routine will enter an infinite loop. That issue could be fixed.

@echo off
setlocal

:: Example usage
call :getUniqueFile "d:\test\myFile" ".txt" myFile
echo myFile="%myFile%"
exit /b

:getUniqueFile  baseName  extension  rtnVar
setlocal
:getUniqueFileLoop
for /f "skip=1" %%A in ('wmic os get localDateTime') do for %%B in (%%A) do set "rtn=%~1_%%B%~2"
if exist "%rtn%" (
  goto :getUniqueFileLoop
) else (
  2>nul >nul (9>"%rtn%" timeout /nobreak 1) || goto :getUniqueFileLoop
)
endlocal & set "%~3=%rtn%"
exit /b

The above should be guaranteed to return a new unique file name for the given path. There is a lot of room for optimization to establish some command to execute during the lock check that takes "long enough" but not "too long"

like image 170
dbenham Avatar answered Sep 24 '22 02:09

dbenham


You could use certutil to base64 encode %date% %time% with a %random% seed like this:

@echo off
setlocal

:: generate unique ID string
>"%temp%\~%~n0.%username%.a" echo %username%%date%%time%%random%
>NUL certutil -encode "%temp%\~%~n0.%username%.a" "%temp%\~%~n0.%username%.b"
for /f "usebackq EOL=- delims==" %%I in ("%temp%\~%~n0.%username%.b") do set "unique_id=%%I"
del "%temp%\~%~n0.%username%.a" "%temp%\~%~n0.%username%.b"

echo %unique_id%

In case the same script is being run from the same directory by multiple users, I added %username% to the temp files to avoid further conflict. I suppose you could replace %random% with %username% for the same effect. Then you'd only get a conflict if a single user executes the same code block twice concurrently.

(Edit: added %username% as a seed for uniqueness.)

like image 28
rojo Avatar answered Sep 25 '22 02:09

rojo