I want to generate a string for a file path inside a powershell script. I want this to work in both in windows and mac.
At the moment the code is hardcoded to windows like paths ("\" -> windows, "/" -> unix):
$templatep="$CoreRoot\templates\$serviceName"
I changed this to:
$templatep= Join-Path $CoreRoot "templates" $serviceName
And it works in mac with Powershell 6.0. BUT it doesn't work in my windows server with Powershell 4. I have to do something like this:
$templatep= Join-Path $CoreRoot -ChildPath "templates" | Join-Path -ChildPath $serviceName
Any idea why this is just working in my mac? Is this a new feature in powershell 5 or 6?
I dont't like the having to pipe multiple Join-Paths. Is there a better way to do this?
Thanks!
First, a workaround using the .NET framework:
[IO.Path]::Combine('a', 'b', 'c')
This yields a/b/c on Unix, and a\b\c on Windows, and conveniently supports any number of path components.
Note:
This workaround is only for filesystem paths, whereas Join-Path is designed to work with any PowerShell drive provider's paths.
Make sure that no component other than the first starts with \ (Windows) or / (Unix), because any preceding component is then ignored; e.g., on Windows:
[IO.Path]::Combine('\a', '\b', 'c') # -> '\b\c' - '\a' is ignored(!)
Note that Join-Path does not exhibit this behavior; see this answer for details.
As an alternative to sequencing Join-Path calls with a pipeline you can simply use (...) (a subexpression):
Join-Path a (Join-Path b c) # -> 'a\b\c' (on Windows)
The syntax displayed by Join-Path -? as of Windows PowerShell v5.1.14393.693 (incidental parameters omitted):
Join-Path [-Path] <String[]> [-ChildPath] <String> ...
This syntax implies that invocation Join-Path a b c results in a syntax error in Windows PowerShell, because there is no parameter to bind the c argument to.
By contrast, the syntax displayed in PowerShell (Core) v6+ reveals an additional parameter:
Join-Path [-Path] <String[]> [-ChildPath] <String> [[-AdditionalChildPath] <String[]>]
It is the additional -AdditionalChildPath parameter, which is declared in a manner that collects all remaining positional arguments that (ValueFromRemainingArguments), that makes specifying an arbitrary number of child components work, so that Join-Path a b c indeed works, for instance.
Unfortunately, this enhancement won't be back-ported to Windows PowerShell.
Note that even though [-Path] <String[]> is an array parameter, its purpose is not to accept multiple child path components of a single output path, but to allow joining of multiple parent-child path pairs; e.g.:
$ Join-Path a,b c # same as: Join-Path -Path a,b -ChildPath c
a\c
b\c
Finally, even you can typically get away with hard-coding / as the path separator on both platforms, because many Windows API functions as well as PowerShell's own cmdlets accept \ and / interchangeably.
However, not all utilities may behave this way, so it's generally safer to use the platform-appropriate separator.
For instance, the following works just fine on Windows:
Get-Item c:/windows/system32 # same as: Get-Item c:\windows\system32
Related
PS C:\Users\robert.lee> new-item C:\temp\junk\delete-me
PS C:\Users\robert.lee> $d=get-childitem C:\temp\junk -file
PS C:\Users\robert.lee> remove-item $d
/* This does NOT delete C:\temp\junk\delete-me, but deletes C:\Users\robert.lee\delete-me. */
PS C:\Users\robert.lee> $d | remove-item
/* This does delete C:\temp\junk\delete-me. */
Why remove-item $d does not know where to find the file (C:\temp\junk) while $d | remove-item does? The object $d has the full path in either case.
PS: remove-item $d does remove file in another location, as tested on PSVersion 7.2.1 on macOS Monterey:
PS /Users/robert.lee/m/Windows> $d=get-childitem /tmp/Junk2 -file
PS /Users/robert.lee/m/Windows> ls /tmp/Junk2
delete-me
PS /Users/robert.lee/m/Windows> remove-item $d
PS /Users/robert.lee/m/Windows> ls /tmp/Junk2
PS /Users/robert.lee/m/Windows>
To complement Abraham Zinala's helpful answer with some background information:
From a usability perspective, $d | Remove-Item and Remove-Item $d should work the same; the reasons they don't are (written as of PowerShell Core 7.2.3):
Argument-based parameter binding is, unfortunately, less sophisticated than pipeline-based binding:
Notably, the ValueFromPipelineByPropertyName property on the -LiteralPath parameter, whose alias is -PSPath, is only honored for input objects provided via the pipeline ($d | Remove-Item) and not also when provided as an argument (Remove-Item $d).
With pipeline input, it is the .PSPath property that PowerShell's provider cmdlets decorate their output objects with that robustly binds to -LiteralPath, because the .PSPath property value is a full, provider-qualified path - see this answer for a detailed explanation.
This binding asymmetry is the subject of GitHub issue #6057.
If you pass a file- or directory-info object as an argument and you do so positionally (i.e. without preceding the argument with the target-parameter name):
It is the wildcard-based -Path parameter that is targeted:
This is normally not a problem, but does become one if the intended-as-literal path happens to contain [ characters (e.g., c:\foo\file[1].txt), given that [ is a metacharacter in PowerShell's wildcard language.
Using a named argument that explicitly targets the -LiteralPath parameter avoids this problem (note that PowerShell (Core) 7+ allows you to shorten to -lp):
# Note: Still not robust in *Windows PowerShell* - see below.
Remove-Item -LiteralPath $d
Both -Path and -LiteralPath are [string[]]-typed, i.e. they accept an array of strings, which in argument-based parameter binding means that any non-string argument is simply stringified (loosely speaking, like calling .ToString() on it)[1]:
In Windows PowerShell - as you've experienced - this can lead to subtle but potentially destructive bugs - because System.IO.FileInfo and System.IO.DirectoryInfo instances situationally stringify to only their .Name property, not to their .FullName property - see this answer for details.
In PowerShell (Core) 7+ this is no longer a problem - stringification now consistently uses .FullName.
However, stringification can still lead to failure for items returned by a PowerShell provider other than the file-system one, such as the registry provider:
# !! The 2nd command fails, because $regItem stringifies to
# !! "HKEY_CURRENT_USER\Console" (the registry-native path),
# !! which Get-Item then misinterprets as a relative file-system path.
$regItem = Get-Item HKCU:\Console
Get-Item $regItem
The upshot:
For target cmdlets that have -Path / -LiteralPath parameters, provide arguments that are provider items, such as output by Get-ChildItem and Get-Item:
preferably via the pipeline:
# Robust, in both PowerShell editions.
Get-ChildItem C:\temp\junk -file | Remove-Item
If you must use an argument, target -LiteralPath explicitly, and use the .PSPath property:
# Robust, in both PowerShell editions.
$d = Get-ChildItem C:\temp\junk -file
Remove-Item -LiteralPath $d.PSPath
If, by contrast, you need to pass the paths of provider items to .NET APIs or external programs:
Since .PSPath values contain a PowerShell provider prefix (e.g., Microsoft.PowerShell.Core\FileSystem::) in front of the item's native path, such values aren't directly understood by outside parties.
The robust approach is to pipe a provider item to Convert-Path in order to convert it to a native path in isolation (piping again results in robust .PSPath binding to -LiteralPath):
$d | Convert-Path # -> 'C:\temp\junk\delete-me'
This also works with path strings that are based on PowerShell-only drives, e.g:
Convert-Path -LiteralPath HKCU:\Console # -> 'HKEY_CURRENT_USER\Console'
[1] For most .NET types, PowerShell indeed calls .ToString(), but requests culture-invariant formatting, where supported, via the IFormattable interface; for select types, notably arrays and [pscustomobject], it uses custom stringification in lieu of calling .ToString() - see this answer for details.
PowerShell can only do so much for you, and one thing it can't do, is guess what you're after all the time. The issue you're having comes from the parameter binding going on behind the scenes. So, the binding with Remove-Item $g only happens to the Name property from the [FileInfo] object passed to it, which will coerce the object into a string and then tie it to the current working directory binding it to Path.
Whereas, $g | Remove-Item, the binding from the pipeline is bound using a different procedure and takes the PSPath instance member and binds it to the LiteralPath which is the FullName property in your [FileInfo] object.
Still coerces the object into an object of [string], just using a different property.
Sauce for an okay understanding of ParameterBinding.
Using Trace-Command, you can get a much better understanding of this.
Long story short, use $g.FullName for both scenarios as it will output an object of [string], and both Path parameters accept the entirety of the value, by pipeline as well.
I want to create an alias in Windows PowerShell to delete multiple folders from the command line.
To remove more than one item I call:
Remove-Item '.\Documents\*\Bin\' ,'.\Documents\*\Inter\' -Force -Recurse
I have tried to create the alias like this:
New-Alias -Name 'Clean-RCD' Remove-Item '.\Documents\*\Bin\' ,'.\Documents\*\Inter\' -Force -Recurse
Output:
New-Alias: A positional parameter cannot be found that accepts argument 'System.Object[]'.
Any idea how to define this alias correctly?
Unlike in bash, aliases in PowerShell are strict 1-to-1 command name mappings - no extra parameter arguments allowed.
You'll want to create a function instead:
function Clean-RCD {
Remove-Item -Path '~\Documents\*\Bin', '~\Documents\*\Inter\' -Force -Recurse
}
Use of ~ (which resolves to your home folder) over . is intentional - this way it'll still work if you've navigated to a different path
For reasons I'm starting to regret, I got into the habit of just working with [System.IO.FileInfo] and now I'm wondering if there is a best practice around avoiding that and just using full names to the file(s) or if there is a different workaround for my current conundrum.
I need to make a lot of my smaller scripts work with powershell.exe vs. pwsh.exe because they're going to be used by folks and computers that don't have PowerShell (Core) installed - but every once in a while there arises an issue. This time it is the handling of whatever is returned from Get-ChildItem and the fact that Windows PowerShell doesn't give you the full path like PowerShell (Core) does. One workaround I have would be to force the full name ($file.FullName), but that in turn breaks the fact that I'm accustomed to working with System.IO.FileInfo variables.
So first question without examples: What is the best practice? Should I have been using System.IO.FileInfo in the first place?
Second question, with examples: Is there a better way to handle this so that Windows PowerShell and PowerShell (Core) act consistently?
Consider the following - at this point I would probably call a function to act on each qualifying input file (using filtering on name or file extension, etc. to get the right set).
PS C:\tmp> Function CustomFunction{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.IO.FileInfo]$inputFile
)
$inputFile.BaseName
$inputFile.DirectoryName
$inputFile.GetType()
"`n`n"
}
PS C:\tmp> (Get-ChildItem -LiteralPath $PWD -File).ForEach({CustomFunction $_})
In PowerShell (Core) - The type System.IO.FileSystemInfo and the function would work even if the file itself isn't located in the working directory
Another File
C:\tmp
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False FileInfo System.IO.FileSystemInfo
Windows PowerShell is still using a type of System.IO.FileSystemInfo - but there is definitely something different between them. I'm not sure what "IsSerial" actually checks, but if CustomFunction were taking action on the files then it won't work if they're not in the working directory.
Another File
C:\tmp
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
So - what's my best course of action? I like the FileInfo objects themselves because they have some handy properties like BaseName, Directory, DirectoryName, and Extension. There are probably also a number of useful methods available that I might be using in my functions.
If I just essentially pass $_.FullName to the function, then within the function it is a string and I'll need to use Split-Path among other things to get similar properties that I'm working with
Yes, the inconsistent stringification of [System.IO.FileInfo] instances in Windows PowerShell is unfortunate - sometimes by file name only, other times by full path (everything in this answer applies analogously to System.IO.DirectoryInfo instances) - this has since been fixed in PowerShell (Core) 7+ (via an underlying .NET API change), where such instances now consistently stringify to their full path.
To make code work robustly in both PowerShell editions, two fundamental scenarios apply:
Passing [System.IO.FileInfo] instances to other file-processing cmdlets, in which case you can provide them as pipeline input - see the next section.
Passing them to external programs and .NET methods, in which case their .FullName property must be used - see the bottom section.
When you need to pass Get-ChildItem / Get-Item to other file-processing cmdlets, namely cmdlets that have pipeline-binding -LiteralPath parameters, such as Get-Item and Get-ChildItem you can stick with [System.IO.FileInfo] instances and provide them as pipeline input ; e.g.:
# Robust alternative to the following *brittle* commands:
# Get-ChildItem $someFileInfoInstance # -Path implied
# Get-ChildItem -LiteralPath $someFileInfoInstance
# Same as:
# Get-ChildItem -LiteralPath $someFileInfoInstance.PSPath
$someFileInfoInstance | Get-ChildItem
A System.IO.FileInfo bound this way - assuming that it wasn't constructed directly - binds via its PowerShell provider-supplied .PSPath property value, which is always its full path, even in Windows PowerShell.
The reason that it is the .PSPath property value that binds is that -PSPath is defined as an alias of the -LiteralPath parameter, combined with declaring the latter as ValueFromPipelineByPropertyName.
Note:
Generally, all standard cmdlets that have a -LiteralPath parameter support this mechanism.
See the bottom section of this answer for how to infer whether the mechanism is supported by a given cmdlet by examining its parameter definitions.
An unfortunate exception due to its buggy implementation in Windows PowerShell is Import-Csv - again, this has since been fixed in PowerShell (Core) 7+.
As an aside: The problem generally wouldn't exist if passing a pipeline-binding parameter's value as an argument exhibited the same behavior as via the pipeline - see GitHub issue #6057.
That is, you would expect Get-ChildItem -LiteralPath $someFileInfoInstance to bind the parameter, which is string-typed, in the same way as $someFileInfoInstance | Get-ChildItem.
However, that is not the case: when passed as an argument, simple .ToString() stringification is performed, which in combination with Windows PowerShell's inconsistent stringification of System.IO.FileInfo instances causes situational malfunctioning.
Alternatively, you can always explicitly use the .PSPath property value to predictably get a full path, but that is both more verbose and error-prone (easy to forget).
Get-ChildItem -LiteralPath $someFileInfoInstance.PSPath
Note that .PSPath values are prefixed by the name of the PowerShell provider underlying the item at hand, such as Microsoft.PowerShell.Core\FileSystem:: for file-system items.
As such, .PSPath isn't suitable for passing for passing file-system paths to external programs or .NET methods, which are discussed next.
Passing file-system paths to external programs and .NET methods:
In these cases, a full, file-system-native path must be passed.
External programs and .NET methods are unaware of PowerShell-only drives (those established with New-PSDrive), so any paths based on them wouldn't be recognized (and neither would PowerShell paths referring to non-file-system items, such as registry keys based on PowerShell's HKLM: drive).
In the case of external programs, a relative file-system path would work too, but .NET methods require a full path, due to .NET's working directory usually differing from PowerShell's (see this answer).
To ensure use of a full, file-system-native path:
For [System.IO.FileInfo] / [System.IO.DirectoryInfo] instances, use their .FullName property.
As stated above, this is no longer necessary in PowerShell (Core) v6.1+, where such instances consistently stringify to their .FullName property values. (Stringification happens invariably when passing arguments to external programs, and when passing arguments to string-typed (file-path) parameters of .NET methods).
For [System.Management.Automation.PathInfo] instances, such as returned by Get-Location and reflected in the automatic $PWD variable variable, use their .ProviderPath property.
Examples:
cmd /c echo (Get-Item .).FullName
cmd /c echo $PWD.ProviderPath
There was a lengthy discussion about this when the change was made to default ToString to result in a full path instead of a relative path.
TL;DR
Steve Lee stated the best practice is to explicitly declare whether you want the full path or the relative path by using $_.Name or $_.Fullname.
Bad Practice ❌
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $fileInfo"
Best Practice ✅
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $($fileInfo.FullName)"
Details
Get-ChildItem returns a System.IO.FileInfo object in both Windows PowerShell and PowerShell Core. The problem you're encountering is with the implementation of the ToString method of the System.IO.FileInfo object.
You type and run this:
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $fileInfo"
...which gets translated into this:
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.ToString())"
..which gets translated to this on Windows PowerShell:
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.Name)"
...and this on PowerShell Core:
$fileInfo = Get-ChildItem $MyFilePath
Write-Host "My file is: $(fileInfo.FullName)"
The reason they moved from using .Name to .FullName for the default implementation of ToString appears to be something related to security because relative paths could be tinkered with.
The reason it's a best practice to explicitly convert objects into strings instead of relying on the object to figure out how to convert itself into a string is because of exactly this scenario, the implementation could change and leave you up a creek.
I would like to concatenate two directories and an asterisk "*", for use in the Copy-Item cmdlet.
Currently I have something like the following
$fooDir = "foo"
$barDir = "bar"
$srcDir = Join-Path -Path $fooDir -ChildPath $barDir
$srcFiles = Join-Path -Path $srcDir -ChildPath "*"
$destDir = "buzz"
Copy-Item -Path "$srcFiles" -Destination $destDir -Recurse
Can I reduce the two Join-Path lines in some way that is just as cross-platform safe?
Or better yet, can I tell the Copy-Item Cmdlet that I want the directory's files copied without needing the asterisk ?
The Join-Path $fooDir $basrDir "*" syntax doesn't seem to work with the asterisk.
If your cross-platform script only ever runs in PowerShell (Core) v6+ on Windows, you can indeed take advantage of Join-Path's -AdditionalChildPath parameter, which, in combination with -Path, you can use positionally as follows:
$srcFiles = Join-Path $fooDir $barDir *
However, you can also take advantage of the fact that PowerShell allows you to use \ and / interchangeably as path separators in file-system paths, so using / explicitly works on Windows too, even in Windows PowerShell:
$srcFiles = "$fooDir/$barDir/*"
Note that if you want to ensure that hidden files and subdirectories are also copied, be sure to also pass the -Force parameter to the Copy-Item call.
I'm writing PowerShell scripts which call other PowerShell scripts with & .\other\script.ps1 but the backslash \ is, I assume, a Windows thing and I want my scripts to work cross-platform.
I've noticed that PowerShell in Windows does accept & ./other/script.ps1 but that may be accidental. Is PowerShell ALWAYS gonna translate my backslashes to the host platform?
What is the best approach to handling path seperators cross-platform in PowerShell?
In my experience Windows PowerShell is happy to accept either \ or / as a path separator so one way to be cross-platform is to always use /.
Alternatively you can use the Path cmdlets to handle building paths for you, for example:
Join-Path -Path $Pwd -ChildPath (Join-Path -Path 'Other' -ChildPath 'script.ps1')
Or to get the path for a file in the current directory:
Resolve-Path test.txt
Path cmdlets:
~> get-command *-path* | Select Name
Name
----
Convert-Path
Join-Path
Resolve-Path
Split-Path
Test-Path
I don't have enough credit to add a comment to the accepted answer by #mark-wragg but I just want to point out I started using forward slashes only in cross-platform scripts etc and hit a problem with symbolic links.
As an example, run
new-item -itemtype SymbolicLink -path TestScript8.ps1 -target ./TestScript.ps1
and you will not be able to open TestScript8.ps1 in VS Code on Windows.