Copy-Item Container parameter
The Copy-Item Container parameter controls whether the folder structure is also copied.
Lets start with the source material – a folder C:\test with a bunch of files. A destination folder C:\D1 also exists
PS> Copy-Item -Path c:\test\* -Destination C:\D1
copies the files from test to D1
PS> Copy-Item -Path c:\test -Destination C:\D1
copies the FOLDER test to D1 BUT NOT the files in the folder
PS> Copy-Item -Path c:\test -Destination C:\D1 –Recurse
copies the FOLDER and its FILES to D1. test becomes a subfolder of D1
PS> Copy-Item -Path c:\test -Destination C:\D1 -Recurse –Container
copies the FOLDER and its FILES to D1. test becomes a subfolder of D1
PS> Copy-Item -Path c:\test -Destination C:\D1 -Recurse -Container:$false
Copies the files from test to D1
If the destination folder DOESN’T exist
PS> Copy-Item -Path c:\test -Destination C:\D2
creates just the folder
PS> Copy-Item -Path c:\test -Destination C:\D2 –Recurse
PS> Copy-Item -Path c:\test -Destination C:\D2 -Recurse –Container
Copy-Item -Path c:\test -Destination C:\D2 -Recurse -Container:$false
All create the folder and copies the files into it
These examples should enable you to utilise the –Container parameter as appropriate.
-like oddity
Recently saw a question asking why this code worked
PS> $a = 'aa'
PS> $b = 'a'
PS> $a -like "*$b*"
True
PS> Remove-Variable -Name b
PS> $a -like "*$b*"
True
Set 2 variables and compare using wildcards. The comparison comes back as True as you’d expect.
Remove $b and try the comparison again. Still comes back as true.
The reason is based on the definition of the wildcard *
By removing $b you’re really testing
PS> $a -like "**"
True
The definition of the * wildcard is that it:
Matches zero or more characters
So, just having a wildcard will always match
PS> $a -like '*'
True
Just a little something to be aware of when using wildcards and variables. If the variable is null – effective result if not present – you’ll always get a match.
Comparing strings and integers
Saw an interesting problem involving the comparison of 2 numbers. In reality its was comparing strings and integers.
The starting point was a CSV file but as I showed last time you can create a CSV in memory
PS> $file = @' >> "Count";"Value" >> "3";"Year1" >> "1";"Year1" >> "5";"Year3" >> "8";"Year4" >> "10";"Year15" >> "12";"Year6" >> "88";"Year7" >> "154";"Year2" >> '@ | ConvertFrom-Csv -Delimiter ';' PS> $file Count Value ----- ----- 3 Year1 1 Year1 5 Year3 8 Year4 10 Year15 12 Year6 88 Year7 154 Year2
The goal is to get all lines where Count is greater than or equal to 8.
The obvious starting point is
PS> $file | where Count -ge 8 Count Value ----- ----- 8 Year4 88 Year7
Which doesn’t supply the right answer.
Checking the object created from the CSV file
PS> $file | Get-Member TypeName: System.Management.Automation.PSCustomObject Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() Count NoteProperty string Count=3 Value NoteProperty string Value=Year1
Count is a string
With comparison operators the RIGHT hand side is converted to be the same as the LEFT hand side if the types don’t match. So in effect you’re getting
PS> "154" -ge "8" False
You need to force the LEFT hand side to be an integer
PS> [int]"154" -ge "8" True PS> ("154" -as [int]) -ge "8" True
That makes the code to work with the CSV
PS> $file | where {[int]$_.Count -ge 8} Count Value ----- ----- 8 Year4 10 Year15 12 Year6 88 Year7 154 Year2
or
PS> $file | where {($_.Count -as [int]) -ge 8} Count Value ----- ----- 8 Year4 10 Year15 12 Year6 88 Year7 154 Year2
CSV files are great but you have to remember about type conversion when you’re comparing properties
CSV cmdlets
I was reading something and when ConvertFrom-CSV was mentioned it made me pause and think about what that cmdlet actually did. This is a quick explanation of the CSV cmdlets.
Export-CSV was available in PowerShell v1. You use it to create a CSV file from PowerShell objects. The code in PowerShell v6.1 is
PS> Get-Process -Name p* | select Name, id, StartTime | Export-Csv -Path C:\test\test1.csv
In Windows PowerShell v5.1 it would be
PS> Get-Process -Name p* | select Name, id, StartTime | Export-Csv -Path C:\test\test1.csv -NoTypeInformation
The –NoTypeInformation parameter is needed to prevent the type information being written to the first line of the CSV. In v6.1 the default is NOT to write type information to the csv file
Import-CSV again has been a staple of PowerShell since v1. You use this cmdlet to read a CSV file that you have saved on disk.
PS> Import-Csv -Path C:\test\test1.csv Name Id StartTime ---- -- --------- powershell 14632 24/09/2018 12:27:17 pwsh 9804 24/09/2018 12:40:38
You can then send the objects down the pipeline for further processing.
The important thing to remember is that Import/Export-CSV deal with files.
ConvertTo-Csv is used to create CSV strings from objects. In PowerShell v6.1
PS> Get-Process -Name p* | select Name, id, StartTime | ConvertTo-Csv "Name","Id","StartTime" "powershell","14632","24/09/2018 12:27:17" "powershell_ise","10968","24/09/2018 12:48:56" "pwsh","9804","24/09/2018 12:40:38"
If you do the same in Windows PowerShell v5.1 you get
PS> Get-Process -Name p* | select Name, id, StartTime | ConvertTo-Csv #TYPE Selected.System.Diagnostics.Process "Name","Id","StartTime" "powershell","14632","24/09/2018 12:27:17" "powershell_ise","10968","24/09/2018 12:48:56" "pwsh","9804","24/09/2018 12:40:38"
So you need to use the –NoTypeInformation parameter again
PS> Get-Process -Name p* | select Name, id, StartTime | ConvertTo-Csv -NoTypeInformation "Name","Id","StartTime" "powershell","14632","24/09/2018 12:27:17" "powershell_ise","10968","24/09/2018 12:48:56" "pwsh","9804","24/09/2018 12:40:38"
ConvertTo-Csv is analogous to Export-CSV except that the data is in memory rather than being on disk.
ConvertFrom-CSV reads CSV strings and converts them to objects
$p = @' Name,Id,StartTime powershell,14632,24/09/2018 12:27:17 pwsh,9804,24/09/2018 12:40:38 '@ ConvertFrom-Csv -InputObject $p
ConvertFrom-Csv is analogous to Import-CSV except that the data is in memory rather than being on disk.
The Import/Export cmdlets deal with data on disk while the ConvertTo/ConvertFrom cmdlets are for data that remains in memory.
PowerShell copy file
An article I wrote for searchwindowsserver on PowerShell file copy was published recently.
The article is available at
Determining the PowerShell host
The PowerShell ecosystem is more diverse than it used to be which makes determining the PowerShell host a bit more difficult.
The variable $host, and its Name property, is a good starting point.
PS> $host.Name
ConsoleHost
You get ConSoleHost if you’re running in the console.
For ISE you get - Windows PowerShell ISE Host
And for VScode you get - Visual Studio Code Host
If you want to know the PowerShell version as well it’s a bit more awkward.
$host.Version works for the console and ISE as it returns the PowerShell version. In VScode however it returns the version of VScode. The best route is to use
$PSVersionTable.PSVersion
While you’re at it you may want to check the edition of PowerShell – Core or Desktop
PS> $PSVersionTable.PSEdition
Core
To make that a bit easier
function get-powershellhost { New-Object -TypeName PSobject -Property @{ Hostname = $host.Name PSversion = $PSVersionTable.PSVersion PSEdition = $PSVersionTable.PSEdition } }
The function runs in the console, ISE and VScode and was tested across Windows PowerShell v5.1 and PowerShell v6.1
Feel free to add other information as required
Splitting paths
PowerShell has the Split-Path cmdlet that provides the leaf and parent of a path. But what if you’re splitting paths and need one or paths at a higher level.
Consider the path
PS> $path = 'C:\Scripts\HyperV\Admin\Optimize-VMDisks.ps1'
Its just an arbitrary path from my test machine.
Using Split-Path you can get the parent (by default) or the leaf path
PS> Split-Path -Path $path C:\Scripts\HyperV\Admin PS> Split-Path -Path $path -Parent C:\Scripts\HyperV\Admin PS> Split-Path -Path $path -Leaf Optimize-VMDisks.ps1
But what if you want the grandfather path. That starts to get ugly just using Split-Path.
PS> Split-Path -Path (Split-Path -Path $path -Parent) C:\Scripts\HyperV
The code gets uglier and uglier as you progress up the path
I decided I needed an object whose properties gave me a consistent view of the path hierarchy – the parent path was always level 1; the grandfather was always level 2 etc. Something like this
Level00 : C:\Scripts\HyperV\Admin\Optimize-VMDisks.ps1 Level01 : C:\Scripts\HyperV\Admin Level02 : C:\Scripts\HyperV Level03 : C:\Scripts Level04 : C:\
My solution was to create this function
function split-multipath { [CmdletBinding()] param ( [string]$path ) if (-not (Test-Path -Path $path -IsValid)) { throw "Invalid path: $path" } $outpaths = [ordered]@{ Level00 = $path } $l = 1 $path = Split-Path -Path $path -Parent while ($path -ne ''){ $level = "Level{0:00}" -f $l $outpaths += @{ $level = $path } $l++ $path = Split-Path -Path $path -Parent } New-Object -TypeName PSobject -Property $outpaths }
The split-multipath function takes a string as a parameter. That string is tested to determine if its a valid path – that could be moved to a ValidateScript test on the parameter.
The output is created from an ordered hash table. The Level00 property is set to the full path as input.
Set the counter and split the path to get the parent.
Use a while loop to add the current path into the hash table – create the property name with the string format operator –f.
Increment the counter and split the path - again taking the parent.
The while loop runs while the path has data. If you try to split when the path just contains the drive you get an empty string
PS> (Split-Path -Path c:\ -Parent) -eq '' True
The data is output as an object so you can access a particular level. For instance if you want the great-grandfather level – you use Level03
PS> $test.Level03 C:\Scripts
You can use the output object to create a new path based on the level of your choice
PS> Join-Path -Path $test.Level03 -ChildPath Test47 C:\Scripts\Test47
PowerShell v6.1
PowerShell v6.1 was released last week – there’s no big ticket items like v6.0 but a lot of bug fixes and minor improvements.
You can download from https://github.com/PowerShell/PowerShell/releases
and find the release notes at
Get-Date –DisplayHint
On the surface Get-Date is a simple cmdlet that’s been around since PowerShell v1. However, it has some interesting quirks. In this post I’ll show how Get-Date –DisplayHint works.
By default Get-Date returns the data and time
PS> Get-Date
13 September 2018 16:09:34
The DisplayHint parameter ( available in PowerShell v5.1, 6.0 and 6.1) modifies the data shown
PS> Get-Date -DisplayHint Date
13 September 2018
PS> Get-Date -DisplayHint DateTime
13 September 2018 16:11:18
PS> Get-Date -DisplayHint Time
16:11:25
You might think that all –DisplayHint does is just force the display to be date, time or datetime and you’ve lost the rest of the data but in reality all that’s happening is that the default display of your object is constrained by the value of the DisplayHint property:
PS> Get-Date | Get-Member -Name DisplayHint
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
DisplayHint NoteProperty DisplayHintType DisplayHint=DateTime
PS> Get-Date -DisplayHint Date | Get-Member -Name DisplayHint
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
DisplayHint NoteProperty DisplayHintType DisplayHint=Date
PS> Get-Date -DisplayHint Time | Get-Member -Name DisplayHint
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
DisplayHint NoteProperty DisplayHintType DisplayHint=Time
Note that DateTime is the default setting if –DisplayHint isn’t used.
PS> $t = Get-Date -DisplayHint Time
PS> $t
16:16:59
But you can still access the other properties of the datetime object returned by get-date
PS> $t.Date
13 September 2018 00:00:00
PS> $t.Year
2018
PS> $t.Hour
16
If you just need to see the date or time –DisplayHint is a good place to start
PowerShell new line
A PowerShell new line can be generated using `n. It’s one of a series of special characters recognised by PowerShell.
The full list of Windows PowerShell v5.1 special characters is:
`0 Null
`a Alert
`b Backspace
`f Form feed
`n New line
`r Carriage return
`t Horizontal tab
`v Vertical tab
--% Stop parsing
Here’s some examples:
`0 is a null character – empty space
PS> "abcd`0efg"
abcd efg
`a causes the machine to issue an alert – beep
PS> "`a Beep "; "`a Beep"
Beep
Beep
Two alerts are sent but you might only hear one.
The backspace character moves the cursor back one space overwriting the character that was there:
PS> "abcd`befg"
abcefg
Form feed is for printers only
A new line is added by `n
PS> "abc`nde`nfg"
abc
de
fg
The carriage return returns the cursor to the beginning of the line so any text before it will be overwrittem
PS> "This original text`rIs overwritten by this brand new text"
Is overwritten by this brand new text
A horizontal tab is added by `t
PS> "This`twill`ttab`tacross`tthe`tscreen"
This will tab across the screen
And `v for vertical tabs but only when printing documents – it doesn’t work on screen output
You can use –% to stop interpreting input as PowerShell commands or expressions. In this case PowerShell attempts to evaluate $x
PS> Write-Host $x = a variable
= a variable
but what you really want is
PS> Write-Host --% $x = a variable
--% $x = a variable
Unfortunately, you also get the –% come through.
The about_parsing help file has an example using icacls.
You’ll have noticed that all of the examples were in double quotes – single quotes stop the special character being recognised:
PS> "This`twill`ttab`tacross`tthe`tscreen"
This will tab across the screen
PS> 'This`twill`ttab`tacross`tthe`tscreen'
This`twill`ttab`tacross`tthe`tscreen
PowerShell v6 introduced `u for working with unicode characters
PS> "`u{2195}"
↕
you can have 1 – 6 hex chacraters with a maximim value of 10FFFF