Monthly Archive

Categories

PowerShell

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

PowerShell check file exists

There are times when you need to check if a file exists – this is how you do a PowerShell check file exists.

If you try to access a file that doesn’t exist you’ll get an error:

PS> Get-Content -Path c:\test\z27.txt
Get-Content : Cannot find path 'C:\test\z27.txt' because it does not exist.
At line:1 char:1
+ Get-Content -Path c:\test\z27.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (C:\test\z27.txt:String) [Get-Content], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand

 

Rather than manage the error directly you can test to see if the file exists

PS> Test-Path -Path c:\test\z27.txt
False

 

Test-Path returns a boolean (true or false) so you can then test the file exists before accessing

PS> if (Test-Path -Path c:\test\z27.txt) {Get-Content -Path c:\test\z27.txt}

 

You’d obviously add an else block to deal with the case of the file not existing – possibly just a warning or a throw depending on what you were doing,

You can also test if a folder exists

PS> Test-Path -Path c:\test\
True
PS> Test-Path -Path c:\test27\
False

 

You can be more exact with whether you’re dealing with a folder or a file by using the –PathType parameter

PS> Test-Path -Path c:\test -PathType Any
True
PS> Test-Path -Path c:\test -PathType Container
True
PS> Test-Path -Path c:\test -PathType Leaf
False
PS> Test-Path -Path C:\test\p1.txt -PathType Container
False
PS> Test-Path -Path C:\test\p1.txt -PathType Leaf
True

 

Use container for a folder and leaf for a file.

Alternatively you can use –Filter, –Include and –Exclude but remember you’ll only get a boolean returned so

PS> Test-Path -Path C:\test\* -Filter *.txt
True

gives a single return even though there are 20 .txt files in the folder!

 

Your last alternative is to –OlderThan or –NewerThan to test against a files age

PS> Test-Path C:\test\* -Filter *.txt -NewerThan ((Get-Date).AddDays(-3))
False
PS> Test-Path C:\test\* -Filter *.txt -OlderThan ((Get-Date).AddDays(-7))
True

 

I do have .txt files older than 7 days but none younger than 3 days old.

The –LiteralPath parameter  means that the path is used exactly as given and ignores any possible interpretation of a character as anything else. use it instead of –Path.

 

Finally –IsValid tests the validity of the path syntax even if the path doesn’t exist so

PS> Test-Path -Path C:\test\z27.txt
False
PS> Test-Path -Path C:\test\z27.txt -IsValid
True

 

The file itself doesn’t exist but the path is valid.  Conversely

PS> Test-Path -Path C:\test\p1.txt
True
PS> Test-Path -Path C:\test\p1.txt -IsValid
True
PS> Test-Path -Path C:\test\p?1.txt -IsValid
False
PS> Test-Path -LiteralPath C:\test\p?1.txt -IsValid
False

p1.txt exists and has a valid path but p?1.txt isn’t a valid path.

 

On Windows folder and file names are case INSENSITIVE so

PS> Test-Path -Path C:\test\p1.txt
True
PS> Test-Path -Path C:\tEsT\p1.txt
True

both work.

 

On Linux folder and file names are case sensitive so be aware. How to deal with this situation is currently under discussion

https://github.com/PowerShell/PowerShell/issues/7578

Hyper-V book soon available

I’ve been working with Andy Syrewicze on a Hyper-V book that’ll be available in the next few months.

https://www.apress.com/gb/book/9781484241158

 

https://www.amazon.co.uk/Pro-Microsoft-Hyper-V-2019-Hands/dp/1484241150/ref=sr_1_1?s=books&ie=UTF8&qid=1536253236&sr=1-1&keywords=Hyper-V+siddaway

 

https://www.amazon.com/Pro-Microsoft-Hyper-V-2019-Hands/dp/1484241150/ref=sr_1_1?ie=UTF8&qid=1536253327&sr=8-1&keywords=hyper-v+siddaway

 

Its’s a rewrite and update of the Month of Lunches book I was working on a while back.

Pleased with the way it turned out and hope you’ll find it useful

PowerShell string contains

How can you check if a PowerShell string contains a character or substring?

You might be tempted to try this:

PS> $s = 'abcdefghijk'
PS> $s -contains 'f'
False

 

But –contains is for working with the contents of arrays. So you could do this:

PS> ($s.ToCharArray()) -contains 'f'
True

 

You’re implicitly converting the string ‘f’ to [char] to make the comparison. Your comparison is actually this

PS> ($s.ToCharArray()) -contains [char]'f'
True

 

That’s fine for a single character but if you want to test a substring

PS> $s -contains 'def'
False
PS> ($s.ToCharArray()) -contains 'def'
False

 

That approach won’t work.

You need to use the Indexof method

PS> $s.Indexof('f')
5
PS> $s.Indexof('def')
3

 

The value returned is the position of the FIRST character in the substring.

You can also test an an array of characters

PS> $a = 'g','j','a'
PS> $s.IndexOfAny($a)
0

 

Again you get the FIRST character in this case the ‘a’

Remember PowerShell is .NET based so the first index is 0

 

Lets get some repetition into our target

PS> $s = $s * 3

PS> $s
abcdefghijkabcdefghijkabcdefghijk

You also have the option of picking the LAST occurrence of the substring

PS> $s.LastIndexOf('f')
27

PS> $s.LastIndexOfAny($a)
31

 

This last one is the last ‘j’ in the string – its the last occurrence of any of the characters you wanted to match.

If there isn’t a match you get –1 returned

PS> $s.IndexOf('z')
-1
PS> $s.LastIndexOf('z')
-1

PowerShell Day UK Agenda Update

There’s been a Powershell Day UK Agenda Update.

 

Looks like there’ll be three tracks for most of the day.

 

Full agenda and tickets from - https://psday.uk/

 

 

Deleting folders based on month name

An interesting problem around deleting folders based on month name.

You have a folder structure that looks like this

C:\Testdata\2011\08-Aug
C:\Testdata\2011\09-Sep
C:\Testdata\2011\10-Oct
C:\Testdata\2011\11-Nov
C:\Testdata\2011\12-Dec
C:\Testdata\2012\01-Jan
C:\Testdata\2012\02-Feb
C:\Testdata\2012\03-Mar
C:\Testdata\2012\04-Apr
C:\Testdata\2012\05-May
C:\Testdata\2012\06-Jun
C:\Testdata\2012\07-Jul
C:\Testdata\2012\08-Aug
C:\Testdata\2012\09-Sep
C:\Testdata\2012\10-Oct
C:\Testdata\2012\11-Nov
C:\Testdata\2012\12-Dec
C:\Testdata\2013\01-Jan
C:\Testdata\2013\02-Feb
C:\Testdata\2013\03-Mar
C:\Testdata\2013\04-Apr
C:\Testdata\2013\05-May
C:\Testdata\2013\06-Jun
C:\Testdata\2013\07-Jul
C:\Testdata\2013\08-Aug

 

The structure extends forward and backward in time. Each year folder has 12 subfolders based on the number and name of the month.

 

You want to delete all folder, subfolders and contents that are more than 7 years old BUT you have to use the folder name for month and its parent for the year. You can’t assume that the create date or last write date will work in this instance.

 

If you want to paly around with this the code to create the folder structure looks like this:

##
## define enum for months
##
enum month {
Jan = 1
Feb = 2
Mar = 3
Apr = 4
May = 5
Jun = 6
Jul = 7
Aug = 8
Sep = 9
Oct = 10
Nov = 11
Dec = 12
}

##
## create folder structure
##

New-Item -Path c:\ -Name Testdata -ItemType Directory

2018..2005 |
foreach {
New-Item -Path c:\Testdata -Name $psitem -ItemType Directory

$path = "c:\Testdata\$psitem"

1..12 | 
foreach {
$name = "{0:00}-{1}" -f $psitem, [month]$psitem
New-Item -Path $path -Name $name -ItemType Directory
}
}

 

Create a top level folder. Then for the years 2018 back to 2005 create a year folder using the year as the name. In each year folder create 12 month folders. I’ve used the string formatting –f operator to put a leading zero on the month number and to get the appropriate value from the enum.

 

Now to delete the folders

enum month {
Jan = 1
Feb = 2
Mar = 3
Apr = 4
May = 5
Jun = 6
Jul = 7
Aug = 8
Sep = 9
Oct = 10
Nov = 11
Dec = 12
}

$date = Get-Date
$year = $date.Year - 8

##
## delete everything 8 years or older
##
Get-ChildItem -Path C:\Testdata -Directory |
where Name -le $year |
foreach {
Remove-Item -Path $psitem.Fullname -Recurse -Force -Confirm:$false
}

##
## if Month -ne January
## need to delete some months
##

if ($date.Month -gt 1){
$path = "C:\testdata\$($year+1)"
$month = $date.Month -1

1..$month | 
foreach {
$mpath = "$path\{0:00}-{1}" -f $psitem, [month]$psitem
Remove-Item -Path $mpath -Recurse -Force -Confirm:$false
}
}

 

The code gets the current date and gets the year minus 8. It loops through your top level folder and gets the folders that are less than or equal to the year you've defined. They, and their contents, are force deleted. Only thing that could stop the deletion is if you have one of the files pinned open.

 

If the current month is January there's nothing else to do. Otherwise create the path to the -7 year folder and calculate the last month you want to delete. Loop through the months, build the path and force the deletion of the folder and its contents.

 

The bulk of the work is done at the year level with a quick clean up of the months.

Variable type

Usually when you create a variable you implicitly set the type by the value you use. Sometimes though you may want to explicitly set the variable type.

 

if you don’t give the variable a type you can do this:

PS> $x = 35
PS> $x
35
PS> $x = 'now a string'
PS> $x
now a string

 

If you give the variable an explicit type

PS> [int]$x = 35
PS> $x
35
PS> $x = 'now a string'
Cannot convert value "now a string" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:1
+ $x = 'now a string'
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException

 

The variable expects an integer or something that can be converted to an integer

PS> $x = '123'
PS> $x
123

 

You can’t supply a type when you use New-Variable so if you need a read only or constant variable then create it as shown above then use Set-Variable to make it read only or constant.

Read only and constant variables

When you create a variable you usually want to be able to change its value – the clue is in the term variable. Sometimes though you might want to use a value that doesn’t change – you want read only and constant variables.

Standard variables are changeable:

PS> $x = 10
PS> $x
10
PS> $x += 5
PS> $x
15
PS> $x -= 3
PS> $x
12

 

The variable holds a value that can be changed to match your needs. Another example is the variable used in a for loop:

PS> for ($i=1; $i -lt 6; $i++){$i}
1
2
3
4
5

 

If you want to use a value in your code that is pre-set and doesn’t change you have a couple of options – read only variables and constants.

To create a read only variable use New-variable with the ReadOnly option

PS> New-Variable -Name x -Value 10 -Option ReadOnly
PS> $x
10

 

Now if you try to change the value

PS> $x += 5
Cannot overwrite variable x because it is read-only or constant.
At line:1 char:1
+ $x += 5
+ ~~~~~~~
+ CategoryInfo : WriteError: (x:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable

 

You can also change an existing variable to be read only

PS> $y = 20
PS> Set-Variable -Name y -Option ReadOnly
PS> $y
20
PS> $y -= 5
Cannot overwrite variable y because it is read-only or constant.
At line:1 char:1
+ $y -= 5
+ ~~~~~~~
+ CategoryInfo : WriteError: (y:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable

 

A read only variable is protected from deletion

PS> Remove-Variable -Name x
Remove-Variable : Cannot remove variable x because it is constant or read-only. If the variable is read-only, try the
operation again specifying the Force option.
At line:1 char:1
+ Remove-Variable -Name x
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (x:String) [Remove-Variable], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotRemovable,Microsoft.PowerShell.Commands.RemoveVariableCommand

 

Time to be a PowerShell Jedi and use the force:

PS> Remove-Variable -Name x -Force

the variable is removed.

 

But what if you want to prevent the variable from being removed. Then you have to make it a constant:

PS> New-Variable -Name x -Value 10 -Option Constant
PS> $x
10
PS> $x += 5
Cannot overwrite variable x because it is read-only or constant.
At line:1 char:1
+ $x += 5
+ ~~~~~~~
+ CategoryInfo : WriteError: (x:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable

PS> Remove-Variable -Name x
Remove-Variable : Cannot remove variable x because it is constant or read-only. If the variable is read-only, try the
operation again specifying the Force option.
At line:1 char:1
+ Remove-Variable -Name x
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (x:String) [Remove-Variable], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotRemovable,Microsoft.PowerShell.Commands.RemoveVariableCommand

PS> Remove-Variable -Name x -Force
Remove-Variable : Cannot remove variable x because it is constant or read-only. If the variable is read-only, try the
operation again specifying the Force option.
At line:1 char:1
+ Remove-Variable -Name x -Force
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (x:String) [Remove-Variable], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotRemovable,Microsoft.PowerShell.Commands.RemoveVariableCommand

 

You can’t modify the variable’s value and you can’t delete it.

The variable will disappear when you close the PowerShell console.

Splitting on a \

Splitting strings is usually easy – you use the –split operator or the Split() method. Occasionally you may hit a problem – for instance splitting on a \ character.

Let me demonstrate with an example.

PS> Get-CimInstance -ClassName Win32_ComputerSystem | select Username

Username
--------
W510W10\Richard

 

I get the domain (in this case the machine as its a local account) and the user name. What I want is just the user name.

PS> (Get-CimInstance -ClassName Win32_ComputerSystem | select -ExpandProperty Username) -split '\'
parsing "\" - Illegal \ at end of pattern.
At line:1 char:1
+ (Get-CimInstance -ClassName Win32_ComputerSystem | select -ExpandProp ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException

 

I’ve seen possible solutions like this

(Get-CimInstance -ClassName Win32_ComputerSystem | select username | out-string).split('\')

 

But Out-String isn’t necessary in this case.

The problem is that \ is the PowerShell escape character so you have to escape it so that its treated as a literal character

PS> ((Get-CimInstance -ClassName Win32_ComputerSystem | select -ExpandProperty Username) -split '\\')[1]
Richard

 

This also works

PS> (Get-CimInstance -ClassName Win32_ComputerSystem | select -ExpandProperty Username).Split('\')[1]
Richard

 

Keep it simple