65

PowerShell 5 introduces the New-TemporaryFile cmdlet, which is handy. How can I do the same thing but instead of a file create a directory? Is there a New-TemporaryDirectory cmdlet?

zett42
  • 25,437
  • 3
  • 35
  • 72
Michael Kropat
  • 14,557
  • 12
  • 70
  • 91

9 Answers9

89

I think it can be done without looping by using a GUID for the directory name:

function New-TemporaryDirectory {
    $parent = [System.IO.Path]::GetTempPath()
    [string] $name = [System.Guid]::NewGuid()
    New-Item -ItemType Directory -Path (Join-Path $parent $name)
}

Original Attempt With GetRandomFileName

Here's my port of this C# solution:

function New-TemporaryDirectory {
    $parent = [System.IO.Path]::GetTempPath()
    $name = [System.IO.Path]::GetRandomFileName()
    New-Item -ItemType Directory -Path (Join-Path $parent $name)
}

Analysis Of Possibility Of Collision

How likely is it that GetRandomFileName will return a name that already exists in the temp folder?

  • Filenames are returned in the form XXXXXXXX.XXX where X can be either a lowercase letter or digit.
  • That gives us 36^11 combinations, which in bits is around 2^56
  • Invoking the birthday paradox, we'd expect a collision once we got to around 2^28 items in the folder, which is about 360 million
  • NTFS supports about 2^32 items in a folder, so it is possible to get a collision using GetRandomFileName

NewGuid on the other hand can be one of 2^122 possibilities, making collisions all but impossible.

Community
  • 1
  • 1
Michael Kropat
  • 14,557
  • 12
  • 70
  • 91
  • 1
    I'd probably put the `GetRandomFileName()` and `New-Item` in a loop to automatically retry in case of a name conflict. – Ansgar Wiechers Jan 01 '16 at 21:30
  • @sodawillow Great question. I mistakenly thought you needed `-Force` to create non-existent parent dirs, but I was wrong, so I updated the answer. – Michael Kropat Jan 02 '16 at 14:22
  • To quote MSDN : *-Force allows the cmdlet to create an item that writes over an existing read-only item. Implementation varies from provider to provider. For more information, see about_Providers. Even using the Force parameter, the cmdlet cannot override security restrictions.* – sodawillow Jan 02 '16 at 14:53
  • 20
    If you have 360 million items in your temp folder, I think you have bigger problems than some PS script failing because of a name collision. – jpmc26 Mar 21 '17 at 21:05
  • GUID is guarantied to be unique, not as stated 'unlikely equality'. – Tomer W Apr 25 '22 at 06:58
  • 1
    A slightly shorter directory name can be achieved like this: `$name = (New-Guid).ToString('n')`, which creates a string like `450d881de6054d5894c7f7378bbb9f51`. – zett42 Jul 27 '22 at 16:38
  • If you need to get the folder name so you can use it you need to add a return to the command like this: `Return (New-Item -ItemType Directory -Path (Join-Path $parent $name)).FullName` – HackSlash Aug 22 '22 at 16:13
36

I also love one-liners, and I'm begging for a downvote here. All I ask is that you put my own vague negative feelings about this into words.

New-TemporaryFile | %{ rm $_; mkdir $_ }

Depending on the type of purist you are, you can do %{ mkdir $_-d }, leaving placeholder to avoid collisions.

And it's reasonable to stand on Join-Path $env:TEMP $(New-Guid) | %{ mkdir $_ } also.

nik.shornikov
  • 1,869
  • 1
  • 17
  • 21
  • 3
    Seems like a good one-liner to me. Until proven otherwise, I'd assume there's a small chance of a race condition, but that's not a real concern for 99% of use cases. – Michael Kropat Jan 16 '17 at 20:55
  • 4
    Beware, I have seen "delete and recreate" strategies fail on systems with slow anti virus software, as hinted in "The case of the asynchronous copy and delete" by Raymond Chen at https://blogs.msdn.microsoft.com/oldnewthing/20120907-00/?p=6663 . – Nathan Schubkegel Jan 09 '19 at 17:47
  • 2
    Here's the "Native PowerShell" variant: `New-TemporaryFile | % { Remove-Item $_; New-Item -ItemType Directory -Path $_ }` – NReilingh Mar 01 '19 at 22:07
  • Adding `-d` doesn't avoid collisions. (What if you're using the same approach in two different scripts at the same time?) But then, you weren't trying to avoid collisions, were you? ;-) – jpaugh Oct 02 '19 at 17:17
  • 1
    To clarify, leaving the placeholder is what would avoid collisions (until the commandlet is reimplemented), if they were a risk on practice. The arbitrary deterministic `-d` is to avoid a so-called “collision” between the resulting file and directory. – nik.shornikov Oct 03 '19 at 21:48
  • Supporting someone else's code is more difficult compared to creating your own. Sure you can create a dense one-liner. God help the poor sap trying to understand your intent to support your recommendation. If your solution is not obvious the likely-hood of a future developer trying to support your code making an error increases substantially. If the code is obvious and clear the likely-hood of making a mistake decreases. Some could argue - that is what comments are for, but others will counter-argue the code should be clear enough to negate the need for comments. – barrypicker Jan 02 '20 at 17:54
  • The commands you've provided create the directory, but it neither changes cwd into the dir nor stores the name of the new dir to a variable for future use. Can you please update the answer so the dir is created and also stored to a variable? – Michael Altfield Sep 14 '20 at 17:43
  • @NathanSchubkegel That's a good point. A character could be appended to the temporary directory name that is being created to avoid a having a naming conflict when the file isn't removed before the directory is created. `New-TemporaryFile | %{ rm $_; mkdir "${_}d" }`. – Dave F Mar 08 '21 at 04:01
19

Here's a variant of user4317867's answer. I create a new directory in the user's Windows "Temp" folder and make the temp folder path available as a variable ($tempFolderPath):

$tempFolderPath = Join-Path $Env:Temp $(New-Guid)
New-Item -Type Directory -Path $tempFolderPath | Out-Null

Here's the same script available as a one-liner:

$tempFolderPath = Join-Path $Env:Temp $(New-Guid); New-Item -Type Directory -Path $tempFolderPath | Out-Null

And here's what the fully qualified temp folder path ($tempFolderPath) looks like:

C:\Users\MassDotNet\AppData\Local\Temp\2ae2dbc4-c709-475b-b762-72108b8ecb9f
Mass Dot Net
  • 2,150
  • 9
  • 38
  • 50
13

If you want the looping solution that is guaranteed to be both race- and collision-free, then here it is:

function New-TemporaryDirectory {
  $parent = [System.IO.Path]::GetTempPath()
  do {
    $name = [System.IO.Path]::GetRandomFileName()
    $item = New-Item -Path $parent -Name $name -ItemType "directory" -ErrorAction SilentlyContinue
  } while (-not $item)
  return $item.FullName
}

According to the analysis in Michael Kropat's answer, the vast majority of the time, this will pass only once through the loop. Rarely will it pass twice. Virtually never will it pass three times.

John Freeman
  • 2,552
  • 1
  • 27
  • 34
  • 1
    This won't work, it'll just get stuck in an infinite loop if there is ever a collision... because $name is not updated inside the loop. – Per von Zweigbergk Apr 02 '19 at 14:53
  • Right, my mistake. I just moved the name creation into the loop. – John Freeman Apr 02 '19 at 14:59
  • 1
    Also it'll throw up an unsightly error if it fails for any reason. Also it returns two objects (because New-Item returns an object). Your general approach is sound, but I submitted an edit to your post to fix the issues I found. As far as I can tell, it seems to work. :-) – Per von Zweigbergk Apr 02 '19 at 15:13
4

Here's my attempt:

function New-TemporaryDirectory {
    $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())

    #if/while path already exists, generate a new path
    while(Test-Path $path)) {
        $path = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
    }

    #create directory with generated path
    New-Item -ItemType Directory -Path $path
}
sodawillow
  • 12,497
  • 4
  • 34
  • 44
  • This code faces a race condition between the end of the `while` and the `New-Item` command. – ComFreek Apr 04 '18 at 06:43
  • @ComFreek, can you explain? Are you talking about when calling this method rapidly in a multi-threaded environment? I don't see how that's possible in a single thread, like a powershell script. – HackSlash Aug 22 '22 at 16:00
3

.NET has had [System.IO.Path]::GetTempFileName() for quite a while; you can use this to generate a file (and the capture the name), then create a folder with the same name after deleting the file.

$tempfile = [System.IO.Path]::GetTempFileName();
remove-item $tempfile;
new-item -type directory -path $tempfile;
alroc
  • 27,574
  • 6
  • 51
  • 97
3

I love one liners if possible. @alroc .NET also has [System.Guid]::NewGuid()

$temp = [System.Guid]::NewGuid();new-item -type directory -Path d:\$temp

Directory: D:\


Mode                LastWriteTime     Length Name                                                                                                                        
----                -------------     ------ ----                                                                                                                        
d----          1/2/2016  11:47 AM            9f4ef43a-a72a-4d54-9ba4-87a926906948  
user4317867
  • 2,397
  • 4
  • 31
  • 57
  • This is my favorite. It's clean, and works on Server 2012 R2 era machines. I would probably set $temp direcly with `$temp = New-Item -Type Directory -Path $env:TEMP -Name ([System.Guid]::NewGuid())` – IsAGuest Jul 08 '22 at 15:08
0

if you want, you can be extremely fancy and call Windows API function GetTempPathA() like this:

# DWORD GetTempPathA(
#   DWORD nBufferLength,
#   LPSTR lpBuffer
# );

$getTempPath = @"
using System;
using System.Runtime.InteropServices;
using System.Text;

public class getTempPath {
    [DllImport("KERNEL32.DLL", EntryPoint = "GetTempPathA")]
    public static extern uint GetTempPath(uint nBufferLength, [Out] StringBuilder lpBuffer);
}
"@

Add-Type $getTempPath

$str = [System.Text.StringBuilder]::new()
$MAX_PATH = 260
$catch_res = [getTempPath]::GetTempPath($MAX_PATH, $str)
Write-Host $str.ToString() #echos temp path to STDOUT
# ... continue your code here and create sub folders as you wish ...
  1. https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha
AK_
  • 1,879
  • 4
  • 21
  • 30
  • 1
    This an exact code that `Path.GetTempPath` does for you internally. – Martin Prikryl Mar 26 '21 at 07:06
  • @martin-prikryl good follow up, do you know how I can view that internal code? – AK_ Mar 26 '21 at 13:07
  • https://referencesource.microsoft.com/#mscorlib/system/io/path.cs,3a7a8c72321c6e1d,references + Though it will correctly use Unicode version of the API, contrary to your legacy Ansi version. – Martin Prikryl Mar 26 '21 at 13:12
-1

Expanding from Michael Kropat's answer: https://stackoverflow.com/a/34559554/8083582

Function New-TemporaryDirectory {
  $tempDirectoryBase = [System.IO.Path]::GetTempPath();
  $newTempDirPath = [String]::Empty;
  Do {
    [string] $name = [System.Guid]::NewGuid();
    $newTempDirPath = (Join-Path $tempDirectoryBase $name);
  } While (Test-Path $newTempDirPath);

  New-Item -ItemType Directory -Path $newTempDirPath;
  Return $newTempDirPath;
}

This should eliminate any issues with collisions.

Mitchell R
  • 21
  • 5
  • 2
    This code faces a race condition between the end of the `while` and the `New-Item` command, exactly as the [other answer](https://stackoverflow.com/a/34565223/603003) above. – ComFreek Apr 04 '18 at 06:44