Public/SubstDrive.ps1
|
function ConvertTo-SubstDriveLetter { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DriveLetter ) if ($DriveLetter -notmatch '^[A-Za-z]:?$') { throw [System.ArgumentException]::new( 'DriveLetter must be a single letter A-Z, optionally followed by a colon.', 'DriveLetter') } return "$($DriveLetter.Substring(0, 1).ToUpperInvariant()):" } function Resolve-SubstTargetDirectory { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$TargetPath ) try { $item = Get-Item -LiteralPath $TargetPath -ErrorAction Stop } catch { throw [System.IO.DirectoryNotFoundException]::new("TargetPath must be an existing directory: $TargetPath") } if (-not $item.PSIsContainer) { throw [System.IO.DirectoryNotFoundException]::new("TargetPath must be an existing directory: $TargetPath") } if ($item.PSProvider.Name -ne 'FileSystem') { throw [System.ArgumentException]::new( "TargetPath must resolve to a FileSystem directory: $TargetPath", 'TargetPath') } return $item.FullName } function New-SubstDriveObject { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$DriveLetter, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$TargetPath ) $item = Get-Item -LiteralPath $TargetPath -ErrorAction SilentlyContinue if ($item -and $item.PSIsContainer -and $item.PSProvider.Name -eq 'FileSystem') { $TargetPath = $item.FullName } $drive = [PSCustomObject]@{ DriveLetter = ConvertTo-SubstDriveLetter -DriveLetter $DriveLetter TargetPath = $TargetPath } $drive.PSObject.TypeNames.Insert(0, 'SubstDrive') return $drive } function Invoke-SubstCommand { [CmdletBinding()] param( [string[]]$ArgumentList = @() ) $substPath = Join-Path $env:SystemRoot 'System32\subst.exe' if (-not (Test-Path -LiteralPath $substPath -PathType Leaf)) { $substPath = (Get-Command subst.exe -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source } $output = & $substPath @ArgumentList 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -ne 0) { $message = ($output | Out-String).Trim() if (-not $message) { $message = "subst.exe exited with code $exitCode." } throw $message } return $output } function ConvertFrom-SubstOutputLine { [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string]$Line ) process { if ($Line -match '^\s*(?<DriveLetter>[A-Za-z]):(?:\\)?:\s*=>\s*(?<TargetPath>.+?)\s*$' -or $Line -match '^\s*(?<DriveLetter>[A-Za-z]):(?:\\)?\s*=>\s*(?<TargetPath>.+?)\s*$') { [PSCustomObject]@{ DriveLetter = "$($Matches.DriveLetter.ToUpperInvariant()):" TargetPath = $Matches.TargetPath.TrimEnd() } } } } function Get-SubstDriveMapping { [CmdletBinding()] param() Invoke-SubstCommand | ConvertFrom-SubstOutputLine } function New-SubstDriveMapping { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[A-Z]:$')] [string]$DriveLetter, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$TargetPath ) Invoke-SubstCommand -ArgumentList @($DriveLetter, $TargetPath) | Out-Null } function Remove-SubstDriveMapping { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[A-Z]:$')] [string]$DriveLetter ) Invoke-SubstCommand -ArgumentList @($DriveLetter, '/D') | Out-Null } function Test-SubstDriveLetterInUse { [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[A-Z]:$')] [string]$DriveLetter ) $letter = $DriveLetter.Substring(0, 1) $existingDrive = Get-PSDrive -Name $letter -PSProvider FileSystem -ErrorAction SilentlyContinue if ($existingDrive) { return $true } return [bool](Get-SubstDriveMapping | Where-Object DriveLetter -EQ $DriveLetter | Select-Object -First 1) } function Get-SubstDrive { <# .SYNOPSIS Gets Windows subst virtual drive mappings. .DESCRIPTION Enumerates current subst drive mappings and returns typed SubstDrive objects containing DriveLetter and TargetPath properties. .PARAMETER DriveLetter Optional drive letter filter. Specify a single letter, with or without a colon. .EXAMPLE Get-SubstDrive Lists all current subst drive mappings. .EXAMPLE Get-SubstDrive -DriveLetter S Gets the subst mapping for drive S:. .NOTES Windows only. #> [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$DriveLetter ) begin { Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name $normalizedDriveLetter = if ($PSBoundParameters.ContainsKey('DriveLetter')) { ConvertTo-SubstDriveLetter -DriveLetter $DriveLetter } } process { foreach ($mapping in Get-SubstDriveMapping) { $mappingDriveLetter = ConvertTo-SubstDriveLetter -DriveLetter $mapping.DriveLetter if ($normalizedDriveLetter -and $mappingDriveLetter -ne $normalizedDriveLetter) { continue } New-SubstDriveObject -DriveLetter $mappingDriveLetter -TargetPath $mapping.TargetPath } } } function New-SubstDrive { <# .SYNOPSIS Creates a Windows subst virtual drive mapping. .DESCRIPTION Maps a drive letter to an existing local FileSystem directory by using the Windows subst capability. .PARAMETER DriveLetter The virtual drive letter to create. Specify a single letter, with or without a colon. .PARAMETER TargetPath The existing local directory that the virtual drive should point to. .EXAMPLE New-SubstDrive -DriveLetter S -TargetPath C:\Source Creates S: as a virtual drive pointing to C:\Source. .NOTES Windows only. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string]$DriveLetter, [Parameter(Mandatory, Position = 1)] [ValidateNotNullOrEmpty()] [string]$TargetPath ) process { Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name $normalizedDriveLetter = ConvertTo-SubstDriveLetter -DriveLetter $DriveLetter $resolvedTargetPath = Resolve-SubstTargetDirectory -TargetPath $TargetPath if (Test-SubstDriveLetterInUse -DriveLetter $normalizedDriveLetter) { throw [System.InvalidOperationException]::new("Drive letter $normalizedDriveLetter is already in use.") } if ($PSCmdlet.ShouldProcess($normalizedDriveLetter, "Map to $resolvedTargetPath")) { New-SubstDriveMapping -DriveLetter $normalizedDriveLetter -TargetPath $resolvedTargetPath New-SubstDriveObject -DriveLetter $normalizedDriveLetter -TargetPath $resolvedTargetPath } } } function Remove-SubstDrive { <# .SYNOPSIS Removes a Windows subst virtual drive mapping. .DESCRIPTION Removes an existing subst mapping by drive letter. Accepts DriveLetter from Get-SubstDrive output through the pipeline by property name. .PARAMETER DriveLetter The virtual drive letter to remove. Specify a single letter, with or without a colon. .EXAMPLE Remove-SubstDrive -DriveLetter S Removes the S: subst mapping. .EXAMPLE Get-SubstDrive -DriveLetter S | Remove-SubstDrive Removes the S: subst mapping returned by Get-SubstDrive. .NOTES Windows only. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$DriveLetter ) process { Assert-WindowsOnly -CommandName $MyInvocation.MyCommand.Name $normalizedDriveLetter = ConvertTo-SubstDriveLetter -DriveLetter $DriveLetter $mapping = Get-SubstDriveMapping | Where-Object DriveLetter -EQ $normalizedDriveLetter | Select-Object -First 1 if (-not $mapping) { throw [System.InvalidOperationException]::new("No subst mapping exists for drive letter $normalizedDriveLetter.") } if ($PSCmdlet.ShouldProcess($normalizedDriveLetter, 'Remove subst mapping')) { Remove-SubstDriveMapping -DriveLetter $normalizedDriveLetter } } } |