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

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)

Related

Pass [switch] inside Start-Job

There is a script for users to log in, it calls other scripts in turn, depending on the conditions.
In order to call scripts separately manually, the [switch]$Silent parameter has been added. Question - how to pass this parameter inside Start-Job? I tried to add to the list of arguments in different ways - the value always falls into the neighboring parameter, regardless of the order.
Main script example
Param(
[string]$location = 'C:\Users',
[switch]$Silent
)
Start-Job -FilePath ".\Fonts_Install.ps1" -ArgumentList ($Silent,$location) | Wait-Job
Fonts_Install.ps1
Param(
[switch]$Silent = $false,
[string]$location = '.'
)
$path_fonts = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts"
$Registry = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
function WriteLog {
Param ([string]$LogString)
$Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
$LogMessage = "$Stamp $LogString"
Add-content $LogFile -value $LogMessage
}
$Logfile = "$env:LOCALAPPDATA\Temp\fonts_install.log"
WriteLog "Silent $Silent"
WriteLog "location $location"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName PresentationCore
$SourceFolder = "$location\Fonts_Install"
$WindowsFonts = [System.Drawing.Text.PrivateFontCollection]::new()
$Fonts = Get-ChildItem -Path $SourceFolder -Include *.ttf, *.otf -Recurse -File
ForEach ($Font in $Fonts) {
$Font_Name = $Font.Name
$font_fullname = $Font.fullname
if (Test-Path -PathType Leaf -Path "$path_fonts\$Font_Name") {
WriteLog "Previously installed $Font_Name"
}
else {
Copy-Item $Font -Destination "$path_fonts" -Force -Confirm:$false -PassThru
$WindowsFonts.AddFontFile("$font_fullname")
$ValueFont = "$path_fonts" + "\" + "$Font_Name"
$Typeface = New-Object -TypeName Windows.Media.GlyphTypeface -ArgumentList "$font_fullname"
[string]$FamilyFaceNames = $Typeface.FamilyNames.Values + $Typeface.FaceNames.Values
$RegistryValue = #{
Path = $Registry
Name = $FamilyFaceNames
Value = $ValueFont
}
if (Test-Path $Registry\$FamilyFaceNames) {
Remove-ItemProperty -name $FamilyFaceNames -path $Registry
}
New-ItemProperty #RegistryValue
WriteLog "New fonts installed $Font_Name"
}
}
switch ($Silent) {
$false {
if ($Error.Count -gt 0) {
for ($i = 0; $i -le ($Error.Items.Count + 1); $i++) {
$errMSG = "$Error"
}
[System.Windows.Forms.MessageBox]::Show("$errMSG", "Error", "OK", "Error")
}
else {
[System.Windows.Forms.MessageBox]::Show("ок", "Fonts", "OK", "Asterisk") | out-null
}
}
}
Unfortunately, specifying pass-through arguments via Start-Job's -ArgumentList (-Args) is limited to positional arguments, which prevents binding [switch] parameters, whose arguments must by definition be named.
As a workaround, instead of using -FilePath, invoke your script via the -ScriptBlock parameter. Inside of a script block ({ ... }, named arguments may be used in script calls, as usual:
Start-Job -ScriptBlock {
# Set the current location to the same location as the caller.
# Note: Only needed in *Windows PowerShell*.
Set-Location -LiteralPath ($using:PWD).ProviderPath
.\Fonts_Install.ps1 -Silent:$using:Silent $using:Location
} | Receive-Job -Wait -AutoRemoveJob
Note the use of the $using: scope in order to embed variable values from the caller's scope in the script block that will execute in the background.
You still need to refer to the -Silent parameter by name, and the whether the switch is on or off can be communicated by appending :$true or :$false to it, which is what :$using:Silent does.
In Windows PowerShell, background jobs execute in a fixed location (working directory), namely the user's Documents folder, hence the Set-Location call to explicitly use the same location as the caller, so that the script file can be referenced by a relative path (.\). This is no longer necessary in PowerShell (Core) 7+, which now thankfully uses the same location as the calller.
Here is a different alternative to mklement0's helpful answer, this answer does not use Start-Job and uses a PowerShell instance instead, using this method we can leverage the automatic variable $PSBoundParameters.
Do note, that for this to work properly, both .ps1 scripts must share the same parameter names or Alias Attribute Declarations that matches the same parameter from the caller. See this answer for more details.
You can use these snippets below as a example for you to test how it works.
caller.ps1
param(
[string] $Path = 'C:\Users',
[switch] $Silent
)
try {
if(-not $PSBoundParameters.ContainsKey('Path')) {
$PSBoundParameters['Path'] = $Path
}
$ps = [powershell]::Create().
AddCommand('path\to\myScript.ps1').
AddParameters($PSBoundParameters)
$iasync = $ps.BeginInvoke()
# Do something else here while the .ps1 runs
# ...
# Get async result from the PS Instance
$ps.EndInvoke($iasync)
}
finally {
if($ps -is [IDisposable]) {
$ps.Dispose()
}
}
myScript.ps1
# Note, since we're bounding this parameters from the caller.ps1,
# We don't want to assign Default Values here!
param(
[string] $Path,
[switch] $Silent
)
foreach($param in $MyInvocation.MyCommand.Parameters.Keys) {
[pscustomobject]#{
Parameter = $param
Value = Get-Variable $param -ValueOnly
}
}
A few examples:
PS /> .\caller.ps1
Parameter Value
--------- -----
Path C:\Users
Silent False
PS /> .\caller.ps1 -Path hello
Parameter Value
--------- -----
Path hello
Silent False
PS /> .\caller.ps1 -Path world -Silent
Parameter Value
--------- -----
Path world
Silent True

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

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/

Powershell bind argument path

When I execute a particular script, the Write-Log function throws an error. This appears to be before I call the function. It is loaded from an external file. This problem is new, it worked previously with no issues.
Error
There are two errors, split by a VERBOSE output statement. The 2nd VERBOSE line is the last script line I included. The format (not colours) is the best I can recreate
C:\Scripts>powershell -file tca_export_tt_ftps.ps1
Test-Path : Cannot bind argument to parameter 'Path' because it is an empty string.
At C:\scripts\Function-Write-Log.ps1:75 char:24
+ if ((Test-Path $Path) -AND $NoClobber) {
+ ~~~~~
+ CategoryInfo : InvalidData: (:) [Test-Path], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAl lowed,Microsoft.PowerShell.Commands.TestPathCommand
VERBOSE: Testing C:\Scripts\log\ exists
Out-File : Cannot bind argument to parameter 'FilePath' because it is an empty string.
At C:\scripts\Function-Write-Log.ps1:110 char:67
+ ... FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
+ ~~~~~
+ CategoryInfo : InvalidData: (:) [Out-File], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAl lowed,Microsoft.PowerShell.Commands.OutFileCommand
VERBOSE: Logfile is: C:\Scripts\log\kh_tca_out_20210222.log
Start of my Script
Function Upload-TCA
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false)]
[Alias('StartDate')]
[Alias('SeedDate')]
[DateTime]$dateSeed = (Get-Date),
#[string]$dateSeedStr = (Get-Date -format "yyyy-MM-dd"),
[Parameter(Mandatory=$false)]
[Alias('DisableFtp')]
[bool]$ftpDisabled=$false,
[Parameter(Mandatory=$false)]
[Alias('DisplayConfig')]
[bool]$emitConfig = $false,
[Parameter(Mandatory=$true)]
[string]$orgCode
)
#########################################################################
# Function Imports
#########################################################################
. c:\scripts\Function-Generate-Folder.ps1
. c:\scripts\Function-Write-Log.ps1
. c:\scripts\Function-Upload-WinSCP-FTPS.ps1
. c:\scripts\Function-Create-CredentialFromFile.ps1
#########################################################################
# Logging Setup
#########################################################################
$logDir = "C:\Scripts\log\"
Generate-Folder $logDir
$logName = "$orgCode_tca_out_$(Get-Date -Format "yyyyMMdd").log"
$logFile = Join-Path -Path $logDir -ChildPath $logName
Write-Log "Logfile is: $logFile" -Path $logFile
Write-Log
The Write-Log function is from the Microsoft Script Center Included here in case the link is broken
<#
.Synopsis
Write-Log writes a message to a specified log file with the current time stamp.
.DESCRIPTION
The Write-Log function is designed to add logging capability to other scripts.
In addition to writing output and/or verbose you can write to a log file for
later debugging.
.NOTES
Created by: Jason Wasser #wasserja
Modified: 11/24/2015 09:30:19 AM
Changelog:
* Code simplification and clarification - thanks to #juneb_get_help
* Added documentation.
* Renamed LogPath parameter to Path to keep it standard - thanks to #JeffHicks
* Revised the Force switch to work as it should - thanks to #JeffHicks
To Do:
* Add error handling if trying to create a log file in a inaccessible location.
* Add ability to write $Message to $Verbose or $Error pipelines to eliminate
duplicates.
.PARAMETER Message
Message is the content that you wish to add to the log file.
.PARAMETER Path
The path to the log file to which you would like to write. By default the function will
create the path and file if it does not exist.
.PARAMETER Level
Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER NoClobber
Use NoClobber if you do not wish to overwrite an existing file.
.EXAMPLE
Write-Log -Message 'Log message'
Writes the message to c:\Logs\PowerShellLog.log.
.EXAMPLE
Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
Writes the content to the specified log file and creates the path and file specified.
.EXAMPLE
Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
.LINK
https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
#>
function Write-Log
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
[Parameter(Mandatory=$false)]
[Alias('LogPath')]
[string]$Path='C:\Logs\PowerShellLog.log',
[Parameter(Mandatory=$false)]
[ValidateSet("Error","Warn","Info")]
[string]$Level="Info",
[Parameter(Mandatory=$false)]
[switch]$NoClobber
)
Begin
{
# Set VerbosePreference to Continue so that verbose messages are displayed.
$VerbosePreference = 'Continue'
}
Process
{
# If the file already exists and NoClobber was specified, do not write to the log.
if ((Test-Path $Path) -AND $NoClobber) {
Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
Return
}
# If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
elseif (!(Test-Path $Path)) {
Write-Verbose "Creating $Path."
$NewLogFile = New-Item $Path -Force -ItemType File
}
else {
# Nothing to see here yet.
}
# Format Date for our Log File
$FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write message to error, warning, or verbose pipeline and specify $LevelText
switch ($Level) {
'Error' {
Write-Error $Message
$LevelText = 'ERROR:'
}
'Warn' {
Write-Warning $Message
$LevelText = 'WARNING:'
}
'Info' {
Write-Verbose $Message
$LevelText = 'INFO:'
}
}
# Write log entry to $Path
"$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
}
End
{
}
}
I hadn't read the Generate-Folder method correctly. I changed the calls in there for Write-Log to Write-Host. It was trying to use the log before it was fully setup.
Function Generate-Folder()
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[string]$path='C:\scripts\logs'
)
Write-Host "Testing $path exists" # cant use write-log as not setup yet
$global:foldPath=$null
foreach($foldername in $path.split("\"))
{
$global:foldPath+=($foldername+"\")
if(!(Test-Path $global:foldPath))
{
New-Item -ItemType Directory -Path $global:foldPath
Write-Host "$global:foldPath Folder Created Successfully" #-Path $logFile
}
}
}

Expand-Archive without Importing and Exporting files

How do I stop exporting then importing files in and out of Powershell when working with .zip files (Expand-Archive)?
I am currently using a temporary folder to extract the .zip file.
Is there a variable or something I missed that would work better than the solution below?
$filename = 'foobar'
$Zip_in_Bytes | Set-Content -Encoding Byte -Path "C:\temp\filename.zip"
Expand-Archive -Path "C:\temp\filename.zip" -DestinationPath "C:\temp\" -Force
[xml]$xml = Get-Content -Path "C:\temp\filename.xml"
Remove-Item "C:\temp\filename.zip"
Remove-Item "C:\temp\filename.xml"
Expand-Archive only support paths parameters, not objects
Is there a better way to handle .zip files?
Using System.IO.Compression you can work with byte arrays and streams rather than temporary files, but it's a bit more work than Expand-Archive.
EDIT: Added Get-ZipEntryContent and Add-ZipEntry sample calls, and tweaked parameters making $ZipFilePath optional.
#( 'System.IO.Compression','System.IO.Compression.FileSystem') | % { [void][System.Reflection.Assembly]::LoadWithPartialName($_) }
function Get-ZipEntryContent(#returns the bytes of the first matching entry
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::Open)),
[string] $EntryPath){
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Read)
$buf = New-Object byte[] (0) #return an empty byte array if not found
$ZipArchive.GetEntry($EntryPath) | ?{$_} | %{ #GetEntry returns first matching entry or null if there is no match
$buf = New-Object byte[] ($_.Length)
Write-Verbose " reading: $($_.Name)"
$_.Open().Read($buf,0,$buf.Length)
}
$ZipArchive.Dispose()
$ZipStream.Close()
$ZipStream.Dispose()
return $buf
}
function Add-ZipEntry(#Adds an entry to the $ZipStream. Sample call: Add-ZipEntry -ZipFilePath "$PSScriptRoot\temp.zip" -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("Testing"))
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::OpenOrCreate)),
[string] $EntryPath,
[byte[]] $Content,
[switch] $OverWrite, #if specified, will not create a second copy of an existing entry
[switch] $PassThru ){#return a copy of $ZipStream
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Update, $true)
$ExistingEntry = $ZipArchive.GetEntry($EntryPath) | ?{$_}
If($OverWrite -and $ExistingEntry){
Write-Verbose " deleting existing $($ExistingEntry.FullName)"
$ExistingEntry.Delete()
}
$Entry = $ZipArchive.CreateEntry($EntryPath)
$WriteStream = New-Object System.IO.StreamWriter($Entry.Open())
$WriteStream.Write($Content,0,$Content.Length)
$WriteStream.Flush()
$WriteStream.Dispose()
$ZipArchive.Dispose()
If($PassThru){
$OutStream = New-Object System.IO.MemoryStream
$ZipStream.Seek(0, 'Begin') | Out-Null
$ZipStream.CopyTo($OutStream)
}
$ZipStream.Close()
$ZipStream.Dispose()
If($PassThru){$OutStream}
}
Here's an example of how you would call Add-ZipEntry and Get-ZipEntryContent functions completely in memory:
$NewZipStream = Add-ZipEntry -ZipStream (New-Object IO.MemoryStream) -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("<xml><test>1</test>")) -PassThru
$bytes = Get-ZipEntryContent -ZipStream $NewZipStream -EntryPath 'Test.xml'
[text.encoding]::UTF8.GetString($bytes)
Although Expand-Archive doesn't accept objects, you can provide it the string property of the objects. E.G. $File.FullName will be a String.
Get-ChildItem C:\temp\ -Filter "*.zip" |
ForEach-Object {
Expand-Archive -Path $_.FullName -DestinationPath "C:\Temp\Extracted\$($_.BaseName)\"
}

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
}