创建唯一文件名Windows批处理

Create unique file name Windows batch

我看过很多关于创建唯一文件名的帖子,从天真的 %TIME% 到貌似合理(但不够充分)的 %RANDOM%。使用 wmic os get localdatetime 好得多,但在多台 CPU/core 机器上它仍然会失败。当在多核机器上运行 5+ shells 时,以下脚本最终会失败。

@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

有没有人有可靠的方法在 shell 脚本中创建唯一的文件名?

您可以使用 certutil 对带有 %random% 种子的 %date% %time% 进行 base64 编码,如下所示:

@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%

为了防止多个用户从同一目录运行 访问同一个脚本,我在临时文件中添加了%username% 以避免进一步的冲突。我想您可以将 %random% 替换为 %username% 以获得相同的效果。那么只有当一个用户同时执行同一个代码块两次时才会发生冲突。

(编辑:添加 %username% 作为唯一性的种子。)

使文件内容成为对象,使用指针内存地址作为文件名的第一部分,使用 Rand() 作为第二部分。即使有多个实例,内存地址对于所有对象都是唯一的 运行.

@echo off
:: generate a tempfilename in %1 (default TEMPFILE)
:loop
set /a y$$=%random%+100000
set y$$=temp%y$$:~1,2%.%y$$:~-3%
if exist "%temp%\%y$$%" goto loop
SET "y$$=%temp%\%y$$%"&copy nul "%temp%\%y$$%" >nul 2>NUL
:: y$$ now has full tempfile name
if "%1"=="" (set "tempfile=%y$$%") else (set "%1=%y$$%")

这是我的临时文件生成器的简化版本,用于在 %temp% 目录中创建一个名为 tempnn.nnn 的文件。

一旦创建了 tempnn.nnn,就可以很简单地为进程创建尽可能多的临时文件,方法是在 %y$$% 后附加一个后缀,例如 %y$$%.a 等。当然,这假定某些其他进程不会在不使用此过程的情况下随机创建文件名。

下面的 Batch-JScript 混合脚本使用了 WSH 的 fso.GetTempName() 方法,该方法正是为此目的而设计的:

@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);

理论上,任何依赖随机数的系统都可能失败,即使是使用 GUID 的系统也是如此。但世界似乎已经接受了 GUID 已经足够好了。我在某处看到使用 CSCRIPT JScript 生成 GUID 的代码。

还有其他方式:

首先,我假设您正在尝试创建一个唯一的临时文件。由于该文件将在您的进程结束后被删除,您所要做的就是在名称为您的临时文件名派生的资源上建立独占锁。 (实际上我反过来 - 临时名称是从锁定的资源名称派生的)。

重定向在输出文件上建立了独占锁,因此我只是从时间(精度为 0.01 秒)中导出一个名称,并尝试在用户的临时文件夹中锁定具有该名称的文件。如果失败,我会返回并重试,直到成功。一旦我成功了,我保证拥有那把锁和所有衍生品的唯一所有权(除非有人故意破坏系统)。

一旦我的进程终止,锁就会被释放,同名的临时文件可以在以后重用。但是脚本通常会在终止时删除临时文件。

@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

除非您真的对系统造成压力,否则由于名称冲突而导致的循环应该很少见。如果是这样,如果使用 WMIC OS GET LOCALDATETIME 而不是 %TIME%.

,则可以将循环的机会减少 10 倍

如果您正在寻找一个持久的唯一名称,那么这个问题就有点困难了,因为您不能无限期地维护锁。对于这种情况,我建议使用 WMIC OS LocalDateTime 方法,并结合两次名称冲突检查。

第一个检查只是验证文件不存在。但这是一个竞争条件——两个进程可以同时进行检查。第二次检查创建文件(空)并在其上建立临时排他锁。诀窍是确保锁的维护时间长于另一个进程检查文件是否存在所需的时间。我很懒惰,所以我只是使用 TIMEOUT 来建立 1 秒的等待时间——远远超出了必要的时间。

:getUniqueFile 例程需要三个参数 - 基本名称、扩展名和要存储结果的变量名。基本名称可以包括驱动器和路径信息。任何路径信息都必须有效,否则程序将进入死循环。该问题可以解决。

@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

以上应该保证return给定路径的新的唯一文件名。在需要 "long enough" 而不是 "too long"

的锁定检查期间建立一些要执行的命令有很大的优化空间

我喜欢 Aacini 的 JScript 混合解决方案。只要我们从其他运行时环境中借用,使用 .NET 的 System.GUID 和 PowerShell 怎么样?

@echo off
setlocal

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

echo %unique_id%

很久以前,在一个名为 alt.msdos.batch 的 galax..newsgroup 中,一位撰稿人坚持针对提出的每个问题发布 QBASIC 解决方案,打包成批处理 shell 并声称它是批处理的,因为它只使用标准 Microsoft-supplied 软件。

经过很长一段时间,他的错误方式被治愈了,但我从那时起就对混合方法严重过敏。当然 - 如果它解决了问题,等等,等等 - 有时无法逃避这门课程(例如,运行 默默地批处理。)除此之外,我真的不想学习一两种新语言的麻烦...所以这就是我避开 *script 解决方案的原因。 YMMV.

所以 - 这是另一种方法...

@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

基本上,将标题设置为随机的。请注意,使用的是 x%random%x,而不仅仅是 %random%,因此搜索 "x123x" 不会检测到 "x1234x".

接下来,获取详细的任务列表,找到标题和标题下划线的位置。 findstr返回的第一行是下划线,下划线设置为targetline,再往上returns表示标题不唯一,回去改一下再试,直到它是独一无二的。

最后,获取第二列中的进程 ID,注意第一列的宽度未知(因此下划线被抓住的原因。)

结果:此进程的 PID 必须是唯一的,因此可以用作唯一临时文件名称的基础 - 将其构建在为此目的保留的目录中。

我想再提交一个方法,看看有没有漏洞。请告诉我。谢谢

C:>title my shell a80en333xyb

C:>type getppid.ps1
(Get-WmiObject -Class Win32_Process -Filter "processid='$pid'").ParentProcessId

C:>powershell getppid.ps1
2380

或者,运行 它而不创建 .ps1 文件:

powershell -NoProfile -Command "(Get-WmiObject -Class Win32_Process -Filter "processid=`'$pid`'").ParentProcessId"

为了验证是否找到了正确的任务,将标题设置为不太可能的值,并使用 tasklist.exe 搜索 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

你可以试试 guid.bat

for /f "tokens=* delims={}" %%# in ('guid.bat') do set "unique=%%#"
echo %unique%

直接从控制台使用:

for /f "tokens=* delims={}" %# in ('guid.bat') do @set "unique=%#"

首先关于使用 PID 作为唯一文件名的一部分的一些说明:

当你只是寻找一个 parent PID 时,这可能是模棱两可的,因为当你 运行 在 for 循环或批处理文件中的调用中的代码总是一个新的 cmd.exe 已创建,您将获得此临时 cmd.exe 进程的 PID。

所以您应该使用批处理“root”cmd.exe 进程的 PID。 为了得到它 - 而不是在任务列表中按标题搜索,这很慢而且可能也有歧义,还有另一种方法。

您可以使用 Windows API 函数获取批“根”cmd.exe 进程的 PID:

  • GetConsoleWindow 提供控制台window句柄
  • GetWindowThreadProcessId 使用此控制台提供 PID window handle

您可以使用 PowerShell 运行 这些 Windows API 函数:

Add-Type -MemberDefinition @"
  // HWND WINAPI GetConsoleWindow(void)
  [DllImport("kernel32.dll", EntryPoint = "GetConsoleWindow")]
  public static extern IntPtr GetConsoleWindow();

  // DWORD GetWindowThreadProcessId(HWND hWnd, LPDWORD lpdwProcessId)
  [DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId")]
  public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

  public static uint GetPID()
  {
    IntPtr h = GetConsoleWindow();
    if ((uint)h==0) return 0;
    uint rc = 0;
    GetWindowThreadProcessId(h, out rc);
    return rc;
  }
"@ -Name Win32 -NameSpace System

[System.Win32]::GetPID();

在批处理文件中,您可以运行将 PowerShell 代码作为编码命令:

set MyPID=
for /f %%a in ('powershell -NoLogo -NoProfile -NonInteractive -EncodedCommand QQBkAGQALQBUAHkAcABlACAALQBNAGUAbQBiAGUAcgBEAGUAZgBpAG4AaQB0AGkAbwBuACAAQAAiAA0ACgBbAEQAbABsAEkAbQBwAG8AcgB0ACgAIgBrAGUAcgBuAGUAbAAzADIALgBkAGwAbAAiACwAIABFAG4AdAByAHkAUABvAGkAbgB0ACAAPQAgACIARwBlAHQAQwBvAG4AcwBvAGwAZQBXAGkAbgBkAG8AdwAiACkAXQANAAoAcAB1AGIAbABpAGMAIABzAHQAYQB0AGkAYwAgAGUAeAB0AGUAcgBuACAASQBuAHQAUAB0AHIAIABHAGUAdABDAG8AbgBzAG8AbABlAFcAaQBuAGQAbwB3ACgAKQA7AA0ACgBbAEQAbABsAEkAbQBwAG8AcgB0ACgAIgB1AHMAZQByADMAMgAuAGQAbABsACIALAAgAEUAbgB0AHIAeQBQAG8AaQBuAHQAIAA9ACAAIgBHAGUAdABXAGkAbgBkAG8AdwBUAGgAcgBlAGEAZABQAHIAbwBjAGUAcwBzAEkAZAAiACkAXQANAAoAcAB1AGIAbABpAGMAIABzAHQAYQB0AGkAYwAgAGUAeAB0AGUAcgBuACAAdQBpAG4AdAAgAEcAZQB0AFcAaQBuAGQAbwB3AFQAaAByAGUAYQBkAFAAcgBvAGMAZQBzAHMASQBkACgASQBuAHQAUAB0AHIAIABoAFcAbgBkACwAIABvAHUAdAAgAHUAaQBuAHQAIABsAHAAZAB3AFAAcgBvAGMAZQBzAHMASQBkACkAOwANAAoAcAB1AGIAbABpAGMAIABzAHQAYQB0AGkAYwAgAHUAaQBuAHQAIABHAGUAdABQAEkARAAoACkADQAKAHsADQAKACAASQBuAHQAUAB0AHIAIABoACAAPQAgAEcAZQB0AEMAbwBuAHMAbwBsAGUAVwBpAG4AZABvAHcAKAApADsADQAKACAAaQBmACAAKAAoAHUAaQBuAHQAKQBoAD0APQAwACkAIAByAGUAdAB1AHIAbgAgADAAOwANAAoAIAB1AGkAbgB0ACAAcgBjACAAPQAgADAAOwANAAoAIABHAGUAdABXAGkAbgBkAG8AdwBUAGgAcgBlAGEAZABQAHIAbwBjAGUAcwBzAEkAZAAoAGgALAAgAG8AdQB0ACAAcgBjACkAOwANAAoAIAByAGUAdAB1AHIAbgAgAHIAYwA7AA0ACgB9AA0ACgAiAEAAIAAtAE4AYQBtAGUAIABXAGkAbgAzADIAIAAtAE4AYQBtAGUAUwBwAGEAYwBlACAAUwB5AHMAdABlAG0ADQAKAFsAUwB5AHMAdABlAG0ALgBXAGkAbgAzADIAXQA6ADoARwBlAHQAUABJAEQAKAApADsADQAKAA^=^= 2^>nul') do (
  set "MyPID=%%~a"
)

if defined MyPID echo %MyPID%

生成唯一编号(日期时间戳)

@echo off
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
set "YY=%dt:~2,2%" & set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%"
set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%"
rem set "datestamp=%YY%%MM%%DD%" & set "timestamp=%HH%%Min%%Sec%"
set "datestamp=%YYYY%%MM%%DD%" 
set "timestamp=%HH%%Min%%Sec%"
set unique_number=%datestamp%%timestamp%
echo %unique_number%

这是另一种方法。请注意,新创建的文件将以零 (0) 长度创建。如果需要,您的代码负责删除它。

C:>TYPE CreateTempFile.bat
FOR /F "delims=" %%A IN ('powershell.exe -NoLogo -NoProfile -Command "(New-TemporaryFile).FullName"') DO (SET "NEWFILE=%%~A")
ECHO New file is "%NEWFILE%"
DIR "%NEWFILE%"
IF EXIST "%NEWFILE%" (DEL "%NEWFILE%")