Taking It Apart with PowerShell
In the last issue, I explored techniques and concepts for putting things together with PowerShell. This time, I'm going to take things apart. There's nothing secret or special. The topic allows me to cover a variety of techniques and concepts that you might find useful in your own scripts and functions.
Arrays
To my mind, breaking apart an array is selecting specific elements from the array. The easiest way to do this is to use the index operator. The index operator is the square brackets []
that you use to access an element in an array. The index is zero-based, meaning the first element is at index 0, the second element is at index 1, and so on.
PS C:\> $a = 1..10
PS C:\> $a[1]
2
You can specify a range of items by using a range operator.
PS C:\> $a[0..3]
1
2
3
4
Or you can use a comma to specify multiple indexes.
PS C:\> $a[7,9]
8
10
What you can't do is combine ranges and individual indexes. This syntax will fail.
$a[(0..3),7,9]
You have to turn things around.
PS C:\> (0..3),7,9 | foreach {$a[$_]}
1
2
3
4
8
10
You can also start at the end of the array and work backward.
PS C:\> $a[-1]
10
PS C:\> $a[-3]
8
PS C:\> $a[-1..-4]
10
9
8
7
Remember, the array is just a group of (typically) like objects. You can always filter the array using the Where-Object
cmdlet or the Where
method.
PS C:\> $a | Where {$_ -ge 6}
6
7
8
9
10
PS C:\> $a.where({$_ -ge 8})
8
9
10
HashTables
HashTables are a bit more complex because they have keys and values.
$hash = @{
Name = 'John'
Age = 30
City = 'New York'
}
You can access a value by using the key.
PS C:\> $hash.name
John
The other way to "break" apart a hashtable is to enumerate the keys and values using the GetEnumerator()
method.
PS C:\> $hash.GetEnumerator()
Name Value
---- -----
Age 30
Name John
City New York
This method returns an array of DictionaryEntry
objects. You can then access the key and value properties.
PS C:\> $hash.GetEnumerator() | Get-Member
TypeName: System.Collections.DictionaryEntry
Name MemberType Definition
---- ---------- ----------
Name AliasProperty Name = Key
Deconstruct Method void Deconstruct([ref] System.Object key, [ref] System.Object value)
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Key Property System.Object Key {get;set;}
Value Property System.Object Value {get;set;}
> Note the alias property of Name
for the Key
property.
Enumeration makes it easy to work with values or keys.
PS C:\> $hash.GetEnumerator() | Where {$_.value -is [string]} | foreach { $_.Value.ToUpper()}
JOHN
NEW YORK
Here's a practical example.
#requires -version 5.1
# Test-ProfilePerformance.ps1
<#
run this script in a new Powershell/pwsh session with no profile
$r = PowerShell -nologo -NoProfile -file C:\scripts\Test-ProfilePerformance.ps1
$r = pwsh -nologo -NoProfile -file C:\scripts\Test-ProfilePerformance.ps1
Or run it in a new Powershell/pwsh session with no profile
#>
#define a hashtable of profile paths in order of processing
$profiles = [ordered]@{
AllUsersAllHosts = $profile.AllUsersAllHosts
AllUsersCurrentHost = $profile.AllUsersCurrentHost
CurrentUserAllHosts = $profile.CurrentUserAllHosts
CurrentUserCurrentHost = $profile.CurrentUserCurrentHost
}
#only need to get these values once
$PSVer = $PSVersionTable.PSVersion
$computer = [System.Environment]::MachineName
$user = "$([System.Environment]::UserDomainName)\$([system.Environment]::userName)"
foreach ($prof in $profiles.GetEnumerator()) {
If (Test-Path $prof.value) {
Write-Host "Measuring script for $($prof.key)" -ForegroundColor cyan
$m = Measure-Command { . $prof.value }
#create a result
[PSCustomObject]@{
Computername = $computer
Username = $user
PSVersion = $PSVer
Profile = $prof.key
Path = $prof.value
TimeMS = $m.totalMilliseconds
}
Clear-Variable -Name m
} #if test path
else {
Write-Host "Skipping profile $($prof.key) because it does not exist" -ForegroundColor yellow
}
} #foreach profile
The script is meant to be run in a new PowerShell session with no profile. It measures the time it takes to run each profile script. The script uses a hashtable to define the profile paths in the order they should be processed. The script then enumerates the hashtable and processes each profile. If the profile path doesn't exist, the script skips it. In the code you can see where I am using the Key
and Value
properties of the DictionaryEntry
object.
PS C:\> powershell.exe -nologo -NoProfile
PS C:\> $r = C:\scripts\Test-ProfilePerformance.ps1
Measuring script for AllUsersAllHosts
Skipping profile AllUsersCurrentHost because it does not exist
Measuring script for CurrentUserAllHosts
Measuring script for CurrentUserCurrentHost
PS C:\> $r | Select-Object Profile,TimeMS
Profile TimeMS
------- ------
AllUsersAllHosts 101.1181
CurrentUserAllHosts 10121.8216
CurrentUserCurrentHost 30.3333
Yeah, I do a lot with my profiles.
Strings
One thing you are likely to break apart is a string. PowerShell has a variety of techniques for working with strings.
$a = "Hello, World! PowerShell is awesome."
$b = "Alice,Bob,Carol,David,Emily"
Splitting
The easiest way to break apart a string is to use the Split()
method. Typically, you specify the character that separates the string into parts.
PS C:\> $a.split("!")
Hello, World
PowerShell is awesome.
PS C:\> $b.split(",")
Alice
Bob
Carol
David
Emily
Splitting will create arrays of strings. You can then access the elements as you would any other array.
Here's a tip for the first split. Notice the extra space in the last element. You might want to clean that up.
PS C:\> $a.split("!").Trim()
Hello, World
PowerShell is awesome.
One feature you might want is to split a string into a fixed number of parts. The Split()
method has an overload that allows you to specify the number of parts.
PS C:\> $b.split(",",3)
Alice
Bob
Carol,David,Emily
The Split()
method is expecting a character, although you can specify multiple characters.
PS C:\> $a.split("is")
Hello, World! PowerShell
awesome.
Notice that the output does not include the split character.
As an alternative, you can use the -split
operator. The benefit with this approach is that you can use a regular expression pattern. Maybe I don't know what separator is being used.
PS C:\> $b -split "[;:,-]"
Alice
Bob
Carol
David
Emily
PS C:\> $a -split "\W+"
Hello
World
PowerShell
is
awesome
PS C:\>
The last example includes a blank line. I can either adjust the regex pattern, or filter out the blank lines.
PS C:\> $a -split "\W+" | Where {$_}
Hello
World
PowerShell
is
awesome
The method also lets you split a string into a fixed number of parts.
PS C:\> $b -split "[,;:-]",3
Alice
Bob
Carol,David,Emily
Substrings
Another way to break apart a string is to extract a substring. The Substring()
method is the easiest way to do this.
$c = "[12/01/2024] This is a reference sample."
The method has two overloads. The first overload requires a starting index.
PS C:\> $c.substring
OverloadDefinitions
-------------------
string Substring(int startIndex)
string Substring(int startIndex, int length)
The string can be indexed like an array.
PS C:\> $c[0,15,-1]
[
i
.
If you specify an index number alone, PowerShell will return the rest of the string.
PS C:\> $c.substring(13)
This is a reference sample.
Or you can specify the number of characters to extract.
PS C:\> $c.substring(1,10)
12/01/2024
This technique is easy when you know what the string looks like. If you need to extract a substring without knowing the exact position, you can use the IndexOf()
method.
PS C:\> $c.IndexOf(" ")
12
PS C:\> $c.Substring($c.IndexOf(" ")+1,1)
T
I found the index number of the first string. Then I got the substring starting at the next character.
A Demonstration
Here's a short code snippet that pulls everything together, including ideas from the previous article. This code requires PowerShell 7 in order to use the range operator with letters. See if you can follow each line of code.
$pw = @()
$pw+= "a".."z" | Get-Random -count 6
$pw+= "A".."Z" | Get-Random -count 6
$pw+= 0..9 | Get-Random -Count 5
$pw+= "!@#$%^&*()_+" -split "\s*" | Where {$_} | Get-Random -Count 4
$tmp = -join ($pw | Get-Random -count $pw.count)
$r = $tmp[-1..-10] -join ''
The code creates a password by selecting random characters from a variety of sets. The password is then shuffled and the last 10 characters are selected in reverse. The result should be a complex random password with non-repeating characters, casing aside..
PS C:\> $r
u7dK+_09zY
Summary
I hope you enjoyed this exploration of putting things together and breaking them apart with PowerShell. You can use these techniques in your scripts and functions to manipulate strings, arrays, and hashtables. And if you need a refresher, many of these ideas are documented in about help topics like about_split and about_join.