Powershell - Need to check if name is ended with a sign - powershell

My script read a path from TFS and I add a string to it but before I need to verify that the path contains or not contains a sign
Example1:
This is the path, in this case I need to add '/database/'
$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22
Example2: I need to add only 'database/'
$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/
Example3: I need to add '/'
$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database
The goal is to continue with the script with the path/database/
so I need to check first the path and then to add or remove the 'database' string
Anyone can help me with that please?

If I understand the question properly, you want to check if the path from TFS ends with a forward slash or not, so you would know what to append to it and for thast you could use a small helper function like this:
function Join-TFSPath {
[CmdletBinding()]
param (
[parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[parameter(Mandatory = $false, Position = 1)]
[string[]] $ChildPath,
[char]$Separator = '/'
)
if ($ChildPath.Count) {
"{0}$separator{1}$Separator" -f $Path.TrimEnd("\/"),
(($ChildPath | ForEach-Object { $_.Trim("\/") } |
Where-Object { $_ -match '\S' }) -join $Separator)
}
else {
"{0}$separator" -f $Path.TrimEnd("\/")
}
}
# test if you need to add `database` or not
$tfsPath = '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database'
$folder = 'database'
if (($tfsPath.TrimEnd("\/") -split '[\\/]')[-1] -ne $folder) {
# use the function adding the $folder as ChildPath
Join-TFSPath -Path $tfsPath -ChildPath $folder
}
else {
# use the function without specifying the ChildPath so it will only ensure it
# ends with the chosen (or in this case default) separator character
Join-TFSPath -Path $tfsPath
}
As per your comment, you could perhaps then use a more dedicated helper function like:
function Append-TFSPath {
[CmdletBinding()]
param (
[parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[parameter(Mandatory = $false, Position = 1)]
[string] $ChildPath = 'database',
[char]$Separator = '/'
)
$Path = $Path -replace '[\\/]+$' # trim off final slash(es)
$ChildPath = $ChildPath -replace '^[\\/]|[\\/]$' -replace '\\', $Separator
if ([string]::IsNullOrWhiteSpace($ChildPath) -or ($Path -replace '\\', $Separator) -like "*$ChildPath") {
"{0}$separator" -f $Path
}
else {
"{0}$separator{1}$Separator" -f $Path, $ChildPath
}
}
Then, just send the path as you have received it to the function and it will return the path you want
$tfsPath = '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database'
$folder = 'V8.6.22/database'
Append-TFSPath -Path $tfsPath -ChildPath $folder
# because 'database' is the default value for the ChildPath parameter, you can leave that out:
# Append-TFSPath -Path $tfsPath
Testcases:
Append-TFSPath -Path '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22'
Append-TFSPath -Path '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/'
Append-TFSPath -Path '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database'
Append-TFSPath -Path '$/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database/'
will all return $/Idu Client-Server/CoreBranches/V6.4/Patches/V8.6.22/database/

Related

PowerShell with Pester Tests - No test files were found and no scriptblocks were provided

Trying to learn Pester (v5.0.4 and PS v7.0.3). I have these files
Get-Planet2.ps1:
function Get-Planet ([string]$Name = '*') {
$planets = #(
#{ Name = 'Mercury' }
#{ Name = 'Venus' }
#{ Name = 'Earth' }
#{ Name = 'Mars' }
#{ Name = 'Jupiter' }
#{ Name = 'Saturn' }
#{ Name = 'Uranus' }
#{ Name = 'Neptune' }
) | ForEach-Object { [PSCustomObject] $_ }
$planets | Where-Object { $_.Name -like $Name }
}
Get-Planet2.Tests.ps1:
BeforeAll {
# This will bring the function from the main file back to scope.
. $PSScriptRoot/Get-Planet2.ps1
param(
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$name,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$title,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$InputFile
)
}
Describe 'Get-Planet' {
It 'Given no parameters, it lists all 8 planets' {
$allPlanets = Get-Planet
$allPlanets.Count | Should -Be 8
}
It 'Earth is the third planet in our Solar System' {
$allPlanets = Get-Planet
$allPlanets[2].Name | Should -Be 'Earth'
}
It 'Pluto is not part of our Solar System' {
$allPlanets = Get-Planet
$plutos = $allPlanets | Where-Object Name -EQ 'Pluto'
$plutos.Count | Should -Be 0
}
It 'Planets have this order: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune' {
$allPlanets = Get-Planet
$planetsInOrder = $allPlanets.Name -join ', '
$planetsInOrder | Should -Be 'Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune'
}
}
Call-Get-Planet2.ps1:
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$path = "$here/Get-Planet2.Tests.ps1"
$parameters = #{name = "John"; title = "Sales"; InputFile = "$here/test.json"}
write-host "Path: $path"
write-host $parameters
Invoke-Pester -Script #{Path=$path;Parameters=$parameters}
My Problem:
When I run Call-Get-Planet2.ps1, I ended with the below error. I can't figure out why it is doing this. Need guidance. Thanks
System.Management.Automation.RuntimeException: No test files were found and no scriptblocks were provided.
at Invoke-Pester, /Users/myaccount/.local/share/powershell/Modules/Pester/5.0.4/Pester.psm1: line 4520
at , /Users/myaccount/repos/myrepo/Pester/Call-Get-Planet2.ps1: line 8
at , : line 1
If you encounter this error ('No test files were found and no scriptblocks were provided'), you should follow LievenKeersmaekers' suggestion (made in the question comments), which is to execute the test file more directly by doing something like the following:
cd {TO\A\FOLDER\WITH\A\TEST_FILE}
Invoke-Pester -Path .\{TEST_FILE}.ps1
You'll likely see a more descriptive error message that will help you to solve your issue.
Pester v5 had a breaking change and removed support for Invoke-Pester -Script <hashtable>. Pester 5.1 re-introduced support for script parameters using containers which you create with New-PesterContainer -Path <file> -Data <parametersHashtable> and invoke using Invoke-Pester -Container ....
For the original demo, upgrade to Pester 5.1 or later and modify:
Get-Planet2.Tests.ps1 to:
# Script parameters need to be in the root of the file.
param(
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$name,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$title,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$InputFile
)
BeforeAll {
# This will bring the function from the main file back to scope.
. $PSScriptRoot/Get-Planet2.ps1
}
Describe 'Get-Planet' {
It 'Given no parameters, it lists all 8 planets' {
$allPlanets = Get-Planet
$allPlanets.Count | Should -Be 8
}
It 'Earth is the third planet in our Solar System' {
$allPlanets = Get-Planet
$allPlanets[2].Name | Should -Be 'Earth'
}
It 'Pluto is not part of our Solar System' {
$allPlanets = Get-Planet
$plutos = $allPlanets | Where-Object Name -EQ 'Pluto'
$plutos.Count | Should -Be 0
}
It 'Planets have this order: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune' {
$allPlanets = Get-Planet
$planetsInOrder = $allPlanets.Name -join ', '
$planetsInOrder | Should -Be 'Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune'
}
}
Call-Get-Planet2.ps1 to:
# $PSScriptRoot is easier than Split-Path :-)
$here = $PSScriptRoot
$path = "$here/Get-Planet2.Tests.ps1"
$parameters = #{name = 'John'; title = 'Sales'; InputFile = "$here/test.json" }
Write-Host "Path: $path"
Write-Host $parameters
# -Script <hashtable> is not supported in v5.
# Use New-PesterContainer
$container = New-PesterContainer -Path $path -Data $parameters
Invoke-Pester -Container $container

How to write a Powershell file parameter that accepts a relative path

The following sample code correctly reads both the PSD1 and the regular file when the path to the PSD1 is a fully qualified path (or when the current location is the same as the current directory).
It fails when supplied a relative path.
param (
[Parameter(Mandatory=$true)][System.IO.FileInfo]$PsdFile,
[Parameter(Mandatory=$false)][System.IO.FileInfo]$RegularFile = 'testfile'
)
$RegularFileContent = Get-Content $RegularFile
$PsdData = Import-LocalizedData -BaseDirectory $PsdFile.DirectoryName -FileName $PsdFile.Name
echo "regular file contents: $RegularFileContent"
echo "Psd data:"
echo $PsdData.Name
How can I fix this so the user can enter a relative path?
I believe the underlying problem is similar to the problem described in this post about FileInfo but I don't know how to incorporate Resolve-Path or similar into the parameter handling.
A lot of your question hinges on which relative you're referring to as that itself is relative. If you want the path to be relative to a user's working directory versus the script's root, you can accomplish that like so:
param(
[Parameter(Mandatory)]
[string]
$PsdFile,
[Parameter()] # Mandatory is $false by default
[string]
$RegularFile = 'testfile'
)
[System.IO.FileInfo]$psd = Join-Path -Path $PWD.Path -ChildPath $PsdFile
$data = Import-LocalizedData -BaseDirectory $psd.DirectoryName -FileName $PsdFile.Name
$content = Get-Content -Path $RegularFile
"regular file contents: $content"
"Psd data:" + $data.Name
param (
[Parameter(Mandatory = $true)]
[ValidateScript( {
if (-Not (Test-Path -Path $_ ) ) { throw "File not found." }
elseif (-Not (Test-Path -Path $_ -PathType Leaf) ) { throw "PsdFile should be a file." }
return $true
})]
[System.IO.FileInfo] $PsdFile,
[Parameter(Mandatory = $false)]
[ValidateScript( {
if (-Not (Test-Path -Path $_ ) ) { throw "File not found." }
elseif (-Not (Test-Path -Path $_ -PathType Leaf) ) { throw "PsdFile should be a file." }
return $true
})]
[System.IO.FileInfo] $RegularFile = 'testfile'
)
$PsdFile = (Resolve-Path -Path $PsdFile).Path;
$RegularFile = (Resolve-Path -Path $RegularFile).Path;
$RegularFileContent = Get-Content $RegularFile.FullName
$PsdData = Import-LocalizedData -BaseDirectory $PsdFile.DirectoryName -FileName $PsdFile.Name
I use a similar approach to #luri, incorporating ValidateScript to test if the path exists. This example tests specifically for files and throws an error if a directory is passed. This is useful for cases when the user is using <TAB> to quickly autocomplete paths for the -Path parameter.
function PathTestFunction
{
[CmdletBinding()]
param (
[Parameter(Mandatory,
ParameterSetName = 'Path',
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[ValidateScript({
# Using Exists() because Test-Path doesn't test for system/hidden files
if ([System.IO.Directory]::Exists($(Resolve-Path -Path $_)))
{
throw "'$(Resolve-Path $_)' is a directory. Please use a file for the -Path parameter."
}
if ([System.IO.File]::Exists($(Resolve-Path -Path $_)) -eq $false)
{
$null = Resolve-Path $_ -ErrorVariable pathError
# Displays a full path name in the error message (vs a relative path if one was passed)
throw $pathError.Exception
}
return $true
})]
[System.IO.FileInfo]$Path
)
Get-Item $Path -Force
}
You could write a second ParameterSet to include a -LiteralPath param for added robustness - this would allow the user to pass a path that included wildcard chars (e.g. 'C:\Test[txt]File.txt').
Using <TAB> to autocomplete paths with wildcards would still work with the example above - but it wouldn't allow the user to type a full path name with unescaped wildcards in it, hence the need for both -Path and -LiteralPath parameters in that case:
'.\Test`[txt`]File.txt' would work with -Path (if using <TAB> to autocomplete path)
'C:\Test`[txt`]File.txt' would work with -Path (if using <TAB> to autocomplete path)
'C:\Test[txt]File.txt' would not work with -Path (if manually typed)

Powershell: Replace strings in all .ini files recursively

I've been wanting an easy to use script that will allow me to replace multiple strings from multiple files for a while. So far I've got this code:
$replacements = #{
'bCompressDiffuseLocalPlayerCharacterTextures=True' = 'bCompressDiffuseLocalPlayerCharacterTextures=False'
'bCompressDiffuseLocalPlayerVehicleTextures=True' = 'bCompressDiffuseLocalPlayerVehicleTextures=False'
'bCompressDiffuseOtherPlayerCharacterTextures=True' = 'bCompressDiffuseOtherPlayerCharacterTextures=False'
'bCompressDiffuseOtherPlayerVehicleTextures=True' = 'bCompressDiffuseOtherPlayerVehicleTextures=False'
'bCompressNormalTextures=True' = 'bCompressNormalTextures=False'
'bDisablePhysXHardwareSupport=True' = 'bDisablePhysXHardwareSupport=False'
'bEnableMouseSmoothing=True' = 'bEnableMouseSmoothing=False'
'bInitializeShadersOnDemand=True' = 'bInitializeShadersOnDemand=False'
'MaxChannels=32' = 'MaxChannels=64'
'MotionBlur=True' = 'MotionBlur=False'
'm_bCalculateOnServer=True' = 'm_bCalculateOnServer=False'
'OneFrameThreadLag=True' = 'OneFrameThreadLag=False'
'PoolSize=140' = 'PoolSize=1024'
'UseMinimalNVIDIADriverShaderOptimization=True' = 'UseMinimalNVIDIADriverShaderOptimization=False'
'UseTextureFileCache=False' = 'UseTextureFileCache=True'
}
function Update-FileContent {
[cmdletbinding()]
param(
[Parameter(ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
Mandatory=$true,
Position=0)]
[Alias('PsPath')]
$Path
)
$lines = Get-Content $Path
$lines | ForEach-Object {
foreach($rep in $replacements.Keys)
{
$_ = $_ -replace $rep, $replacements[$rep]
}
$_
} | Set-Content $Path
}
Get-ChildItem -Recurse *.ini | Update-FileContent
It works but only if a file is 1 directory deep.
I'd do something like this:
$replacements = #{
'bCompressDiffuseLocalPlayerCharacterTextures=True' = 'bCompressDiffuseLocalPlayerCharacterTextures=False'
'bCompressDiffuseLocalPlayerVehicleTextures=True' = 'bCompressDiffuseLocalPlayerVehicleTextures=False'
'bCompressDiffuseOtherPlayerCharacterTextures=True' = 'bCompressDiffuseOtherPlayerCharacterTextures=False'
'bCompressDiffuseOtherPlayerVehicleTextures=True' = 'bCompressDiffuseOtherPlayerVehicleTextures=False'
'bCompressNormalTextures=True' = 'bCompressNormalTextures=False'
'bDisablePhysXHardwareSupport=True' = 'bDisablePhysXHardwareSupport=False'
'bEnableMouseSmoothing=True' = 'bEnableMouseSmoothing=False'
'bInitializeShadersOnDemand=True' = 'bInitializeShadersOnDemand=False'
'MaxChannels=32' = 'MaxChannels=64'
'MotionBlur=True' = 'MotionBlur=False'
'm_bCalculateOnServer=True' = 'm_bCalculateOnServer=False'
'OneFrameThreadLag=True' = 'OneFrameThreadLag=False'
'PoolSize=140' = 'PoolSize=1024'
'UseMinimalNVIDIADriverShaderOptimization=True' = 'UseMinimalNVIDIADriverShaderOptimization=False'
'UseTextureFileCache=False' = 'UseTextureFileCache=True'
}
function Update-FileContent {
[cmdletbinding()]
param(
[Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Mandatory=$true, Position=0)]
[Alias('PsPath')]
$Path
)
# read the ini file in as one single string
$content = Get-Content $Path -Raw
# copy that string so we can compare at the end of the loop if anything has changed at all
$newContent = $content
foreach($rep in $replacements.Keys) {
$newContent = $newContent -replace $rep, $replacements[$rep]
}
# only replace the contents of the ini file if changes are made
if ($newContent -ne $content) {
Write-Host "Replacing content of file '$Path'"
Set-Content $Path -Value $content
}
}
$rootPath = '<PATH OF THE ROOTFOLDER TO RECURSE WHERE THE INI FILES ARE LOCATED>'
Get-ChildItem -Path $rootPath -Filter '*.ini' -Recurse | ForEach-Object { Update-FileContent $_.FullName }

Dynamic Parameters - with Dynamic ValidateSet

I have a script that I've been working on to provide parsing of SCCM log files. This script takes a computername and a location on disk to build a dynamic parameter list and then present it to the user to choose the log file they want to parse. Trouble is I cannot seem to get the ValidateSet portion of the dynamic parameter to provide values to the user. In addition the script won't display the -log dynamic parameter when attempting to call the function.
When you run it for the first time you are not presented with the dynamic parameter Log as I mentioned above. If you then use -log and then hit tab you’ll get the command completer for the files in the directory you are in. Not what you’d expect; you'd expect that it would present you the Logfile names that were gathered during the dynamic parameter execution.
PSVersion 5.1.14409.1012
So the question is how do I get PowerShell to present the proper Validate set items to the user?
If you issue one of the items in the error log you get the proper behavior:
Here are the two functions that i use to make this possible:
function Get-CCMLog
{
[CmdletBinding()]
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
if(test-path $FilePath)
{
$logs = gci "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
}
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
# Your code goes here
#dir -Path $Path
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
[PSCustomObject]#{"$($log)Log"=$results}
}
}
function Get-CMLog
{
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path -Path $File -Leaf
if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-cotnet -path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]#{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
The problem is that you have all the dynamic logic inside scriptblock in the if statement, and it handles the parameter addition only if the path provided contains a semicolon (':').
You could change it to something like:
if($path.ToCharArray() -contains ':') {
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
} else {
$FilePath = $path
}
and continue your code from there
PS 6 can do a dynamic [ValidateSet] with a class:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#dynamic-validateset-values

How do I get a path with the correct (canonical) case in PowerShell?

I have a script that accepts a directory as an argument from the user. I'd like to display the name of the directory path as it is displayed in Windows. I.e.,
PS C:\SomeDirectory> cd .\anotherdirectory
PS C:\AnotherDirectory> . .\myscript.ps1 "c:\somedirectory"
C:\SomeDirectory
How do I retrieve "C:\SomeDirectory" when given "c:\somedirectory"?
The accepted answer only gets the correct case of the file. Parent paths are left with the case provided by the user. Here's my solution.
$getPathNameSignature = #'
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern uint GetLongPathName(
string shortPath,
StringBuilder sb,
int bufferSize);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError=true)]
public static extern uint GetShortPathName(
string longPath,
StringBuilder shortPath,
uint bufferSize);
'#
$getPathNameType = Add-Type -MemberDefinition $getPathNameSignature -Name GetPathNameType -UsingNamespace System.Text -PassThru
function Get-PathCanonicalCase
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]
# Gets the real case of a path
$Path
)
if( -not (Test-Path $Path) )
{
Write-Error "Path '$Path' doesn't exist."
return
}
$shortBuffer = New-Object Text.StringBuilder ($Path.Length * 2)
[void] $getPathNameType::GetShortPathName( $Path, $shortBuffer, $shortBuffer.Capacity )
$longBuffer = New-Object Text.StringBuilder ($Path.Length * 2)
[void] $getPathNameType::GetLongPathName( $shortBuffer.ToString(), $longBuffer, $longBuffer.Capacity )
return $longBuffer.ToString()
}
I've integrated the above code into Resolve-PathCase, part of the Carbon PowerShell module. Disclaimer: I'm the owner/maintainer of Carbon.
This should work:
function Get-PathCanonicalCase {
param($path)
$newPath = (Resolve-Path $path).Path
$parent = Split-Path $newPath
if($parent) {
$leaf = Split-Path $newPath -Leaf
(Get-ChildItem $parent| Where-Object{$_.Name -eq $leaf}).FullName
} else {
(Get-PSDrive ($newPath -split ':')[0]).Root
}
}
I found a different and simpler approach using PowerShell wild cards.
$canonicalCasePath = Get-ChildItem -Path $wrongCasingPath.Replace("\","\*") | Where FullName -IEQ $wrongCasingPath | Select -ExpandProperty FullName
The first part of the pipe replaces all backslashes in the path by
backslash and asterisk \ → \* and return all matching files
The where part makes sure that only the desired file is returned and
not any other potential match. IEQ is case insesitive equal
The last select part extracts canonical case path
of the file
Using Christian's GetDirectories suggestion, here's another solution that's not quite as involved:
function Get-PathCanonicalCase
{
param( $path )
$newPath = (Resolve-Path $path).Path
$root = [System.IO.Path]::GetPathRoot( $newPath )
if ( $newPath -ne $root ) # Handle case where changing to root directory
{ $newPath = [System.IO.Directory]::GetDirectories( $root, $newPath.Substring( $root.Length ) )[ 0 ] }
$newPath
}
EDIT: Thanks for all the help.
Btw, all I wanted this for was to use in a little utility script overriding the default cd alias, allowing me to specify some 'root' directories that are searched if the path doesn't exist relative to the current directory. I.e., it allows me to cd Documents, cd trunk, cd Release-10.4 regardless of my current location. And it annoyed me to have the prompt in the case that I entered it, instead of its actual case.
# Usage:
# Set up in $profile - define the functions and reassign 'cd'. Example:
# -----
# . .\Set-LocationEx.ps1 "c:\dev\Code", "c:\dev\Code\releases", "$HOME" -Verbose
# if (test-path alias:cd) { remove-item alias:cd > $null }
# Set-Alias cd Set-LocationEx
# -----
param( [parameter(Mandatory = $true)][string[]]$roots )
Set-StrictMode -Version Latest
Write-Verbose "Set-LocationEx roots: $(Join-String -Strings $roots -Separator ', ')"
function Set-LocationEx
{
param( [Parameter( Mandatory="true" )]$path )
process
{
$verbose = ( $PSCmdlet.MyInvocation.BoundParameters.ContainsKey( "Verbose" ) -and $PSCmdlet.MyInvocation.BoundParameters[ "Verbose" ].IsPresent )
if ( $verbose )
{ Write-Verbose( "$(Join-String -Strings $roots -Separator ', ')" ) }
if ( !( Test-Path $path ) )
{
foreach ( $p in $roots )
{
$newPath = Join-Path $p $path
if ( $verbose ) { Write-Verbose "Looking for $newPath" }
if ( Test-Path $newPath )
{
$newPath = Get-PathCanonicalCase( $newPath )
if ( $verbose ) { Write-Verbose "Found $newPath" }
Push-Location $newPath
return
}
}
}
if ( Test-Path $path )
{ $path = Get-PathCanonicalCase( $path ) }
Push-Location $path
}
}
function Get-LocationExRoots
{
process
{
Write-Output (Join-String -Strings $roots -NewLine)
}
}
function Get-PathCanonicalCase
{
param( $path )
$newPath = (Resolve-Path $path).Path
$root = [System.IO.Path]::GetPathRoot( $newPath )
if ( $newPath -ne $root ) # Handle root directory
{ $newPath = [System.IO.Directory]::GetDirectories( $root, $newPath.Substring( $root.Length ) )[ 0 ] }
$newPath
}