From f83ccddf80100516019bfb0a2fad9411bb1b62ed Mon Sep 17 00:00:00 2001
From: Kirtikumar Anandrao Ramchandani
<33368817+KirtiRamchandani@users.noreply.github.com>
Date: Tue, 18 Aug 2026 00:05:04 +0530
Subject: [PATCH] Add New-TemporaryDirectory cmdlet (#27549)
Co-authored-by: Justin Chung <124807742+jshigetomi@users.noreply.github.com>
---
.../utility/NewTemporaryDirectoryCommand.cs | 113 ++++++++++++++++++
.../Microsoft.PowerShell.Utility.psd1 | 2 +-
.../Microsoft.PowerShell.Utility.psd1 | 2 +-
.../New-TemporaryDirectory.Tests.ps1 | 110 +++++++++++++++++
.../engine/Basic/DefaultCommands.Tests.ps1 | 1 +
5 files changed, 226 insertions(+), 2 deletions(-)
create mode 100644 src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewTemporaryDirectoryCommand.cs
create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Utility/New-TemporaryDirectory.Tests.ps1
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewTemporaryDirectoryCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewTemporaryDirectoryCommand.cs
new file mode 100644
index 000000000..6a19c0792
--- /dev/null
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewTemporaryDirectoryCommand.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Management.Automation;
+
+namespace Microsoft.PowerShell.Commands
+{
+ ///
+ /// The implementation of the "New-TemporaryDirectory" cmdlet.
+ ///
+ [Cmdlet(VerbsCommon.New, "TemporaryDirectory", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low, HelpUri = "https://go.microsoft.com/fwlink/?LinkId=2097032")]
+ [OutputType(typeof(System.IO.DirectoryInfo))]
+ public class NewTemporaryDirectoryCommand : Cmdlet
+ {
+ private const string NewTemporaryDirectoryInvalidPrefix = "NewTemporaryDirectoryInvalidPrefix";
+ private const string NewTemporaryDirectoryWriteError = "NewTemporaryDirectoryWriteError";
+
+ ///
+ /// Gets or sets an optional prefix for the temporary directory name.
+ ///
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string Prefix { get; set; }
+
+ ///
+ /// Returns a temporary directory.
+ ///
+ protected override void EndProcessing()
+ {
+ string tempPath = Path.GetTempPath();
+
+ if (!string.IsNullOrEmpty(Prefix) && IsInvalidPrefix(Prefix))
+ {
+ ThrowTerminatingError(
+ CreateErrorRecord(
+ new System.ArgumentException("The prefix contains invalid characters.", nameof(Prefix)),
+ NewTemporaryDirectoryInvalidPrefix,
+ ErrorCategory.InvalidArgument,
+ Prefix));
+ return;
+ }
+
+ string targetDescription = string.IsNullOrEmpty(Prefix)
+ ? $"temporary directory under '{tempPath}'"
+ : $"temporary directory under '{tempPath}' with prefix '{Prefix}'";
+
+ if (!ShouldProcess(targetDescription, "Create temporary directory"))
+ {
+ return;
+ }
+
+ try
+ {
+ DirectoryInfo createdDirectory = string.IsNullOrEmpty(Prefix)
+ ? Directory.CreateTempSubdirectory()
+ : Directory.CreateTempSubdirectory(Prefix);
+
+ WriteObject(createdDirectory);
+ }
+ catch (IOException ioException)
+ {
+ ThrowTerminatingError(
+ CreateErrorRecord(
+ ioException,
+ NewTemporaryDirectoryWriteError,
+ ErrorCategory.WriteError,
+ targetDescription));
+ }
+ catch (System.UnauthorizedAccessException unauthorizedAccessException)
+ {
+ ThrowTerminatingError(
+ CreateErrorRecord(
+ unauthorizedAccessException,
+ NewTemporaryDirectoryWriteError,
+ ErrorCategory.PermissionDenied,
+ targetDescription));
+ }
+ }
+
+ private static bool IsInvalidPrefix(string prefix)
+ {
+ if (prefix.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
+ {
+ return true;
+ }
+
+ foreach (char prefixCharacter in prefix)
+ {
+ if (prefixCharacter is '/' or '\\' || char.IsControl(prefixCharacter))
+ {
+ return true;
+ }
+ }
+
+ return prefix.Equals(".", System.StringComparison.Ordinal)
+ || prefix.Equals("..", System.StringComparison.Ordinal);
+ }
+
+ private static ErrorRecord CreateErrorRecord(
+ System.Exception exception,
+ string errorId,
+ ErrorCategory category,
+ object targetObject)
+ {
+ return new ErrorRecord(
+ exception,
+ errorId,
+ category,
+ targetObject);
+ }
+ }
+}
diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
index df8418376..477dc1c5a 100644
--- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
+++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
@@ -21,7 +21,7 @@ CmdletsToExport = @(
'Get-PSBreakpoint', 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession',
'Import-PSSession', 'Get-Random', 'Get-SecureRandom', 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace',
'Disable-RunspaceDebug', 'Enable-RunspaceDebug', 'Get-RunspaceDebug', 'Start-Sleep', 'Join-String',
- 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan',
+ 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryDirectory', 'New-TemporaryFile', 'New-TimeSpan',
'Get-TraceSource', 'Set-TraceSource', 'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData',
'Get-UICulture', 'Get-Unique', 'Get-Uptime', 'Clear-Variable', 'Get-Variable', 'New-Variable',
'Remove-Variable', 'Set-Variable', 'Get-Verb', 'Write-Verbose', 'Write-Warning', 'Invoke-WebRequest',
diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
index 2043543a8..64813ba5b 100644
--- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
+++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1
@@ -20,7 +20,7 @@ CmdletsToExport = @(
'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', 'Get-SecureRandom'
'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', 'Disable-RunspaceDebug', 'Enable-RunspaceDebug',
'Get-RunspaceDebug', 'ConvertFrom-SddlString', 'Start-Sleep', 'Join-String', 'Out-String', 'Select-String',
- 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource',
+ 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryDirectory', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource',
'Add-Type', 'Get-TypeData', 'Remove-TypeData', 'Update-TypeData', 'Get-UICulture', 'Get-Unique', 'Get-Uptime',
'Clear-Variable', 'Get-Variable', 'New-Variable', 'Remove-Variable', 'Set-Variable', 'Get-Verb', 'Write-Verbose',
'Write-Warning', 'Invoke-WebRequest', 'Format-Wide', 'ConvertTo-Xml', 'Select-Xml', 'Get-Error', 'Update-List',
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/New-TemporaryDirectory.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-TemporaryDirectory.Tests.ps1
new file mode 100644
index 000000000..333e8db5b
--- /dev/null
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-TemporaryDirectory.Tests.ps1
@@ -0,0 +1,110 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+Describe "New-TemporaryDirectory" -Tags "CI" {
+ BeforeEach {
+ $tempDirectory = $null
+ }
+
+ AfterEach {
+ if ($tempDirectory -and (Test-Path -LiteralPath $tempDirectory.FullName)) {
+ Remove-Item -LiteralPath $tempDirectory.FullName -Force -Recurse -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "creates a new temporary directory" {
+ $tempDirectory = New-TemporaryDirectory
+
+ $tempDirectory | Should -Exist
+ $tempDirectory | Should -BeOfType System.IO.DirectoryInfo
+ $tempDirectory.FullName | Should -BeLikeExactly "$([System.IO.Path]::GetTempPath())*"
+ (Get-Item -LiteralPath $tempDirectory.FullName).PSIsContainer | Should -BeTrue
+ }
+
+ It "creates unique temporary directories" {
+ $tempDirectory = New-TemporaryDirectory
+ $secondTempDirectory = New-TemporaryDirectory
+
+ try {
+ $secondTempDirectory.FullName | Should -Not -BeExactly $tempDirectory.FullName
+ $secondTempDirectory | Should -Exist
+ $secondTempDirectory | Should -BeOfType System.IO.DirectoryInfo
+ }
+ finally {
+ if ($secondTempDirectory -and (Test-Path -LiteralPath $secondTempDirectory.FullName)) {
+ Remove-Item -LiteralPath $secondTempDirectory.FullName -Force -Recurse -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ It "with WhatIf does not create a directory" {
+ New-TemporaryDirectory -WhatIf | Should -BeNullOrEmpty
+ }
+
+ It "with WhatIf and Prefix does not create a directory" {
+ New-TemporaryDirectory -Prefix "Test_" -WhatIf | Should -BeNullOrEmpty
+ }
+
+ It "with WhatIf and Prefix reports a descriptive target" {
+ $prefix = "Test_"
+ $rawTarget = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $prefix)
+ $whatIfOutput = & "$PSHOME/pwsh" -NoProfile -CommandWithArgs 'param($prefix) New-TemporaryDirectory -Prefix $prefix -WhatIf' $prefix 2>&1 | Out-String
+
+ $whatIfOutput | Should -Not -Match ([regex]::Escape("`"$rawTarget`""))
+ $whatIfOutput | Should -Match ([regex]::Escape("with prefix '$prefix'"))
+ }
+
+ It "rejects special relative path Prefix ''" -TestCases @(
+ @{ Prefix = "." }
+ @{ Prefix = ".." }
+ ) {
+ param($Prefix)
+
+ { New-TemporaryDirectory -Prefix $Prefix -ErrorAction Stop } | Should -Throw -ErrorId "NewTemporaryDirectoryInvalidPrefix,Microsoft.PowerShell.Commands.NewTemporaryDirectoryCommand"
+ }
+
+ It "rejects separator-like Prefix ''" -TestCases @(
+ @{ Prefix = "path/name" }
+ @{ Prefix = "path\name" }
+ @{ Prefix = "../name" }
+ @{ Prefix = "..\name" }
+ ) {
+ param($Prefix)
+
+ { New-TemporaryDirectory -Prefix $Prefix -ErrorAction Stop } | Should -Throw -ErrorId "NewTemporaryDirectoryInvalidPrefix,Microsoft.PowerShell.Commands.NewTemporaryDirectoryCommand"
+ }
+
+ It "rejects Prefix with control character ''" -TestCases @(
+ @{ CharacterCode = 10 }
+ @{ CharacterCode = 13 }
+ @{ CharacterCode = 9 }
+ @{ CharacterCode = 27 }
+ ) {
+ param($CharacterCode)
+
+ $Prefix = "bad$([char]$CharacterCode)name"
+ { New-TemporaryDirectory -Prefix $Prefix -ErrorAction Stop } | Should -Throw -ErrorId "NewTemporaryDirectoryInvalidPrefix,Microsoft.PowerShell.Commands.NewTemporaryDirectoryCommand"
+ }
+
+ It "has an OutputType of System.IO.DirectoryInfo" {
+ (Get-Command New-TemporaryDirectory).OutputType.Name | Should -Contain "System.IO.DirectoryInfo"
+ }
+
+ It "creates a directory with custom prefix" {
+ $prefix = "MyApp"
+ $tempDirectory = New-TemporaryDirectory -Prefix $prefix
+
+ $tempDirectory | Should -Exist
+ $tempDirectory | Should -BeOfType System.IO.DirectoryInfo
+ $tempDirectory.Name | Should -BeLike "$prefix*"
+ }
+
+ It "rejects Prefix with invalid file name characters" {
+ $invalidChars = [System.IO.Path]::GetInvalidFileNameChars()
+
+ if ($invalidChars.Count -gt 0) {
+ $badPrefix = "bad$($invalidChars[0])prefix"
+ { New-TemporaryDirectory -Prefix $badPrefix -ErrorAction Stop } | Should -Throw -ErrorId "NewTemporaryDirectoryInvalidPrefix,Microsoft.PowerShell.Commands.NewTemporaryDirectoryCommand"
+ }
+ }
+}
diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
index 7a757e6ea..f69ba01a0 100644
--- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
+++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
@@ -401,6 +401,7 @@ Describe "Verify aliases and cmdlets" -Tags "CI" {
"Cmdlet", "New-PSSessionOption", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "New-PSTransportOption", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "New-Service", "", $($FullCLR -or $CoreWindows ), "", "", "Medium"
+"Cmdlet", "New-TemporaryDirectory", "", $( $CoreWindows -or $CoreUnix), "", "", "Low"
"Cmdlet", "New-TemporaryFile", "", $( $CoreWindows -or $CoreUnix), "", "", "Low"
"Cmdlet", "New-TimeSpan", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "None"
"Cmdlet", "New-Variable", "", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "", "Low"