Powershell Code Snippets

A collection of Powershell code snippets that might come handy at some point.

Don’t expect well thought out descriptions or highly structured content here; these are snippets.
(But there are some links spread around for further reading.)

You can also find some more practical functions in the Powershell area of my “misc_public” Git repository on BitBucket.org.


Function/variable not available in session after running a script file

When you run “.\script.ps1”, you are executing the script but your function or variable will not be accessible from your session. You need to load the script via dot sourcing to make its functions, variables, etc. available to your running session (see also “Script Scope And Dot Sourcing” in ‘about_Scripts’):

> . .\script.ps1

Convert a SID to a name

Convert SID to an account name, respectively an account name to a SID; both within the same domain.

$SID = 'S-1-5-21-314159-2658589793-314159265-3589'
$AccountName = ([System.Security.Principal.SecurityIdentifier]$SID).Translate([System.Security.Principal.NTAccount]).Value

$AccountName = 'contoso\john.doe'
$SID = ([System.Security.Principal.NTAccount]$AccountName).Translate([System.Security.Principal.SecurityIdentifier]).Value

In case the AD object’s SID is from a different (but trusted) domain:

(Get-ACL -Path C:\Folder\Subfolder1\Subfolder2).Access.IdentityReference |
    % { Get-ADObject -Server example.net -Filter "ObjectSID -eq '$_'" }

And here it is again, this time packaged as a helper function:

function convertSIDToNameAsString
{
    param
    (
        [Parameter(Mandatory=$true)] [string] $SID,
        [Parameter(Mandatory=$true)] [string] $Domain
    )
    Get-ADObject -Server $Domain -Filter "ObjectSID -eq '$SID'" | select Name -ExpandProperty Name
}

$dom = "example.com"
$sid = "S-1-2-34-5678901234-567890123-0006660000-00305"

convertSIDToNameAsString -SID $sid -Domain $dom

# ---- Another usage example: ----

$acl_access_obj = (Get-ACL C:\path).Access

$acl_access_obj | select -property *, @{n="Resolved Name"; e={convertSIDToNameAsString -Domain $Domain -SID $_.IdentityReference}}

Another way to resolve the SID from a different (but trusted) domain (there’s probably a reason, why it is more verbose)

Add-Type -AssemblyName System.DirectoryServices.AccountManagement

$principal_context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new(
    [System.DirectoryServices.AccountManagement.ContextType]::Domain, $domain)

$object = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity(
    $principal_context, [System.DirectoryServices.AccountManagement.IdentityType]::Sid, $SID)

$object

Note that in the example above we explicitly search for a group; there are other possible values: (Slightly weird inheritance chain, if one compare group and computers/users, but that’s what Microsoft writes…)


List all AD domains with which our own domains have a trust relationship

$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()

ForEach ($forest_domain in $Forest.Domains)
{       
    Write-Output "Own domain: $($forest_domain.Name)"
}

$TrustedDomains = @()
$Trusts         = $Forest.GetAllTrustRelationships()

ForEach ($trust in $Trusts)
{
    Foreach ($Domain in $trust.TrustedDomainInformation)
    {
        $dom = $Domain.dnsname
        $dom = $dom.ToLower()
        
        If (-not ($dom.Contains("custom")) -and
            ($trust.TrustDirection -eq "Inbound" -or $trust.TrustDirection -eq "Bidirectional"))
        {
            $TrustedDomains += $dom
        }
    }
}

Write-Output "---- Trusted Domains ----"
return $TrustedDomains

For the above mentioned points (SID from a different [trusted] domain), there are also functions here that may help:


Find all AD groups that have term in its name

Because I had a mental block and needed to look up the -Filter syntax… :-O

Get-ADGroup -Filter {Name -like "*term*"} -server example.net | select mame | sort name

For scripts with a (Windows Forms) GUI: Show or hide the console

Based on http://powershell.cz/2013/04/04/hide-and-show-console-window-from-gui/

Add-Type -Name Window -Namespace Console `
    -MemberDefinition '[DllImport("Kernel32.dll")] public static extern IntPtr GetConsoleWindow();
                       [DllImport("user32.dll")]   public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);'
 
function Show-Console ([bool] $show)
{
    $consolePtr = [Console.Window]::GetConsoleWindow()

    if ($show)
    {
        [Console.Window]::ShowWindow($consolePtr, 5) # Show
    }
    else
    {
        [Console.Window]::ShowWindow($consolePtr, 0) # Hide
    }
}

Report the (free) disk space on a remote computer

$ComputerName = "MyComputer"

$disk = Get-WmiObject Win32_LogicalDisk -ComputerName $ComputerName -Filter "DeviceID='C:'" |
    Select-Object Size,FreeSpace

Write-Output ("Disk C: on remote computer $ComputerName has {0:#.0} GB free `
    of {1:#.0} GB total" -f ($disk.FreeSpace/1GB), ($disk.Size/1GB))

Tail a file

Keep an eye on a file and update the last 10 lines after the file has been modified (and saved!); might be useful for logfiles.

Get-Content -Path C:\folder\file.txt -Tail 10 -Wait

(Nested) Progress Bar

$total = (1..100)
$subtask = (1..10)

ForEach ($a in $total)
{
    $percent = [math]::Round($a/$total.Count*100)
    
    Write-Progress `
    -Id 1               <# The ID Must be an integer value #> `
    -Activity "Working..." `
    -Status "Please wait." `
    -CurrentOperation "$percent% complete" `
    -PercentComplete $percent
    
    Write-Output "Main task: $percent% completed."
    
    ForEach ($b in $subtask)
    {
        $percent2 = [math]::Round($b/$subtask.Count*100)
        
        Write-Progress `
        -Id 2 `
        -ParentId 1     <# To get a nested progress bar, specify the parent's ID here #> `
        -Activity "Working on sub-task..." `
        -Status "Please wait." `
        -CurrentOperation "$percent2% complete" `
        -PercentComplete $percent2
        
        Write-Output "`tSub task: $percent2% completed."
    }
}

GUI: Folder Browser Dialogue Box

Inspired by https://code.adonline.id.au/folder-file-browser-dialogues-powershell/

function Find-Folders
{
    [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
    [System.Windows.Forms.Application]::EnableVisualStyles()

    $browse = New-Object System.Windows.Forms.FolderBrowserDialog

    $browse.SelectedPath = "C:\"
    #$browse.RootFolder = [System.Environment+SpecialFolder]'MyComputer'

    $browse.ShowNewFolderButton = $false
    $browse.Description = "Select a directory"

    $loop = $true

    while($loop)
    {
        if ($browse.ShowDialog() -eq "OK")
        {
            $loop = $false

            #Insert your script here

        }
        else
        {
            return
        }
    }
    $browse.SelectedPath
    $browse.Dispose()
} Find-Folders

GUI: File Browser Dialogue Box (with Multiselect)

Inspired by https://code.adonline.id.au/folder-file-browser-dialogues-powershell/

Add-Type -AssemblyName System.Windows.Forms
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
    Multiselect = $true                               # Multiple files can be chosen
    Filter      = 'Images (*.jpg, *.png)|*.jpg;*.png' # Specified file types
}
 
[void]$FileBrowser.ShowDialog()

$path = $FileBrowser.FileNames;

If($FileBrowser.FileNames -like "*\*")
{

    # Do something before work on individual files commences
    $FileBrowser.FileNames #Lists selected files (optional)

    foreach($file in Get-ChildItem $path)
    {
        Get-ChildItem ($file) |	ForEach-Object {
            # Do something to each file
        }
    }
    # Do something when work on individual files is complete
}
else
{
    Write-Host "Cancelled by user"
}

Match users to computers

Compares two lists: One CSV file that contains computer names, one TXT file that contains users names. The computer data in the CSV file, like “Inventory - Computer.OS Login”, comes from a special tool, so adjust as needed!

Loops over all entries and looks for matches, that will be collected in a new list, that is then saved to an output/result text file at the end.

Nice goodie: Shows two nested Write-Progress bars for the work, using the -Id and -ParentId festures.

$computers = Import-Csv -Path 'H:\AllComputers.csv' -Delimiter ";"

function match-users-to-computers ($file)
{
    $list_of_matched_computers = New-Object System.Collections.Generic.List[System.Object]
    $users                     = Get-Content -Path $file
    $user_progress             = 0

    foreach ($user in $users)
    {
        $comp_progress = 0

        foreach ($comp in $computers)
        {
            $u = $comp | Select-Object -ExpandProperty "Inventory - Computer.OS Login"

            if ($u -eq $user)
            {
                $c = $comp | Select-Object -ExpandProperty "Computer.Computer Name"
                $list_of_matched_computers.Add($c)
            }

            Write-Progress -ParentId 1 -Activity "Work in Progress" ` 
                -Status "Computers: $comp_progress of $($computers.Count)" ` 
                -PercentComplete (($comp_progress / $computers.Count) * 100);
            $comp_progress++
        }

        Write-Progress -Id 1 -Activity "Work in Progress" ` 
            -Status "Users: $user_progress of $($users.Count)" ` 
            -PercentComplete (($user_progress / $users.Count) * 100);
        $user_progress++
    }

    $filename = Get-ChildItem $file | Select-Object -ExpandProperty Name
    $list_of_matched_computers | Out-File -FilePath .\$($filename)_output.txt
}

match-users-to-computers "C:\users.txt"

CSV files

For a standard .csv file, where the first line specifies the column names:

$csv = Import-Csv -Path (Join-Path (Get-Location) "test.csv") -Delimiter ';' -Encoding 'UTF-8'

To be imported CSV file has no header row

If the CSV file does not have a header row, we can define them explictly when getting the data.

$csv_no_header = Get-Content (Join-Path (Get-Location) "test.csv") |
    ConvertFrom-Csv -Delimiter ';' -Header Name, Path, ID

Count of items seperated by ‘' in a column of a CSV file

Sample input:

"test1";"folder1\folder1.1\folder1.2\folder1.3";"0010"
"test2";"folder2\folder2.1\folder2.2";"0020"
"test3";"folder3\folder3.1\folder3.2\folder3.3\folder3.4";"0030"
$csv = Get-Content (Join-Path (Get-Location) "test.csv") |
    ConvertFrom-Csv -Delimiter ';' -Header Name, Path, ID

$level_count = @()
$path_values = @()
$i = 0

ForEach ($line in $csv)
{
    $level_count += $line.Path.split('\').count
    New-Variable -Name "path_values_$($i)" -Force
    Set-Variable -Name path_values_$($i) -Value ($line.Path.split('\'))
    $path_values += Get-Variable -Name path_values_$($i)
    $i++
}

$path_values

Unexpected output in CSV file: Length numbers instead of text

Explanation: Export-Csv takes Objects and outputs properties. The only properties for a String[] is Length, so the CSV file only contains Lengths. To fix this we need to change the String[] into an Object[]. The simplest way is with Select-Object.

https://stackoverflow.com/questions/19450616/export-csv-exports-length-but-not-name

$some_data | % { (get-xxx -YY $_.Value).AccountName } |
    Select-Object @{ Name = 'AccountName'; Expression = { $_ } } |
        Export-Csv -Path "U:\temp\xx.csv" -Delimiter ';' -Encoding Default -NoTypeInformation

Clipboard

Set-Clipboard -Value "This is a test string."
Set-Clipboard -Value "This is appended." -Append  # Appends also a linebreak!

Get-Clipboard -Raw
    # 'Raw' ignores newline characters and gets the entire contents of the clipboard.

Get 6 random unique numbers from a range of 1 to 49

Get-Random -Count 6 -InputObject (1..49)

Measure objects and commands

Calculates the numeric properties of objects, and the characters, words, and lines in string objects, such as files of text:

> ($x = 1, 5, 20, 30) | Measure-Object -Maximum -Sum

Count    : 4
Average  :
Sum      : 56
Maximum  : 30
Minimum  :
Property :

Measure the time it takes to run script blocks and cmdlets:

Measure-Command -Expression { <CommandA>; <CommandB>; ... }

Select-Object: Calculate as…

Example:

Get-Process -Name p* |
    Select-Object -Property Name, Id, @{Name="multiplied by 100"; Expression={"x 100 = "+($_.Id*100)}} |
        format-table *
Name         Id multiplied by 100
----         -- -----------------
pageant    1636 x 100 = 163600
powershell 9252 x 100 = 925200
procexp    7088 x 100 = 708800
PROCEXP64  6480 x 100 = 648000

Note: @{Name="..."; Expression={...}} can be shortend to @{n="..."; e={...}}.


EventLog and Format-Table and Out-GridView

Get the ten most recent errors and warning from the System event log:

Get-EventLog -LogName System -EntryType Error, Warning -Newest 10

Show it in a table, with the most interesting part (the message) sadly being truncated.
To get the full view, use this:

Get-EventLog -LogName System -EntryType Error, Warning -Newest 10 | Format-Table -Wrap

There was/is also Out-GridView, but…


Array + Hashtable

Array

$array = @(1, 2, 3, 4)         # or simply: $array = 1, 2, 3, 4
$array[0]                      # Accessing the first element
1

# Two-Dimensional array (array of arrays)
$mdarray = @(1, 2, 3), @(4, 5, 6), @(7, 8, 9)

$mdarray[1][1]                 # Accessing the second element of the second array.
5

$array = @(                    # One can declare an array also on multiple lines.
    'Zero'                     # The comma is then optional and generally left out.
    'One'
    'Two'
    'Three'
)

Hashtable

$hashtable = @{ Cat = 'Frisky'; Dog = 'Spot'; Fish = 'Nimo'; Hamster = 'Whiskers' }

$hashtable["Fish"]             # Accessing the value for the key "Fish", which is 'Nimo'.

$hashtable = @{                # One can declare an array also on multiple lines.
    0 = 'Zero'                 # The semicolon is then optional and generally left out.
    1 = 'One'  
    2 = 'Two'  
    3 = 'Three'
}

Get all items of an hashtable as if it was an array

$ht = @{
    "One" = @(1, 2, 3)
    "Two" = @("I", "II", "III")
    "Three" = @('a', 'b', 'c')
}

You can unwrap the object’s individual elements with the GetEnumerator() method:
(Note the difference between the table and the element access: It’s one, it’s shown as Name, but is really Key!)

> $ht

Name                           Value
----                           -----
One                            {1, 2, 3}
Three                          {a, b, c}
Two                            {I, II, III}

> $ht.GetEnumerator() | % {$_.Key}
One
Three
Two

> $ht.GetEnumerator() | % {$_.Value}
1
2
3
a
b
c
I
II
III

In combination: Hashtable of Arrays

$hashtable2 = @{
    "Level 1" = @()
    "Level 2" = @()
    "Level 3" = @()
}

$hashtable2["Level 1"] += 1, 2, 3
$hashtable2["Level 2"] += 'A', 'B', 'C'

> $hashtable2

Name                           Value
----                           -----
Level 1                        {1, 2, 3}
Level 2                        {A, B, C}
Level 3                        {}

> $hashtable2["Level 2"]
A
B
C

> $hashtable2["Level 2"][1]
B

Regular Expressions

With a focus on Powershell:

A very cool webapplication to compose and test regular expressions: RegEx101.com

Note To Myself: More general stuff is in my ‘RegEx.md’ file.

More elaborate post may also be found under this blog’s RegEx tag.

Handy RegEx snippets

# Extract a text string:
$inputstring = "C:\Folder1\Foler2\AnotherFolder3\;DOMAIN\GroupName-RW;Modify, Synchronize;Allow;inherited"
$pattern = ".*DOMAIN\\(.*?);"
[regex]::Match($inputstring, $pattern).Groups[1].Value # matches "GroupName-RW"

# Get the five digits of an ID like 'id01234' and use the extracted number to build another ID like 'aaa01234zzz':
[regex] $Filter = "^([iI])([dD])(\d{5})$"
$user.extensionAttribute2 | select-string -pattern $Filter | % { $_.matches.groups[3].Value } | % { "aaa" + $_ + "zzz"}

Unicode

Embed an Unicode glyph in a string by its Code Point

I prefer to save my scripts as UTF-8 encoded files (without BOM, if possible); but I once had the strange case where I was using Send-MailMessage1 with an HTML mail body and encoding set to UTF8, but still had problems with the subject line.

While the text in the mail body was OK (mind you, it was read from an UTF-8 text file), the subject line, which was a string within the UTF-8 encoded *.ps1 file and using a german Umlaut, displayed garbled symbols or question marks instead of the desired ü when printed or when the mail was received.

So, I read a bit on the Internet, tried a few conversion things, but none of those helped. The only one which lead to success was the tip to embed the code point of the character’s glyph in the string with its hexadecimal representation (i.e. U+00FC applied as 0x00FC for “ü”), and then cast to a char:

$Subject = [System.Text.Encoding]::UTF8 # Not sure if this is strictly necessary, but it works.
$Subject = "Benutzername und Passwort f$([char]0x00FC)r den Anmeldeprozess"

  1. Yes, I know that Microsoft says it’s obsolete, but since they don’t offer an on-board alternative: What should one do for those handy ‘notification-from-a-script’ mails instead…? ↩︎