Create unique file name Windows batch

2019-01-25 09:37发布

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?

8条回答
等我变得足够好
2楼-- · 2019-01-25 09:40

I would like to submit another method and find out if there are any holes is it. Please let me know. Thanks.

C:>title my shell a80en333xyb

C:>type getppid.ps1
(gwmi win32_process -Filter "processid='$pid'").ParentProcessId

C:>powershell getppid.ps1
2380

Or, to run it without creating a .ps1 file:

powershell -NoProfile -Command "(gwmi win32_process -Filter "processid=`'$pid`'").ParentProcessId"

To verify that the correct task is found, the title is set to an unlikely value and tasklist.exe is used to search for the value returned by getppid.ps1.

C:>tasklist /v | find "2380"
cmd.exe                       2380 RDP-Tcp#0                  7      8,124 K Running         PHSNT\pwatson                                           0:00:03 my shell a80en333xyb
查看更多
男人必须洒脱
3楼-- · 2019-01-25 09:45

I like Aacini's JScript hybrid solution. As long as we're borrowing from other runtime environments, how about using .NET's System.GUID with PowerShell?

@echo off
setlocal

for /f "delims=" %%I in ('powershell -command "[string][guid]::NewGuid()"') do (
    set "unique_id=%%I"
)

echo %unique_id%
查看更多
放荡不羁爱自由
4楼-- · 2019-01-25 09:45

A long time ago in a galax..newsgroup called alt.msdos.batch, a contributor insisted on posting a QBASIC solution to each issue raised, wrapped in a batch shell and claiming it was batch because it only used standard Microsoft-supplied software.

After a long time, he was cured of his errant ways, but I've been severely allergic to hybrid methods ever since. Sure - if it cures the problem, blah,blah - and sometimes there's no escaping that course (run batch silently, for instance.) Apart from that, I really don't want thebother of learning a new language or two...So that's why I eschew *script solutions. YMMV.

So - here's another approach...

@ECHO OFF
SETLOCAL
:rndtitle
SET "progtitle=My Batch File x%random%x"
TITLE "%progtitle%"
SET "underline="
SET "targetline="
FOR /f "delims=" %%a IN ('TASKLIST /v^|findstr /i /L /c:"======" /c:"%progtitle%"') DO (
 IF DEFINED underline (
  IF DEFINED targetline GOTO rndtitle
  SET "targetline=%%a"
 ) ELSE (SET "underline=%%a")
)

:: Here be a trap. The first column expands to fit the longest image name...
:pidloop
IF "%underline:~0,1%"=="=" SET "underline=%underline:~1%"&SET "targetline=%targetline:~1%"&GOTO pidloop
FOR /f %%a IN ("%targetline%") DO SET /a pid=%%a
ECHO %pid%

GOTO :EOF

Essentially, set the title to something random. Note that x%random%x is used, not simply %random% so that a search for "x123x" won't detect "x1234x".

Next, get a verbose tasklist, locating the line underlining the heading and the title. The first line returned by findstr will be the underline, the next will set targetline and any further returns indicate that the title is not unique, so go back, change it and try again until it is unique.

Finally, get the process ID which is in the second column, noting that the first is of unknown width (hence why the underline is grabbed.)

Result : this process's PID which must be unique and hence can be used as the basis for a unique tempfile name - build it in a directory reserved for the purpose.

查看更多
看我几分像从前
5楼-- · 2019-01-25 09:48

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.)

查看更多
何必那么认真
6楼-- · 2019-01-25 09:57

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"

查看更多
Ridiculous、
7楼-- · 2019-01-25 09:58

The Batch-JScript hybrid script below uses WSH's fso.GetTempName() method that was designed precisely for this purpose:

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

@echo off

for /F "delims=" %%a in ('cscript //nologo //E:JScript "%~F0"') do set "fileName=%%a"
echo Created file: "%fileName%"
goto :EOF

@end

var fso = new ActiveXObject("Scripting.FileSystemObject"), fileName;
do { fileName = fso.GetTempName(); } while ( fso.FileExists(fileName) );
fso.CreateTextFile(fileName).Close();
WScript.Echo(fileName);
查看更多
登录 后发表回答