Utils/mktemp.ps1
|
Register-GnuUtil @{ Name = 'mktemp' Category = 'File and directory operations' Synopsis = 'Create a temporary file or directory.' Usage = @('mktemp [OPTION]... [TEMPLATE]') Description = @( 'Create a temporary file or directory, safely, and print its name.', 'TEMPLATE must contain at least 3 consecutive ''X''s in last component.', 'If TEMPLATE is not specified, use tmp.XXXXXXXXXX, and --tmpdir is implied.', 'Files are created u+rw, and directories u+rwx, minus umask restrictions.' ) Options = @( @{ Short = 'd'; Long = 'directory'; Help = 'create a directory, not a file' } @{ Short = 'u'; Long = 'dry-run'; Help = 'do not create anything; merely print a name (unsafe)' } @{ Short = 'q'; Long = 'quiet'; Help = 'suppress diagnostics about file/dir-creation failure' } @{ Long = 'suffix'; Arg = 'SUFF'; Help = 'append SUFF to TEMPLATE; SUFF must not contain a slash' } # GNU: -p always takes DIR; only the long form has an optional argument. @{ Short = 'p'; Key = 'tmpdir'; Arg = 'DIR'; Help = 'same as --tmpdir=DIR' } @{ Long = 'tmpdir'; Arg = 'DIR'; OptionalArg = $true; Help = 'interpret TEMPLATE relative to DIR; if DIR is not specified, use $TMPDIR if set, else the system temp directory' } @{ Short = 't'; Help = 'interpret TEMPLATE as a single file name component, relative to a directory: $TMPDIR, if set; else the directory specified via -p; else the system temp directory [deprecated]' } ) Notes = @('PowerShell note: the system temp directory is [System.IO.Path]::GetTempPath() (e.g. %TEMP% on Windows, /tmp on Linux).') } function script:Get-GnuTempDirectory { <# .SYNOPSIS $TMPDIR if set, else the system temp directory, without a trailing separator. #> $directory = [string]$env:TMPDIR if (-not $directory) { $directory = [System.IO.Path]::GetTempPath() } $trimmed = $directory.TrimEnd((Get-GnuPathSeparator)) if ([string]::IsNullOrEmpty($trimmed) -or $trimmed -match '^[A-Za-z]:$') { return $directory } $trimmed } function script:New-GnuTemporaryItem { <# .SYNOPSIS Implementation of mktemp. #> param([Parameter(Mandatory)][hashtable]$Context) $operands = @($Context.Operands) if ($operands.Count -gt 1) { Write-GnuUsageError -Context $Context -Message "too many templates" return } $quiet = Test-GnuOption -Context $Context -Key 'quiet' $makeDirectory = Test-GnuOption -Context $Context -Key 'directory' $template = 'tmp.XXXXXXXXXX' $useTempDirectory = $true if ($operands.Count -eq 1) { $template = $operands[0] $useTempDirectory = (Test-GnuOption -Context $Context -Key 'tmpdir') -or (Test-GnuOption -Context $Context -Key 't') } $suffix = '' if (Test-GnuOption -Context $Context -Key 'suffix') { $suffix = [string](Get-GnuOptionValue -Context $Context -Key 'suffix') if ($suffix.IndexOfAny((Get-GnuPathSeparator)) -ge 0) { Write-GnuError -Context $Context -Message "invalid suffix '$suffix', contains directory separator" return } } elseif ($template -match '^(.*X{3,})([^X]*)$' -and $Matches[2].IndexOfAny((Get-GnuPathSeparator)) -lt 0) { # GNU: text after the last run of X's is an implicit suffix. $template = $Matches[1] $suffix = $Matches[2] } $xCount = $template.Length - $template.TrimEnd('X').Length if ($xCount -lt 3) { Write-GnuError -Context $Context -Message "too few X's in template '$($template + $suffix)'" return } $directory = '' if ($useTempDirectory) { if ([System.IO.Path]::IsPathRooted($template)) { Write-GnuError -Context $Context -Message "invalid template, '$template'; with --tmpdir, it may not be absolute" return } $directory = Get-GnuTempDirectory if ((Test-GnuOption -Context $Context -Key 'tmpdir') -and (Get-GnuOptionValue -Context $Context -Key 'tmpdir')) { $directory = [string](Get-GnuOptionValue -Context $Context -Key 'tmpdir') } } $stem = $template.Substring(0, $template.Length - $xCount) $dryRun = Test-GnuOption -Context $Context -Key 'dry-run' $lastError = $null for ($attempt = 0; $attempt -lt 100; $attempt++) { $name = $stem + (New-GnuRandomText -Length $xCount) + $suffix $display = $name if ($directory) { $display = [System.IO.Path]::Combine($directory, $name) } $path = Resolve-GnuPath -Path $display # Directories and dry runs need an explicit check; for files, CreateNew detects collisions atomically. $taken = [System.IO.File]::Exists($path) -or [System.IO.Directory]::Exists($path) if ($taken -and ($makeDirectory -or $dryRun -or [System.IO.Directory]::Exists($path))) { continue } if ($dryRun) { return $display } try { if ($makeDirectory) { $parent = Get-GnuParentPath -Path $path if ($parent -and -not [System.IO.Directory]::Exists($parent)) { throw (New-Object System.IO.DirectoryNotFoundException $parent) } $null = [System.IO.Directory]::CreateDirectory($path) $null = Set-GnuUnixMode -Path $path -Mode 448 # 0700 } else { $stream = New-Object System.IO.FileStream -ArgumentList $path, ([System.IO.FileMode]::CreateNew), ([System.IO.FileAccess]::Write) $stream.Dispose() $null = Set-GnuUnixMode -Path $path -Mode 384 # 0600 } return $display } catch { $lastError = $_.Exception if ([System.IO.File]::Exists($path)) { # The name was taken: not an error, try another one. $lastError = $null continue } break } } $kind = 'file' if ($makeDirectory) { $kind = 'directory' } $reason = 'File exists' if ($null -ne $lastError) { $reason = Get-GnuErrorReason -Exception $lastError } if ($quiet) { $Context.ExitCode = 1 } else { Write-GnuError -Context $Context -Message "failed to create $kind via template '$($template + $suffix)': $reason" } } function script:New-GnuRandomText { <# .SYNOPSIS Length random characters from [A-Za-z0-9], from a cryptographic random number generator. #> param([Parameter(Mandatory)][int]$Length) $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' $bytes = New-Object byte[] $Length $random = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $random.GetBytes($bytes) } finally { $random.Dispose() } -join @(foreach ($byte in $bytes) { $alphabet[$byte % $alphabet.Length] }) } |