Skip to content

Instantly share code, notes, and snippets.

@bigbadmoshe
Last active November 10, 2024 08:55
Show Gist options
  • Save bigbadmoshe/8cfac7334d3423a4c1c128c568ec9164 to your computer and use it in GitHub Desktop.
Save bigbadmoshe/8cfac7334d3423a4c1c128c568ec9164 to your computer and use it in GitHub Desktop.
PowerShell Snippets

PowerShell Array Contains Explained

Arrays allow you to store multiple values in one variable. This way you can easily loop through each value and process it in your script. But sometimes you need to check if the PowerShell array contains a specific value.

To check if a value exists in an array, we can use the -Contains operator in PowerShell. This will return return true if the value exists. But there are other options as well.

The recommended way to check if an array contains a specific value is by using the -contains operator or the .Contains() method in PowerShell. Even though both look similar, there are some important differences between the two.

First, let’s take a quick look at how to use the -contains operator. To simply check if a string is in an array you can do the following:

$array = @('apple', 'banana', 'cherry')

if ($array -contains 'banana') {
    Write-Host "Array contains banana"
}else{
    Write-Host "Banana not found in the array"
}

The method above will simply check if the string exists in the array. The comparison is case insensitive, and the operator will stop when the results are found. So when an item, a fruit in the example above, is listed multiple times in the array, then it will still just return True or False.

So there are a couple of other options when it comes to checking if a value is listed in an array. Besides the .Contains() method, we can also use:

-in operator – checks if a value is in an array, just another way of writing it. -ccontains operator – case-sensitive comparison -eq – Can also check if an array contains a specific value Where-Object – Allows you to do more complex comparisons

Contains Operator vs Contains Method

So I want to first explain the difference between the contains operator and the contains method. Because they look really similar, but there is more to it.

The Contains method, written .contains(), is a method from the underlying .NET framework, whereas the operator is specific to PowerShell. Now the .NET method is faster, so when you need to look up a value in a large array, then there is a real benefit there.

Another important difference between the two is that the contains method is case-sensitive by default, whereas the operator is not:

$array = @('apple', 'banana', 'cherry', 'banana')

$array -contains 'Banana' # Returns true
$array.Contains('Banana') # Returns false

Case-Sensitive Contains

As mentioned, the contains operator is case-in-sensitive, which is often perfectly fine. But when you can also do an exact case value match, by using the -ccontains operator (note the double c).

$array = @('apple', 'banana', 'cherry', 'banana')

# Case sensitive comparison
$array -ccontains 'Banana' # Returns false

Array Not Contains

Instead of checking if an array contains a specific value, we can also check if an array does not contains a specified value. To do this, we can use the -notcontains operator. The operator will return True if the value is not listed in the array.

For example, we have a list of users who have completed a security training, and we want to verify if a particular user has not completed it yet. In this case, -notcontains is a more logical option to use:

# List of users
$completedTraining = @('John', 'Alice', 'Robert', 'Sarah')

# Check if the user has not completed the training
if ($completedTraining -notcontains 'Jane'){
    Write-Host "Jane didn't completed the training"
}

Using the -In Operator

The -in operator is the same as the contains operator, only reversed. It also returns true when the value is in the array, the only difference is that you place the array on the right of the operator, instead of the left.

You should use the -in operator when it’s more intuitive and concise for the situation. For example, if we want to check if a user is a member of an Admin group, then it makes more sense to check if “Jane” is in the admin group, than to check if the admin group contains “Jane”:

# List of admin users
$adminUsers = @('John', 'Alice', 'Robert')

# Using the -in operator
if ('Alice' -in $adminUsers) {
    Write-Output "Alice is an admin."
}

The key here is to make your code easier to read and understand. You can also use the -notin operator, which checks if a value is not in the array.

Equal Operator

You might not expect it, but we can also use the -eq operator to check if an array contains a specific value in PowerShell. The big difference with the -contains operator, however, the equal operator does not return true or false, but it returns the value.

And, even more importantly, it does not stop searching for the value when the value is found. This means that we can also count or check if a value is listed multiple times in an array:

$array = @('apple', 'banana', 'cherry', 'banana')

$result = $array -eq "banana"

if ($result) {
    Write-Host "Banana is listed $($result.Length) time(s) in the array"
}

Result
Banana is listed 2 time(s) in the array

Using Where-Object

The last option that I want to show you is the Where-Object method to check if an array contains a specific value. This method is mainly used when you have an array of objects or need to do other complex comparisons.

For example, we have an array with user objects, and want to check if the array contains a specific user:

$users = @(
    [PSCustomObject]@{ Name = 'John'; Age = 30 },
    [PSCustomObject]@{ Name = 'Jane'; Age = 25 },
    [PSCustomObject]@{ Name = 'Alice'; Age = 28 }
)

$result = $users | Where-Object { $_.Name -contains 'Jane'}

Wrapping Up

The most common way to check if an array contains a specific value is by using the -contains operator. Keep in mind though, that this is case-in-sensitive, whereas the .contains() method is case-sensitive.

Try to use the appropriate operator for the situation, this will make your code easier to read and understand.

In PowerShell 5, there are several different types of arrays that you can work with. Here are the main types

Numeric arrays: Numeric arrays in PowerShell are created using integer indices. The index of the first element is 0, and subsequent elements have indices increasing by 1. Numeric arrays can store any type of data, including strings, numbers, and objects

$numericArray = @(1, 2, 3, 4, 5)

Associative arrays (also known as hash tables): Associative arrays use key-value pairs to store data. Each element in the array is accessed using a unique key instead of an index. Keys can be of any data type, and values can be any PowerShell object

$associativeArray = @{
    "Name" = "John Doe"
    "Age" = 30
    "City" = "New York"
}

Multidimensional arrays: PowerShell supports multidimensional arrays, which are arrays with more than one dimension. You can create arrays with two or more dimensions to store data in a tabular or matrix-like structure

$multiDimArray = @(
    @(1, 2, 3),
    @(4, 5, 6),
    @(7, 8, 9)
)

Jagged arrays: Jagged arrays are arrays of arrays, where each subarray can have a different length. This allows you to create arrays with varying lengths within a single array

$jaggedArray = @(
    @(1, 2, 3),
    @(4, 5),
    @(6, 7, 8, 9)
)

Numerical Array:

  • Indices: Numerical arrays use integer indices to access and identify elements. The indices typically start from 0 and increment by 1 for each subsequent element.
  • Element Type: Numerical arrays are often used to store numeric values, such as integers or floating-point numbers.
$numericalArray = @(1, 2, 3, 4, 5)

Regular Array (Indexed Array):

  • Indices: Regular arrays also use integer indices to access and identify elements, similar to numerical arrays.
  • Element Type: Regular arrays can store elements of any data type, including strings, numbers, objects, or even a mixture of different data types.
  • Example:
$regularArray = @("Apple", "Banana", "Orange", "Grape")

The key difference between the two lies in the intended usage and the types of values typically stored. Numerical arrays are often used when the elements have a numeric nature, while regular arrays offer flexibility and can store various types of data.

In PowerShell, the term "numerical array" is not an official designation or a specific data type; rather, it is a descriptive term used to refer to arrays that predominantly store numeric values. Regular arrays, on the other hand, can encompass arrays that hold any type of data.

Both numerical arrays and regular arrays use indices to access elements, but the distinction lies in the expected data types and the common use cases associated with each.

You typically need to use the $( ) subexpression syntax in PowerShell in the following situations

Complex Expressions: When you want to embed a complex expression within a string, you can use $( ) to ensure that the expression is evaluated correctly.

$num = 5
Write-Host "The result is $($num * 2)"

The result is 10

Variable Names with Special Characters: If your variable name contains special characters, spaces, or includes properties or methods, using $( ) helps ensure that the entire variable reference is evaluated correctly.

$person = [PSCustomObject]@{
    Name = "John Doe"
    Age = 30
}
Write-Host "Person's name is $($person.Name)"

Person's name is John Doe

Nested Variable Expansion: If you want to expand multiple variables within a string, you can use $( ) to nest variable references and ensure proper evaluation.

$name = "John"
$greeting = "Hello, $($name)!"
Write-Host $greeting

Hello, John!

In summary, you can use $( ) in PowerShell to enclose complex expressions, reference variables with special characters, or nest multiple variable expansions within a string. In simpler cases, where you are directly accessing the value of a variable without any additional complexity, you can omit $( ).

ASCIIART

$ASCIIART = "ICAgICAgICAgICAgICAgICAgIExPQURJTkcuLi4gICAgICAgICAgICAgICAgICAK4pWU4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWXCuKVkSAwJSDilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojilojiloggMTAwJSDilZEgCuKVmuKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVnQogICAgICAgICAgICAgICAgICAgQ09NUExFVEVEISAg"
# https://symbl.cc/en/
# Heart Symbol (💗)
$heart = [System.Char]::ConvertFromUtf32(0x1F497)
$heartCodePoint = [int][char]$heart
Write-Host "Heart Symbol Code Point: 0x$($heartCodePoint.ToString("X4"))"
# Copyright Symbol (©)
$copyright = [System.Char]0x00A9
$copyrightCodePoint = [int][char]$copyright
Write-Host "Copyright Symbol Code Point: 0x$($copyrightCodePoint.ToString("X4"))"
# En Dash (–)
$endash = [System.Char]0x2013
$endashCodePoint = [int][char]$endash
Write-Host "En Dash Code Point: 0x$($endashCodePoint.ToString("X4"))"
©
# When we're scripting solutions, we need to understand how to concatenate strings and variables in PowerShell. "Concatenate" is just a posh word for joining/linking together.
# There are many ways to concatenate strings and variables in PowerShell – below are a few examples:
$join1 = -join ("Join", " ", "me", " ", "together")
Write-Host $join1
$join2 = "{0} {1} {2}" -f "Join", "me", "together"
Write-Host $join2
$join3 = [System.String]::Concat("Join", " ", "Me", " ", "Together")
Write-Host $join3
# The first example uses the join operator (-join), the second example uses the format operator (-f) and the third example uses the .Net Concat method.
# We can apply the same examples above by substituting the strings of text with variables like so:
$word1 = "Join"
$word2 = "me"
$word3 = "together"
$join1 = -join ($word1, " ", $word2, " ", $word3)
Write-Host $join1
$join2 = "{0} {1} {2}" -f $word1, $word2, $word3
Write-Host $join2
$join3 = [System.String]::Concat($word1, " ", $word2, " ", $word3)
Write-Host $join3
$join4 = "$word1 $word2 $word3"
Write-Host $join4
# Note above that we also added a “Join 4” example, where we merely inject the three variables into a string using double quotes to join them together. This method of substitution works slightly differently when we’re dealing with object properties. Consider this example where, for example, we’re grabbing the ID or a process called ‘explorer’:
#get process ID of first process called 'explorer'
$proc = Get-Process -Name "explorer" | Select-Object -First 1
$join4 = "$word1 $word2 $word3 and the process ID is $proc.id"
Write-Host $join4
# You will notice that that the output is:
# Join me together and the process ID is System.Diagnostics.Process (explorer).id
# Which is not what we want! What we need to do in these instances is to ask PowerShell to resolve the value of $proc.id by enclosing it inside $() like so:
$proc = Get-Process -Name "explorer" | Select-Object -First 1
$proc.id
$join4 = "$word1 $word2 $word3 and the process ID is $($proc.id)"
Write-Host $join4
And the output is now:
# Join me together and the process ID is 3648

what is dot-sourcing in Powershell

https://dotnet-helpers.com/powershell/what-is-dot-sourcing-in-powershell/

Dot-sourcing is a concept in PowerShell that allows you to reference code defined in one script.

When you writing large size of PowerShell scripts, then there always seems to come a time when a single script just isn’t enough. If we working on large size of script then it important to keep your scripts Simple and Reusable, so you can keep your block of script as modular. The Dot-sourcing is a way to do just that. Making script as modular it also useful when requirment came for adding some more functionlaiyt in exting script. Other than building a module, dot-sourcing is a better way to make that code in another script available to you.

For example, let’s say I’ve got a script that has two functions in it. We’ll call this script CommunicateUser.ps1. Here i had created single module which contain the two functionality in single file which used for sending email and SMS to the customer.

function SendEmail {
param($EmailContent)
#Write your Logic here
Write-Output "****************************************"
Write-Output "Sending Mail" $EmailContent
Write-Output "****************************************"
}`
function SentSMS {
param($SMSContent)
#Write your Logic here
Write-Output "****************************************"
Write-Output "Sending SMS" $SMSContent
Write-Output "****************************************"
}
# Script-file JustAtest.ps1 with function

 function ABCDE{
     [CmdletBinding()]
     param (
         $ComputerName
     )
     Write-Output $ComputerName
 }

# Script Call-Script.ps1 calling another script "JustAtest.ps1" in the same folder

# relative path

 .\JustAtest.ps1
 ABCDE -ComputerName Computer12

# fullpath

 C:\Junk\JustAtest.ps1
 ABCDE -ComputerName Computer13

# fullpath

 & "C:\Junk\JustAtest.ps1"
 ABCDE -ComputerName Computer14

# fullpath

 . "C:\Junk\JustAtest.ps1"
 ABCDE -ComputerName Computer15

Dotnet or not Dotnet this is the question we will ask in this post

Lets find out if the .NET .Where() method is significantly faster than their equivalent in native PowerShell In this post, we'll compare the performance of native PowerShell methods with their .NET counterparts, specifically focusing on the .Where() method. We'll also use the .net[Linq.Enumerable] class to analyze a different dataset - passenger data from the Titanic - instead of the usual Active Directory user data.

The Code We'll be using three different methods to compare performance:

# Define a collection of objects
$Import = @( 
    [PSCustomObject]@{ Name = "John"; Sex = "male" },
    [PSCustomObject]@{ Name = "Mary"; Sex = "female" },
    [PSCustomObject]@{ Name = "Peter"; Sex = "male" }
)

# .NET LINQ method
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$delegate = [Func[object,bool]] { $args[0].Sex -eq "male" }
$Result = [Linq.Enumerable]::Where($Import, $delegate)
$Result = [Linq.Enumerable]::ToList($Result)
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = "Linq Where-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})
 
# PowerShell pipeline with Where-Object
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$Result = $Import | Where-Object{$_.Sex -eq "male"}
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = "Piped Where-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})

# .NET Where() method
$StopWatch = New-Object System.Diagnostics.Stopwatch
$StopWatch.Start()
$Result = $Import.Where({$_.Sex -eq "male"})
$StopWatch.Stop()
$TestList.add([PSCustomObject]@{
    Method = ".where()-Method"
    ResultCounter = $Result.Count
    TimeElapsed = $StopWatch.Elapsed
    TimeElapsedMS = $StopWatch.ElapsedMilliseconds
})

A Scary Realization: Inconsistent Execution Times As I was checking the results and testing the reliability of my code, I executed my code segments multiple times. I noticed that there were times when there was another winner when it comes to execution time, and the results were somewhat different each time I ran the code. I was wondering how this could happen, so I decided to switch from PowerShell Version 7.x to 5.1, but the results were nearly the same.

To investigate this further, I performed the same action 101 times for each version of PowerShell on my machine and took the average of each 101 runs, and put them into a table.

The Results: Comparing PowerShell Versions 7.X and 5.1

Here are the results of my tests:

AverageOf101ms Method PSVersion
3,0495049504950495 Linq Where-Method 7
5,851485148514851 Piped Where-Method 7
1,3465346534653466 .where()-Method 7

PowerShell Version 5.1

AverageOf101ms Method PSVersion
6,88118811881188 Linq Where-Method 5
11,2871287128713 Piped Where-Method 5
3,88118811881188 .where()-Method 5
$myArray = 1..1000

# Using ForEach-Object
Measure-Command {
    $myArray | ForEach-Object {
        # Do something with the array element
        $result = $_ * 2
    }
}

# Using .ForEach() method
Measure-Command {
    $myArray.ForEach({
        # Do something with the array element
        $result = $_ * 2
    })
}

Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 13 Ticks : 136114 TotalDays : 1.57539351851852E-07 TotalHours : 3.78094444444444E-06 TotalMinutes : 0.000226856666666667 TotalSeconds : 0.0136114 TotalMilliseconds : 13.6114

Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 16 Ticks : 162860 TotalDays : 1.8849537037037E-07 TotalHours : 4.52388888888889E-06 TotalMinutes : 0.000271433333333333 TotalSeconds : 0.016286 TotalMilliseconds : 16.286

function Compare-NetVsPowerShell {
    param(
        [string]$Path
    )

    # .NET LINQ method
    $dotNetLinqStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    [System.IO.Directory]::EnumerateFiles($Path, "*", [System.IO.SearchOption]::AllDirectories) | Where-Object { $_.EndsWith('.txt') }
    $dotNetLinqStopwatch.Stop()
    $dotNetLinqTime = $dotNetLinqStopwatch.Elapsed.TotalSeconds

    # PowerShell pipeline with Where-Object
    $powershellPipeStopwatch = Measure-Command {
        Get-ChildItem -Path $Path -Recurse -File | Where-Object { $_.Extension -eq '.txt' }
    }
    $powershellPipeTime = $powershellPipeStopwatch.TotalSeconds

    # .NET Where() method
    $dotNetWhereStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    [System.IO.Directory]::GetFiles($Path, "*", [System.IO.SearchOption]::AllDirectories).Where({ $_.EndsWith('.txt') })
    $dotNetWhereStopwatch.Stop()
    $dotNetWhereTime = $dotNetWhereStopwatch.Elapsed.TotalSeconds

    Write-Host "Time taken by .NET LINQ method: $dotNetLinqTime seconds"
    Write-Host "Time taken by PowerShell pipeline with Where-Object: $powershellPipeTime seconds"
    Write-Host "Time taken by .NET Where() method: $dotNetWhereTime seconds"
}

Compare-NetVsPowerShell -Path "C:\Windows\System32"

Here we explain how to use PowerShell to read and write environment variables, which exist in the user, process and machine contexts

$env:ComputerName          # The name of the computer
$env:UserName              # The username of the current user
$env:SystemRoot            # The path to the Windows directory
$env:TEMP                  # The path to the temporary folder
$env:ProgramFiles          # The path to the Program Files directory
$env:UserProfile           # The path to the user's profile directory
$env:Path                  # The system PATH environment variable

$DesktopFolder = Join-Path -Path $env:UserProfile -ChildPath "Desktop"
$DocumentsFolder = Join-Path -Path $env:UserProfile -ChildPath "Documents"

Get-ChildItem Env:

$Env:Path -split ';' | ForEach-Object { Write-Host $_ }

# List Paths
$Env:Path

Get-Item -Path Env:

[Environment]::GetEnvironmentVariable("Path")

# List PowerShell's Paths
$Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment"
(Get-ItemProperty -Path "$Reg" -Name PATH).Path

# We can read and write environment variables in a variety of ways, but the easiest ways to read an environment variable (ComputerName in this example) are as follows

# option 1
(Get-Item -Path Env:ComputerName).Value

# option 2
$env:ComputerName

# Similarly, we can set environment variables like so:
Set-Item -Path Env:\ComputerName -Value "NewComputerName"
$env:ComputerName = "NewComputerName"

# But both of these methods only set the environment variable for the current process (i.e the current session).  To set it persistently, we can use the .net class to manipulate environment variables.  To read we can do the following:
([System.Environment]::GetFolderPath('MyDocuments'))

# Add more folders as needed...
$Console = Split-Path $Host.Name -Leaf
[System.Environment]::CommandLine

([System.Environment]::GetEnvironmentVariables()).COMPUTERNAME

# We can get variables from the different contexts like so:
[System.Environment]::GetEnvironmentVariable('ComputerName','User')
[System.Environment]::GetEnvironmentVariable('ComputerName','Process')
[System.Environment]::GetEnvironmentVariable('ComputerName','Machine')

# and set environment variables like so:
[System.Environment]::SetEnvironmentVariable('ComputerName','NewComputerName','User')
[System.Environment]::SetEnvironmentVariable('ComputerName','NewComputerName','Process')
[System.Environment]::SetEnvironmentVariable('ComputerName','NewComputerName','Machine')

# Special folders differ per user and platform. You can use this method to look up locations like the LocalAppData and favorites.
[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ApplicationData)
[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Favorites)
[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Desktop)

# Another useful method is Is64BitProcess to determine whether the currently running process is 64-bit.
[System.Environment]::Is64BitProcess

# You can also find informationa about the user, like user name.
[System.Environment]::UserName

# Using the System.Environment Class
[System.Environment]::OSVersion

# Using the Win32_OperatingSystem CIM Class
Get-CimInstance Win32_OperatingSystem

# Using the systeminfo executable
systeminfo.exe /fo csv | ConvertFrom-Csv

# Using the Get-ComputerInfo Cmdlet
# NOTE: OsHardwareAbstractionLayer was deprecated in version 21H1
Get-ComputerInfo | Select WindowsProductName, WindowsVersion, OsHardwareAbstractionLayer

<#.SYNOPSIS
    PowerShell function to modify Env:Path in the registry
    .DESCRIPTION
    Includes a parameter to append the Env:Path
    .EXAMPLE
    Add-Path -NewPath "D:\Downloads"
#>
Function Global:Add-Path {
    Param (
    [String]
    $NewPath ="D:\Powershell"
    )

    Begin
    {
    Clear-Host
    } # End of small begin section

    Process
    {
        Clear-Host
        $Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment"
        $OldPath = (Get-ItemProperty -Path "$Reg" -Name PATH).Path
        $NewPath = $OldPath + ';' + $NewPath
        Set-ItemProperty -Path "$Reg" -Name PATH -Value $NewPath -Confirm
    } #End of Process
}
# This is what you type to call the function.
# Powershell Errors
function Capture-ErrorDetails
{
param (
[Parameter(Mandatory = $true)]
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
try
{
# Skip errors with a specific code
if ($ErrorRecord.Exception.Message -match "0x80092004")
{
return
}
# Timestamp and severity
$Timestamp = Get-Date -Format 'dd/MM/yyyy HH:mm:ss.ff'
$Severity = 'ERROR'
# Capture file info (if available)
$ErrorInFile = if ($ErrorRecord.InvocationInfo.PSCommandPath)
{
Split-Path -Path $ErrorRecord.InvocationInfo.PSCommandPath -Leaf
}
else
{
'N/A'
}
# Capture other error details
$LineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber
$Line = $ErrorRecord.InvocationInfo.Line.Trim()
$Category = $ErrorRecord.CategoryInfo.Category
$ExceptionType = $ErrorRecord.Exception.GetType().FullName
$ExceptionMessage = $ErrorRecord.Exception.Message
# Format the error output
$FormattedOutput = @"
[$Timestamp] [$Severity] [InFile: $ErrorInFile] [LineNumber: $LineNumber] [Line: $Line]
[ExceptionMessage: $ExceptionMessage]
[Category: $Category] [ExceptionType: $ExceptionType]
"@
return $FormattedOutput
}
catch
{
# If error info is incomplete or another error occurs while capturing details
Write-Host "Error capturing details: $($_.Exception.Message)"
return $null
}
}
# Process the global error list
$ErrorOutput = @($Global:Error) | ForEach-Object -Process {
Capture-ErrorDetails -ErrorRecord $_
}
# Display or log the captured error details
$ErrorOutput | ForEach-Object { Write-Host $_ }
# The three methods you mentioned are used to handle and throw errors in PowerShell, but they behave slightly differently. Here's a breakdown of each:
# 1. throw $_
# What it does: This throws the current error stored in $_ within the catch block. It rethrows the exact same error that was caught by the catch, so it can propagate up to higher-level error handlers.
# When to use: Use this when you want to rethrow the same non-terminating error that occurred, without converting it to a terminating error. It just passes the error up to any outer error-handling blocks or stops execution if there are no other handlers.
# Effect: This will rethrow the current error and halt execution unless there's an outer try block to handle it.
catch
{
Write-Host "Error: $($_.Exception.Message)"
throw $_ # Rethrows the same error that was caught
}
# 2. throw
# What it does: Without any arguments, throw generates a new terminating error. If you use throw without an expression or object, it raises a generic error.
# When to use: Use this when you want to raise a new terminating error or explicitly stop execution with a custom error message or object.
try
{
# Some code
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
throw # Raises a generic terminating error
}
# 3. $PSCmdlet.ThrowTerminatingError($_)
# What it does: This is a special method in advanced functions or cmdlets (those using [CmdletBinding()]) that allows you to throw a terminating error. It explicitly marks the error as terminating and provides additional details from the error record. It's the recommended way in advanced functions to throw terminating errors because it works better in pipeline and cmdlet scenarios.
# When to use: Use this when you want to throw a terminating error in an advanced function. It provides more control, allowing the error to be handled in cmdlet pipelines and reports detailed error information.
# Effect: This will mark the error as terminating, and if your function is part of a pipeline, it will immediately stop pipeline execution.
try
{
# Some code
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
$PSCmdlet.ThrowTerminatingError($_) # Throws the error as a terminating error
}
# Key Differences:
# throw $_: Rethrows the caught error in its original form, allowing for more flexible error handling but may not stop pipeline execution unless the error was originally terminating.
# throw: Can raise a new error (or rethrow the current one if an object is passed). If nothing is passed, it raises a generic error, stopping execution.
# $PSCmdlet.ThrowTerminatingError($_): Explicitly throws a terminating error in advanced cmdlets, designed for use in functions marked with [CmdletBinding()] and in pipeline scenarios, where it immediately halts processing.
# If you are writing advanced functions and need to guarantee that an error stops execution or the pipeline, $PSCmdlet.ThrowTerminatingError($_) is the best approach.
$Error
$Error | Group-Object | Sort-Object -Property Count -Descending | Format-Table -Property Count, Name -AutoSize
$Error[0] | Format-List *
$Error[0] | Format-Table *
$Error[0] | Format-List * -Force
$Error[0].Exception
$Error[0].Exception | Format-List * -Force
$Error[0].Exception.InnerException | Format-List * -Force
$Error[0].ScriptStackTrace #for locations in PowerShell functions/scripts
$Error[0].Exception.StackTrace #for locations in compiled cmdlets/dlls
# Get exception name
$Error.Exception.GetType().FullName
(Get-Error).Exception.Type
try
{
Start-Something -Path $path -ErrorAction Stop
}
catch [System.IO.DirectoryNotFoundException], [System.IO.FileNotFoundException]
{
Write-Output "The path or file was not found: [$path]"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: [$path]"
}
try
{
Start-Something -Path $path
}
catch [System.IO.FileNotFoundException]
{
Write-Output "Could not find $path"
}
catch [System.IO.IOException]
{
Write-Output "IO error with the file: $path"
}
$ErrorOutput = @($Global:Error) | ForEach-Object -Process {
# Attempt to access properties, and only format if they exist
try
{
$Timestamp = Get-Date -Format 'dd-MM-yyyy HH:mm:ss.ffff'
$Severity = 'ERROR'
$ErrorInFile = if ($_.InvocationInfo.PSCommandPath)
{
Split-Path -Path $_.InvocationInfo.PSCommandPath -Leaf
}
$LineNumber = $_.InvocationInfo.ScriptLineNumber
$Line = $_.InvocationInfo.Line.Trim()
$CommandName = $_.InvocationInfo.MyCommand.Name
$Category = $_.CategoryInfo.Category
$ExceptionType = $_.Exception.GetType().FullName
$ExceptionMessage = $_.Exception.Message
$StackTrace = $_.Exception.StackTrace
$InnerExceptionMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message }
$FullyQualifiedErrorId = $_.FullyQualifiedErrorId
$FormattedOutput = '[{0}] [{1}] [{2}] [{3}] [{4}] [{5}] [{6}] [{7}] [{8}] [{9}] [{10}]' -f $Timestamp, $Severity, $ErrorInFile, $LineNumber, $Line, $CommandName, $Category, $ExceptionType, $ExceptionMessage, $StackTrace, $InnerExceptionMessage
}
catch
{
# Ignore errors if properties don't exist
$null
}
$FormattedOutput
}
$FormattedOutput = $ErrorOutput -join "`n"
Write-Host $FormattedOutput
# https://gist.github.com/techthoughts2/0945276362aeebb4926a11b848844926
function Reset-Errors
{
$Global:Error.Clear()
$psISE.Options.ErrorForegroundColor = '#FFFF0000'
$Global:ErrorView = 'NormalView'
}
Reset-Errors
#generate an error
function Show-Error
{
Get-Item c:\doesnotexist.txt
}
Show-Error
#all errors are stored in:
$Error
#lets make it less overwhelming (and prioritized and actionable)
$Error | Group-Object | Sort-Object -Property Count -Descending | Format-Table -Property Count, Name -AutoSize
#what about speific error details?
$Error[0] | Format-List *
#PS is dumb somtimes and this doesn't provide the date we are looking for.
#use the force, luke!
$Error[0] | Format-List * -Force
#when the top level information inst' clear, go deeper
$Error[0].Exception
$Error[0].Exception | Format-List * -Force
$Error[0].Exception.InnerException | Format-List * -Force
#leverage the stack traces
$Error[0].ScriptStackTrace #for locations in PowerShell functions/scripts
$Error[0].Exception.StackTrace #for locations in compiled cmdlets/dlls
#don't forget to clean up behind yourself as you deal with errors
$Error.Remove($Error[0]) #remove a specific error
$Error.RemoveAt(0) #remove by index
$Error.RemoveRange(0, 10) #remove by index + count
$Error.Clear() #clear the error collection
#-------------------------------------------
#consider ussing ThrowTerminating error
1 / 0
Write-Host 'Will this run?' -ForegroundColor Cyan
function Test-1
{
[CmdletBinding()]
param()
try
{
1 / 0; Write-Host 'Will this run?' -ForegroundColor Cyan
}
catch
{
$PSCmdlet.ThrowTerminatingError($_)
}
}
function Test-2
{
[CmdletBinding()]
param()
try
{
1 / 0; Write-Host 'Will this run?' -ForegroundColor Cyan
}
catch
{
throw
}
}
#compare Test-1 (clean error identifying the source line where it happened in the calling script
Test-1
#to Test-2 (error showing internals that don't identify the source line at all)
Test-2
#-------------------------------------------------------------------------------
#crafting a custom ErrorRecord for the purposes of properly mocking failures
Mock Invoke-RestMethod
{
[System.Exception]$exception = "The remote server returned an error: (400) Bad Request."
[System.String]$errorId = 'BadRequest'
[Management.Automation.ErrorCategory]$errorCategory = [Management.Automation.ErrorCategory]::InvalidOperation
[System.Object]$target = 'Whatevs'
$errorRecord = New-Object Management.Automation.ErrorRecord ($exception, $errorID, $errorCategory, $target)
[System.Management.Automation.ErrorDetails]$errorDetails = '{"message":"Database could not be reached"}'
$errorRecord.ErrorDetails = $errorDetails
throw $errorRecord
}
#-------------------------------------------------------------------------------
$formatstring = "{0} : {1}`n{2}`n" +
" + CategoryInfo : {3}`n" +
" + FullyQualifiedErrorId : {4}`n"
$fields = $_.InvocationInfo.MyCommand.Name,
$_.ErrorDetails.Message,
$_.InvocationInfo.PositionMessage,
$_.CategoryInfo.ToString(),
$_.FullyQualifiedErrorId
$formatstring -f $fields
#-------------------------------------------------------------------------------

​Get Office Installs via PS

Get-ItemProperty HKLM:\Software\Microsoft\Office\*\Registration\* | Select-Object ProductName

Get-WmiObject -Class Win32_Product | Where-Object {$_.Name -like "Microsoft Office*"} | Select-Object Name, Version

Get-ItemProperty HKLM:\Software\Microsoft\Office\*\Registration\* | Select-Object ProductName

Get-ChildItem -Path "C:\Program Files\Microsoft Office*" | Select-Object Name, LastWriteTime

$officeInstalls = Get-ItemProperty HKLM:\Software\Microsoft\Office\*\Registration\*
$installedProducts = @()

foreach ($officeInstall in $officeInstalls) {
  $installPath = $officeInstall.InstallPath + "Office"
  $productName = $officeInstall.ProductName
  if (Test-Path -Path $installPath) {
    $installedProducts += New-Object PSObject -Property @{
      Name = $productName
      InstallPath = $installPath
    }
  }
}

$installedProducts
# Example usage in PowerShell:
$BIOSRegistry = Get-ItemProperty -Path 'HKLM:\HARDWARE\DESCRIPTION\System\BIOS'
# BiosMajorRelease : Major version of the BIOS firmware.
# BiosMinorRelease : Minor version of the BIOS firmware.
# ECFirmwareMajorRelease : Major version of the embedded controller firmware.
# ECFirmwareMinorRelease : Minor version of the embedded controller firmware.
# BaseBoardManufacturer : The manufacturer of the motherboard (e.g., Hewlett-Packard).
# BaseBoardProduct : The product or model number of the motherboard.
# BaseBoardVersion : The version of the motherboard.
# BIOSReleaseDate : The release date of the BIOS firmware.
# BIOSVendor : The vendor or manufacturer of the BIOS (often matches BaseBoardManufacturer).
# BIOSVersion : The version identifier for the BIOS firmware.
# SystemFamily : Family or classification of the system (may include specific model identifiers).
# SystemManufacturer : The manufacturer of the system (often matches BaseBoardManufacturer).
# SystemProductName : The model or product name of the system (e.g., HP ProBook 450 G2).
# SystemSKU : The stock-keeping unit, identifying the specific system configuration.
# SystemVersion : Version identifier for the system.
$Output = [PSCustomObject]@{
BaseBoardManufacturer = $BIOSRegistry.BaseBoardManufacturer
BaseBoardProduct = $BIOSRegistry.BaseBoardProduct
SystemFamily = $BIOSRegistry.SystemFamily
SystemManufacturer = $BIOSRegistry.SystemManufacturer
SystemProductName = $BIOSRegistry.SystemProductName
}
Get-CimInstance -ClassName Win32_BIOS
# Useful properties:
# Manufacturer - BIOS manufacturer
# Version - BIOS version
# ReleaseDate - Date of BIOS release
# SerialNumber - BIOS serial number
Get-CimInstance -ClassName Win32_BaseBoard
# Useful Properties:
# Manufacturer - The manufacturer of the motherboard (e.g.,Dell, ASUS).
# Product - The model or product name of the motherboard.
# SerialNumber - The serial number of the motherboard, which can be useful for inventory tracking.
# Version - The version of the motherboard.
# Name - The name of the baseboard, though it's often generic.
# Status - Operational status of the motherboard; usually OK or another state if there is a problem.
# PoweredOn - Indicates if the baseboard is currently powered on (though this property may not be populated on all systems).
Get-CimInstance -ClassName Win32_Processor
# Useful properties:
# Name - Processor name
# NumberOfCores - Number of cores
# NumberOfLogicalProcessors - Logical processors (threads)
# MaxClockSpeed - Max clock speed in MHz
# ProcessorId - Unique processor ID
Get-CimInstance -ClassName Win32_DiskDrive
# Useful properties:
# Model - Disk model
# Size - Size of disk in bytes
# SerialNumber - Disk serial number
# MediaType - Media type (e.g., SSD or HDD)
Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled }
# Useful properties:
# Description - Network adapter name
# MACAddress - MAC address
# IPAddress - Array of IP addresses
# DefaultIPGateway - Default gateway
# DNSServerSearchOrder - DNS servers
Get-CimInstance -ClassName Win32_LogicalDisk
# Useful properties:
# DeviceID - Drive letter (e.g.,C:)
# DriveType - Type of drive (e.g.,fixed, network, CD-ROM)
# FileSystem - File system type (e.g.,NTFS)
# FreeSpace - Free space in bytes
# Size - Total size in bytes
# Laptops
Get-CimInstance -ClassName Win32_Battery
# Useful Properties:
# BatteryStatus - Indicates the current battery status:
# 1 = Discharging
# 2 = AC power, not charging
# 3 = Fully charged
# 4 = Low
# 5 = Critical
# 6 = Charging
# 7 = Charging and high
# 8 = Charging and low
# 9 = Charging and critical
# 10 = Undefined
# 11 = Partially charged
# EstimatedChargeRemaining - Remaining battery charge as a percentage.
# EstimatedRunTime - Estimated remaining runtime in minutes (if discharging).
# ExpectedBatteryLife - Estimated total battery life in minutes.
# ExpectedLife - Expected battery life span, usually in years.
# TimeOnBattery - Time, in seconds, since the last switch to battery power.
# DesignCapacity - Original capacity of the battery in mWh.
# FullChargeCapacity - Current full charge capacity in mWh.
# Chemistry - Battery chemistry (e.g., Lithium-Ion, Nickel-Cadmium).
Get-CimInstance -ClassName Win32_ComputerSystem
# Useful properties:
# Manufacturer - Manufacturer of the system (e.g.,Dell, HP)
# Model - Model of the system
# SystemType - System type (e.g.,x64-based PC)
# TotalPhysicalMemory - Total RAM in bytes
Get-CimInstance -ClassName Win32_OperatingSystem
# Useful properties:
# Caption - OS name (e.g., Windows 10 Pro)
# Version - Version number (e.g., 10.0.19042)
# BuildNumber - Build number
# OSArchitecture - Architecture (e.g., 64-bit)
# InstallDate - OS installation date
# LastBootUpTime - Last reboot time
# SerialNumber - OS serial number
# https://github.com/Trael-Kun/Powershell/blob/a3158f47be8f56f173d79ec5da757089a3920626/BIOS_Dell_UpdateBios.ps1
###################################################################################################
# TASK - GET BATTERY EXISTANCE AND AMOUNT OF BATTERY CHARGE REMAINING
###################################################################################################
[int]$HasBattery = (Get-CimInstance -ClassName Win32_Battery).Availability
[int]$ChargeRemaining = (Get-CimInstance -ClassName Win32_Battery).EstimatedChargeRemaining
###################################################################################################
# TASK - IF THE BATTERY IS LESS THAN 90% CHARGED, WAIT UNTIL IT IS
###################################################################################################
if ($HasBattery -ne 0 -AND $ChargeRemaining -le 60)
{
DO
{
Start-Sleep -s 20
[int]$ChargeRemaining = (Get-CimInstance -ClassName Win32_Battery).EstimatedChargeRemaining
Format-Form
} Until ($ChargeRemaining -ge 60)
}

logging functions

#==============================================================================================
#region Logging Functions
function Write-Log
{
    [cmdletbinding()]
    param([Parameter(
        Mandatory=$true, Position=0, ValueFromPipeline=$true
        )]
        [AllowEmptyString()]
        [string]$Message
    )
    Write-Verbose  ("[{0:s}] [$(get-caller)] {1}`r`n" -f (get-date), $Message)
}

function write-ok($text="")
    { write-emoji -code "u+2705" -text $text }

function write-done($text="")
    { write-emoji -code "u+2705" -text "done. $text" }

function write-angry
    { write-emoji "u+1F975" }

function write-emoji($code='u+2705', $text="")
{
    $StrippedUnicode = $code -replace 'U\+',''
    $UnicodeInt = [System.Convert]::toInt32($StrippedUnicode,16)
    [System.Char]::ConvertFromUtf32($UnicodeInt) + " $text"
}

function Write-Success($Message)
    {
        Write-Host (New-FormattedLog -Type "WIN" -Time (get-date) -Message $Message) -ForegroundColor Green
    }

function Write-Fail($Message)
    {
         Write-Host (New-FormattedLog -Type "ERROR" -Time (get-date) -Message $Message) -ForegroundColor Red
    }

function Write-Debug($Message)
    {
         Write-Host (New-FormattedLog -Type "DEBUG" -Time (get-date) -Message $Message) -ForegroundColor Cyan
     }

function Write-Normal($Message)
    {
        Write-Host (New-FormattedLog -Type "INFO" -Time (get-date) -Message $Message)
}

function Write-Yellow($Message)
    {
        Write-Host (New-FormattedLog -Type "INFO" -Time (get-date) -Message $Message) -ForegroundColor Yellow
    }

function Write-Color($Message,$color)
    {
        Write-Host (New-FormattedLog -Type "INFO" -Time (get-date) -Message $Message) -ForegroundColor $color
    }

function New-FormattedLog ($Type, $Time, $Message)
    {
        ("[{0,-5}] [{1:s}] [{2}] {3}`r" -f $type, $Time, (get-caller 3), $Message)
    }

function get-caller($stackNum = 2)
    {
         process{ return ((Get-PSCallStack)[$stackNum].Command) }
    }

function write-compare
{
    [cmdletbinding()]
    Param($label,$expected,$actual)
    return ("{0,-15} Expected: {1,-45} ==> Actual: {2,-25}" -f $label, "[$expected]", "[$actual]" )
}

function write-header
{
    [cmdletbinding()]
    Param($label,$color= "magenta", [switch]$min, $max = 100)
    $i = $label | measure-object -character | select -expandproperty characters
    $split = (($max-$i)/2)

    if (-not $min)
     { 
        write-host "$('='*$max)" -foregroundcolor $color
      }

    write-host ("{0}  {1}  {2}" -f ('='*($split-4)),$label, ('='*$split)) -foregroundcolor $color
    if (-not $min)
     { 
        write-host "$('='*$max)" -foregroundcolor $color
     }
}
#endregion

#==============================================================================================

Export-ModuleMember -Function Write-Log
Export-ModuleMember -Function write-ok
Export-ModuleMember -Function write-done
Export-ModuleMember -Function write-angry
Export-ModuleMember -Function write-emoji
Export-ModuleMember -Function Write-Success
Export-ModuleMember -Function Write-Fail
Export-ModuleMember -Function Write-Debug
Export-ModuleMember -Function Write-Normal
Export-ModuleMember -Function Write-Yellow
Export-ModuleMember -Function New-FormattedLog
Export-ModuleMember -Function get-caller
Export-ModuleMember -Function write-compar
Export-ModuleMember -Function write-header
#Export-ModuleMember -Function *
Function Write-ProtocolEntry {

        <#
        .SYNOPSIS

            Output of an event with timestamp and different formatting
            depending on the level. If the Log parameter is set, the
            output is also stored in a file.
        #>

        [CmdletBinding()]
        Param (

            [String]
            $Text,

            [String]
            $LogLevel
        )

        $Time = Get-Date -Format G

        Switch ($LogLevel) {
            "Info"    { $Message = "[*] $Time - $Text"; Write-Host $Message; Break }
            "Debug"   { $Message = "[-] $Time - $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
            "Warning" { $Message = "[?] $Time - $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
            "Error"   { $Message = "[!] $Time - $Text"; Write-Host -ForegroundColor Red $Message; Break }
            "Success" { $Message = "[$] $Time - $Text"; Write-Host -ForegroundColor Green $Message; Break }
            "Notime"  { $Message = "[*] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
            Default   { $Message = "[*] $Time - $Text"; Write-Host $Message; }
        }

        If ($Log) {
            Add-MessageToFile -Text $Message -File $LogFile
        }
    }

    Function Add-MessageToFile {

        <#
        .SYNOPSIS

            Write message to a file, this function can be used for logs,
            reports, backups and more.
        #>

        [CmdletBinding()]
        Param (

            [String]
            $Text,

            [String]
            $File
        )

        try {
            Add-Content -Path $File -Value $Text -ErrorAction Stop
        } catch {
            Write-ProtocolEntry -Text "Error while writing log entries into $File. Aborting..." -LogLevel "Error"
            Break
        }

    }

    Function Write-ResultEntry {

        <#
        .SYNOPSIS

            Output of the assessment result with different formatting
            depending on the severity level. If emoji support is enabled,
            a suitable symbol is used for the severity rating.
        #>

        [CmdletBinding()]
        Param (

            [String]
            $Text,

            [String]
            $SeverityLevel
        )

        If ($EmojiSupport) {

            Switch ($SeverityLevel) {

                "Passed" { $Emoji = [char]::ConvertFromUtf32(0x1F63A); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
                "Low"    { $Emoji = [char]::ConvertFromUtf32(0x1F63C); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
                "Medium" { $Emoji = [char]::ConvertFromUtf32(0x1F63F); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
                "High"   { $Emoji = [char]::ConvertFromUtf32(0x1F640); $Message = "[$Emoji] $Text"; Write-Host -ForegroundColor Red $Message; Break }
                Default  { $Message = "[*] $Text"; Write-Host $Message; }
            }

        } Else {

            Switch ($SeverityLevel) {

                "Passed" { $Message = "[+] $Text"; Write-Host -ForegroundColor Gray $Message; Break }
                "Low"    { $Message = "[-] $Text"; Write-Host -ForegroundColor Cyan $Message; Break }
                "Medium" { $Message = "[$] $Text"; Write-Host -ForegroundColor Yellow $Message; Break }
                "High"   { $Message = "[!] $Text"; Write-Host -ForegroundColor Red $Message; Break }
                Default  { $Message = "[*] $Text"; Write-Host $Message; }
            }
        }
    }
## Set up logging function
function Log-Message
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Error','Info')]
        [string]$Level,
        [Parameter(Mandatory=$true)]
        [string]$Message
    )

# Get current date and time
$timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')

# Get current script or function name
$caller = (Get-Variable MyInvocation -Scope 1).Value.InvocationName

# Get current line number
$line = (Get-Variable MyInvocation -Scope 1).Value.ScriptLineNumber

# Get current file name
$file = (Get-Variable MyInvocation -Scope 1).Value.ScriptName

# Build log message
$logMessage = "[$timestamp] [$Level] [$file:$line] $caller: $Message"

# Write log message to file
Add-Content -Path C:\logs\script.log -Value $logMessage

# Write log message to console (optional)
Write-Output $logMessage
}

# Example usage
Log-Message -Level Info -Message "This is an info message"
Log-Message -Level Error -Message "This is an error message"
# Set up log file path and filename
$logFile = "C:\Logs\Script.log"

# Set up log function
function Write-Log
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateSet('Error','Info')]
        [string]$Type,
        [Parameter(Mandatory=$true)]
        [string]$Message
    )

    # Get caller information
    $caller = (Get-Variable MyInvocation -Scope 1).Value
    $line = $caller.ScriptLineNumber
    $position = $caller.ScriptLineNumber
    $file = $caller.ScriptName
    $function = $caller.MyCommand.Name

    # Format log message
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp $Type: Line $line, Pos $position, File $file, Function $function - $Message"
    $logMessage = '{0} [{1}] [{2} {3} {4}]: {5}' -f $Timestamp, $Type, $Line - $Function - $File, $Message

    # Write log message to file
    Add-Content -Path $logFile -Value $logMessage

    # Write log message to console
    Write-Output $logMessage
}

# Example usage
Write-Log -Type Error -Message "An error has occurred."
Write-Log -Type Info -Message "This is an info message."
function Log
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Info", "Warning", "Error")]
        [string]$Severity = "Info"
    )

    # Determine the current date and time
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    # Write the log message to the console
    Write-Output "$Timestamp $Severity: $Message"

    # Append the log message to a log file
    Add-Content -Path "C:\Scripts\Logs\Script.log" -Value "$Timestamp $Severity: $Message"
}

Log -Message "This is an info message"
Log -Message "This is a warning message" -Severity "Warning"
Log -Message "This is an error message" -Severity "Error"
Function Write-LogEntry
{
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message,

        [Parameter(
            Mandatory = $false,
            Position = 2
        )]
        [string]
        $Source = '',

        [Parameter(
            Mandatory = $false,
            HelpMessage = "Severity for the log entry (INFORMATION, WARNING or ERROR)"
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(
            "INFORMATION",
            "WARNING",
            "ERROR"
        )]
        [String]
        $Severity = "INFORMATION",

        [parameter(
            Mandatory = $false,
            HelpMessage = "Name of the log file that the entry will written to"
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutputLogFile = $Global:LogFilePath
    )

    Begin
    {
        $TimeStamp = Get-Date -Format '[MM/dd/yyyy hh:mm:ss]'
    }

    Process
    {
        # Get the file name of the source script
        Try
        {
            If ($script:MyInvocation.Value.ScriptName)
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -LeafBase -ErrorAction 'Stop'
            }
            Else
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -LeafBase -ErrorAction 'Stop'
            }
        }
        Catch
        {
            $ScriptSource = ''
        }

        # $LogFormat = "{0} {1}: {2} {3}" -f $TimeStamp, $Severity, $ScriptSource, $Message
        $LogFormat = ("[$TimeStamp] [$Severity]:: In File:$ScriptSource Error:$Error[0].Exception.Message $Message")

        # Add value to log file
        try
        {
            if ( -not (Test-Path -Path $LogFilePath -PathType leaf) )
            {
                Add-Content -Path $OutputLogFile -Value $LogFormat -Encoding Default

            }
        }
        catch
        {
            Write-Host ("[{0}] [{1}]: Unable to append log entry to [{1}], Error: {2}" -f $TimeStamp, $ScriptSource, $OutputLogFile, "$Error[0].Exception.Message") -ForegroundColor Red
        }
    }
}

Function Write-LogEntry
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,

        [Parameter(Mandatory = $false, Position = 2)]
        [string]$Source,

        [parameter(Mandatory = $false)]
        [ValidateSet(0, 1, 2, 3, 4, 5)]
        [int16]$Severity = 1,

        [parameter(Mandatory = $false, HelpMessage = "Name of the log file that the entry will written to.")]
        [ValidateNotNullOrEmpty()]
        [string]$OutputLogFile = $Global:LogFilePath,

        [parameter(Mandatory = $false)]
        [switch]$Outhost
    )
    ## Get the name of this function
    #[string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
    if (-not $PSBoundParameters.ContainsKey('Verbose'))
    {
        $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference')
    }

    if (-not $PSBoundParameters.ContainsKey('Debug'))
    {
        $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference')
    }
    #get BIAS time
    [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
    [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
    [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes
    [string]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias

    #  Get the file name of the source script
    If ($Source)
    {
        $ScriptSource = $Source
    }
    Else
    {
        Try
        {
            If ($script:MyInvocation.Value.ScriptName)
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -Leaf -ErrorAction 'Stop'
            }
            Else
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -Leaf -ErrorAction 'Stop'
            }
        }
        Catch
        {
            $ScriptSource = ''
        }
    }

    #if the severity is 4 or 5 make them 1; but output as verbose or debug respectfully.
    If ($Severity -eq 4) { $logSeverityAs = 1 }Else { $logSeverityAs = $Severity }
    If ($Severity -eq 5) { $logSeverityAs = 1 }Else { $logSeverityAs = $Severity }

    #generate CMTrace log format
    $LogFormat = "<![LOG[$Message]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$ScriptSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$logSeverityAs`" " + "thread=`"$PID`" " + "file=`"$ScriptSource`">"

    # Add value to log file
    try
    {
        Out-File -InputObject $LogFormat -Append -NoClobber -Encoding Default -FilePath $OutputLogFile -ErrorAction Stop
    }
    catch
    {
        Write-Host ("[{0}] [{1}] :: Unable to append log entry to [{1}], error: {2}" -f $LogTimePlusBias, $ScriptSource, $OutputLogFile, $_.Exception.ErrorMessage) -ForegroundColor Red
    }

    #output the message to host
    If ($Outhost)
    {
        If ($Source)
        {
            $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $Source, $Message)
        }
        Else
        {
            $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $ScriptSource, $Message)
        }

        Switch ($Severity)
        {
            0 { Write-Host $OutputMsg -ForegroundColor Green }
            1 { Write-Host $OutputMsg -ForegroundColor Gray }
            2 { Write-Host $OutputMsg -ForegroundColor Yellow }
            3 { Write-Host $OutputMsg -ForegroundColor Red }
            4 { Write-Verbose $OutputMsg }
            5 { Write-Debug $OutputMsg }
            default { Write-Host $OutputMsg }
        }
    }
}
function Write-ErrorsToFile
{
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $false,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [string]
        $Message,

        [parameter(
            Mandatory = $false
        )]
        [ValidateSet(0, 1, 2, 3, 4, 5)]
        [int16]
        $Severity = 1,

        [parameter(
            Mandatory = $false,
            HelpMessage = "Name of the log file that the entry will written to."
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutputLogFile = $Global:LogFilePath
    )

    # $DateStamp = Get-Date -Format 'MM-dd-yyyy'
    # $LogFilePath = "$env:USERPROFILE\Desktop\$DateStamp-$env:ComputerName-GlobalErrors.log"

    if ($Global:Error)
    {
        if ( -not (Test-Path -Path $LogFilePath -PathType leaf) )
        {
            New-Item -Path $LogFilePath -ItemType File -Force
        }

    ($Global:Error | ForEach-Object -Process {
            # Some errors may have the Windows nature and don't have a path to any of the module's files
            $Global:ErrorInFile = if ($_.InvocationInfo.PSCommandPath)
            {
                Split-Path -Path $_.InvocationInfo.PSCommandPath -Leaf
            }

            [PSCustomObject] @{
                TimeStamp  = (Get-Date -Format '[MM/dd/yyyy hh:mm:ss]')
                LineNumber = $_.InvocationInfo.ScriptLineNumber
                InFile     = $ErrorInFile
                Message    = $_.Exception.Message
            }
        } | Sort-Object TimeStamp | Format-Table -HideTableHeaders -Wrap | Out-String).Trim() | Add-Content -Path $LogFilePath -Force -Encoding Default

        if (Test-Path -Path '\\DD2\Logs$' -PathType Container)
        {
            Move-Item -Path "$env:USERPROFILE\Desktop\*.log" -Destination '\\DD2\Logs$' -Force
        }
    }
}
<#
    .SYNOPSIS
    Writes a message to a log file.

    .DESCRIPTION
        Writes an informational, warning or error message to a log file. Log entries can be written in basic (default) or cmtrace format. When using basic format, you can choose to include a date/time stamp if required.

    .PARAMETER Message
        THe message to write to the log file

    .PARAMETER Severity
        The severity of message to write to the log file. This can be Information, Warning or Error. Defaults to Information.

    .PARAMETER Path
        The path to the log file. Recommended to use Set-LogPath to set the path.
        #.PARAMETER AddDateTime (currently not supported)
        Adds a datetime stamp to each entry in the format YYYY-MM-DD HH:mm:ss.fff

    .EXAMPLE
        Write-LogEntry -Message "Searching for file" -Severity Information -Path C:\MyLog.log

        Description
        -----------
        Writes a basic log entry

    .EXAMPLE
        Write-LogEntry -Message "Searching for file" -Severity Warning -LogPath C:\MyLog.log -CMTraceFormat

        Description
        -----------
        Writes a CMTrace format log entry

    .EXAMPLE
        $Script:LogPath = "C:\MyLog.log"
        Write-LogEntry -Message "Searching for file" -Severity Information

        Description
        -----------
        First line creates the script variable LogPath
        Second line writes to the log file.
#>

function Write-LogEntry
{
    [CmdletBinding()]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [String]
        $Message,

        [Parameter(
            Mandatory = $false,
            Position = 1,
            HelpMessage = "Severity for the log entry (Information, Warning or Error)"
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(
            "Information",
            "Warning",
            "Error")]
        [String]
        $Severity = "Information",

        [Parameter(
            Mandatory = $false,
            Position = 2,
            HelpMessage = "The full path of the log file that the entry will written to"
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            { (Test-Path -Path $_.Substring(0, $_.LastIndexOf("\")) -PathType Container) -and (Test-Path -Path $_ -PathType Leaf -IsValid) }
        )]
        [String]
        $Path = $LogFilePath,

        [Parameter(
            ParameterSetName = "CMTraceFormat",
            HelpMessage = "Indicates to use cmtrace compatible logging"
        )]
        [Switch]
        $CMTraceFormat
    )

    # Construct date and time for log entry (based on current culture)
    $Date = Get-Date -Format (Get-Culture).DateTimeFormat.ShortDatePattern
    $Time = Get-Date -Format (Get-Culture).DateTimeFormat.LongTimePattern.Replace("ss", "ss.fff")

    # Determine parameter set
    if ($CMTraceFormat)
    {
        # Convert severity value
        switch ($Severity)
        {
            "Information"
            {
                $CMSeverity = 1
            }
            "Warning"
            {
                $CMSeverity = 2
            }
            "Error"
            {
                $CMSeverity = 3
            }
        }

        # Construct components for log entry
        $Component = (Get-PSCallStack)[1].Command
        $ScriptFile = $MyInvocation.ScriptName

        # Construct context for CM log entry
        $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
        $LogText = "<![LOG[$($Message)]LOG]!><time=""$($Time)"" date=""$($Date)"" component=""$($Component)"" context=""$($Context)"" type=""$($CMSeverity)"" thread=""$($PID)"" file=""$($ScriptFile)"">"
    }
    else
    {
        # Construct basic log entry
        # AddDateTime parameter currently not supported
        #if ($AddDateTime) {
        $LogText = "[{0} {1}] {2}: {3}" -f $Date, $Time, $Severity, $Message
        $logMessage = '{0} [{1}] [{2} {3} {4}]: {5}' -f $Timestamp, $Type, $Line - $Function - $File, $Message
        #}
        #else {
        #    $LogText = "{0}: {1}" -f $Severity, $Message
        #}
    }

    # Add value to log file
    try
    {
        Out-File -InputObject $LogText -Append -NoClobber -Encoding Default -FilePath $Path -ErrorAction Stop
    }
    catch [System.Exception]
    {
        Write-Warning -Message "Unable to append log entry to $($Path) file. Error message: $($_.Exception.Message)"
    }
}
<#
.SYNOPSIS
    A short one-line action-based description, e.g. 'Tests if a function is valid'

.DESCRIPTION
    A longer description of the function, its purpose, common use cases, etc.

.NOTES
    Information or caveats about the function e.g. 'This function is not supported in Linux'

.LINK
    Specify a URI to a help page, this will show when Get-Help -Online is used.

.EXAMPLE
    Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

    Write-LogEntry ("Reboot is required for remving the Feature on Demand package: {0}" -f $FeatName)

    Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

    Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

    try
        {
            Show-ProgressStatus -Message ("Removing Feature on Demand V2 package: {0}" -f $Feature) -Step $f -MaxStep $OnDemandFeatures.count -Outhost

            $results = Remove-WindowsCapability -Name $Feature -Online -ErrorAction Stop
            if ($results.RestartNeeded -eq $true)
            {
                Write-LogEntry ("Reboot is required for remving the Feature on Demand package: {0}" -f $FeatName)
            }
        }
        catch [System.Exception]
        {
            Write-LogEntry -Message ("Failed to remove Feature on Demand V2 package: {0}" -f $_.Message) -Severity 3 -Outhost
        }

        $ErrorMessage = $_.Exception.Message

Write-LogEntry ("Unable to remove item from [{0}]. Error [{1}]" -f $Path.FullName, $ErrorMessage) -Source ${CmdletName} -Severity 3 -Outhost

Write-LogEntry -Message ("Failed to copy files: {0}. Error [{1}]" -f $ErrorMessage) -Source ${CmdletName} -Severity 3 -Outhost

Write-LogEntry ("Unable to remove item from [{0}] because it does not exist any longer" -f $Item.FullName) -Source ${CmdletName} -Severity 2 -Outhost

Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

Write-LogEntry ("Reboot is required for remving the Feature on Demand package: {0}" -f $FeatName)

Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

Write-LogEntry -Message ("Removed {0} built-in App PROVISIONED Package's" -f $d) -Outhost

try
{
    Show-ProgressStatus -Message ("Removing Feature on Demand V2 package: {0}" -f $Feature) -Step $f -MaxStep $OnDemandFeatures.count -Outhost

    $results = Remove-WindowsCapability -Name $Feature -Online -ErrorAction Stop
    if ($results.RestartNeeded -eq $true)
    {
        Write-LogEntry ("Reboot is required for remving the Feature on Demand package: {0}" -f $FeatName)
    }
}
catch [System.Exception]
{
    Write-LogEntry -Message ("Failed to remove Feature on Demand V2 package: {0}" -f $_.Message) -Severity 3 -Outhost
}
#>

Function Write-LogEntry
{
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Message,

        [Parameter(
            Mandatory = $false,
            Position = 2
        )]
        [string]
        $Source = '',

        [parameter(
            Mandatory = $false
        )]
        [ValidateSet(0, 1, 2, 3, 4)]
        [int16]
        $Severity,

        [parameter(
            Mandatory = $false,
            HelpMessage = "Name of the log file that the entry will written to"
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutputLogFile = $Global:LogFilePath,

        [parameter(
            Mandatory = $false
        )]
        [switch]
        $Outhost
    )

    Begin
    {
        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
        [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes
        [string]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias

    }

    Process
    {
        # Get the file name of the source script
        Try
        {
            If ($script:MyInvocation.Value.ScriptName)
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -Leaf -ErrorAction 'Stop'
            }
            Else
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -Leaf -ErrorAction 'Stop'
            }
        }
        Catch
        {
            $ScriptSource = ''
        }


        If (!$Severity)
        {
            $Severity = 1
        }

        $LogFormat = "<![LOG[$Message]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$ScriptSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$Severity`" " + "thread=`"$PID`" " + "file=`"$ScriptSource`">"

        # Add value to log file
        try
        {
            Out-File -InputObject $LogFormat -Append -NoClobber -Encoding Default -FilePath $OutputLogFile -ErrorAction Stop
        }
        catch
        {
            Write-Host ("[{0}] [{1}] :: Unable to append log entry to [{1}], error: {2}" -f $LogTimePlusBias, $ScriptSource, $OutputLogFile, $_.Exception.Message) -ForegroundColor Red
        }
    }
    End
    {
        If ($Outhost -or $Global:OutTohost)
        {
            If ($Source)
            {
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $Source, $Message)
            }
            Else
            {
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $ScriptSource, $Message)
            }

            Switch ($Severity)
            {
                0 { Write-Host $OutputMsg -ForegroundColor Green }
                1 { Write-Host $OutputMsg -ForegroundColor Gray }
                2 { Write-Warning $OutputMsg }
                3 { Write-Host $OutputMsg -ForegroundColor Red }
                4 { If ($Global:Verbose) { Write-Verbose $OutputMsg } }
                default { Write-Host $OutputMsg }
            }
        }
    }
}
Function Format-ElapsedTime
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $TS
    )

    $elapsedTime = ""
    if ($ts.Minutes -gt 0)
    {
        $elapsedTime = [string]::Format( "{0:00} min. {1:00}.{2:00} sec", $ts.Minutes, $ts.Seconds, $ts.Milliseconds / 10 )
    }
    else
    {
        $elapsedTime = [string]::Format( "{0:00}.{1:00} sec", $ts.Seconds, $ts.Milliseconds / 10 )
    }
    if ($ts.Hours -eq 0 -and $ts.Minutes -eq 0 -and $ts.Seconds -eq 0)
    {
        $elapsedTime = [string]::Format("{0:00} ms", $ts.Milliseconds)
    }
    if ($ts.Milliseconds -eq 0)
    {
        $elapsedTime = [string]::Format("{0} ms", $ts.TotalMilliseconds)
    }
    return $elapsedTime
}

Function Format-DatePrefix
{
    [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
    [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
    return ($LogDate + " " + $LogTime)
}
<#
.EXAMPLE
    Write-LogEntry -Message ("Unable to process appx removal because the Windows OS version [{0}] was not tested" -f $OSInfo.version)

    Write-LogEntry -Message "Failed removing AppxProvisioningPackage: $($Error[0].Exception.Message)" -Severity Error

    Write-LogEntry -Message "Failed : $($Error[0].Exception.Message)" -Severity Warning
    Write-LogEntry -Message "Failed : $($Error[0].Exception.Message)" -Severity Warning -CMTraceFormat

    Write-LogEntry ("LGPO applying [{3}] to registry: [{0}\{1}\{2}] as a Group Policy item" -f 'hello','Testing','one','two') -CMTraceFormat

    try
    {
        Show-ProgressStatus -Message ("Removing Feature on Demand V2 package: {0}" -f $Feature) -Step $f -MaxStep $OnDemandFeatures.count -Outhost

        $results = Remove-WindowsCapability -Name $Feature -Online -ErrorAction Stop
        if ($results.RestartNeeded -eq $true)
        {
            Write-LogEntry ("Reboot is required for removing the Feature on Demand package: {0}" -f $FeatName)
        }
    }
    catch [System.Exception]
    {
        Write-LogEntry -Message ("Failed to remove Feature on Demand V2 package: {0}" -f $_.Message) -Severity 3 -Outhost
    }
# >

Function Write-LogEntry
{
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $Message,

        [Parameter(
            Mandatory = $false,
            Position = 2
        )]
        [string]
        $Source = '',

        [Parameter(
            Mandatory = $false,
            HelpMessage = "Severity for the log entry (Information, Warning or Error)"
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet(
            "Information",
            "Warning",
            "Error"
        )]
        [String]
        $Severity = "Information",

        [parameter(
            Mandatory = $false,
            HelpMessage = "Name of the log file that the entry will written to"
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $OutputLogFile = $Global:LogFilePath,

        [parameter(
            Mandatory = $false
        )]
        [switch]
        $Outhost,

        [Parameter(
            ParameterSetName = "CMTraceFormat",
            HelpMessage = "Indicates to use cmtrace compatible logging"
        )]
        [Switch]
        $CMTraceFormat
    )

    Begin
    {
        [string]$LogTime = (Get-Date -Format 'HH:mm:ss.fff').ToString()
        [string]$LogDate = (Get-Date -Format 'MM-dd-yyyy').ToString()
        [int32]$script:LogTimeZoneBias = [timezone]::CurrentTimeZone.GetUtcOffset([datetime]::Now).TotalMinutes
        [string]$LogTimePlusBias = $LogTime + $script:LogTimeZoneBias

        $TimeStamp = Get-Date -Format '[MM/dd/yyyy hh:mm:ss]'
    }

    Process
    {
        # Get the file name of the source script
        Try
        {
            If ($script:MyInvocation.Value.ScriptName)
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.Value.ScriptName -Leaf -ErrorAction 'Stop'
            }
            Else
            {
                [string]$ScriptSource = Split-Path -Path $script:MyInvocation.MyCommand.Definition -Leaf -ErrorAction 'Stop'
            }
        }
        Catch
        {
            $ScriptSource = ''
        }

        if ($CMTraceFormat)
        {
            # Convert severity value
            switch ($Severity)
            {
                "Information"
                {
                    $CMSeverity = 1
                }
                "Warning"
                {
                    $CMSeverity = 2
                }
                "Error"
                {
                    $CMSeverity = 3
                }
            }

            $LogFormat = "<![LOG[$Message]LOG]!>" + "<time=`"$LogTimePlusBias`" " + "date=`"$LogDate`" " + "component=`"$ScriptSource`" " + "context=`"$([Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + "type=`"$CMSeverity`" " + "thread=`"$PID`" " + "file=`"$ScriptSource`">"
        }
        else
        {
            $LogFormat = "{0} {1}: {2}" -f $TimeStamp, $Severity, $ScriptSource + $Message
        }

        # Add value to log file
        try
        {
            Out-File -InputObject $LogFormat -Append -NoClobber -Encoding Default -FilePath $OutputLogFile -ErrorAction Stop
        }
        catch
        {
            Write-Host ("[{0}] [{1}] :: Unable to append log entry to [{1}], error: {2}" -f $LogTimePlusBias, $ScriptSource, $OutputLogFile, $_.Exception.Message) -ForegroundColor Red
        }
    }

    End
    {
        If ($Outhost -or $Global:OutTohost)
        {
            If ($Source)
            {
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $Source, $Message)
            }
            Else
            {
                $OutputMsg = ("[{0}] [{1}] :: {2}" -f $LogTimePlusBias, $ScriptSource, $Message)
            }

            Switch ($Severity)
            {
                0 { Write-Host $OutputMsg -ForegroundColor Green }
                1 { Write-Host $OutputMsg -ForegroundColor Gray }
                2 { Write-Warning $OutputMsg }
                3 { Write-Host $OutputMsg -ForegroundColor Red }
                4 { If ($Global:Verbose) { Write-Verbose $OutputMsg } }
                default { Write-Host $OutputMsg }
            }
        }
    }
}

$DateStamp = Get-Date -Format 'MM-dd-yyyy'

if ($Global:Error)
{
    if ( -not (Test-Path -Path "$env:USERPROFILE\Desktop\$DateStamp-Errors.log" -PathType leaf) )
    {
        New-Item -Path "$env:USERPROFILE\Desktop\$DateStamp-Errors.log" -ItemType File -Force
    }

    ($Global:Error | ForEach-Object -Process {
        # Some errors may have the Windows nature and don't have a path to any of the module's files
        $Global:ErrorInFile = if ($_.InvocationInfo.PSCommandPath)
        {
            Split-Path -Path $_.InvocationInfo.PSCommandPath -Leaf
        }

        [PSCustomObject] @{
            TimeStamp  = (Get-Date -Format '[MM/dd/yyyy hh:mm:ss]')
            LineNumber = $_.InvocationInfo.ScriptLineNumber
            InFile     = $ErrorInFile
            Message    = $_.Exception.Message
        }
    } | Out-String).Trim() | Sort-Object TimeStamp | Format-Table -Wrap | Add-Content -Path "$env:USERPROFILE\Desktop\$DateStamp-Errors.log" -Force

    if (Test-Path -Path '\\DD2\Logs$' -PathType Container)
    {
        Copy-Item -Path "$env:USERPROFILE\Desktop\*" -Destination '\\DD2\Logs$' -Filter *.log -Container:$false
    }
}

if ($Global:Error)
{
    $Message = (
        $Global:Error | ForEach-Object -Process {
            # Some errors may have the Windows nature and don't have a path to any of the module's files
            $Global:ErrorInFile = if ($_.InvocationInfo.PSCommandPath)
            {
                Split-Path -Path $_.InvocationInfo.PSCommandPath -Leaf
            }

            [PSCustomObject] @{
                TimeStamp  = (Get-Date -Format '[MM/dd/yyyy hh:mm:ss]')
                LineNumber = $_.InvocationInfo.ScriptLineNumber
                InFile     = $ErrorInFile
                Message    = $_.Exception.Message
            }
        }
    )
}

if ( -not (Test-Path -Path "$env:USERPROFILE\Desktop\$DateStamp-Errors.log" -PathType leaf) )
{
    New-Item -Path "$env:USERPROFILE\Desktop\$DateStamp-Errors.log" -ItemType File -Force
}

$Message | Sort-Object TimeStamp | Format-Table -AutoSize -Wrap
Sort-Object TimeStamp | Format-Table -AutoSize -Wrap

$Message | Add-Content -Path "$Env:USERPROFILE\Desktop\$DateStamp-Errors.log" -Force -Encoding UTF8

if (Test-Path -Path '\\DD2\Logs$' -PathType Container)
{
    Copy-Item -Path "\\dd2\Logs$\$DateStamp-Errors.log" -Destination '\\DD2\Logs$' -Filter *.log -Recurse -Container:$false
}

Get-Content -Path 'asd'
Write-LogEntry ("Unable to Copy item from [{0}] because it does not exist any longer" -f $Source.FullName) -Severity Warning

Write-LogEntry -Message ("Error {0}" -f $Error[0].Exception.Message"") -Severity Error

$Error.Clear()

Remove-Module -Name PSLogging -Force
Import-Module -Name PSLogging

Set-Variable -Name sScriptVersion -Value '1.0'
Set-Variable -Name sLogPath -Value '\\dd2\Logs$\Logging'
Set-Variable -Name sLogName -Value "$(Get-Date -Format 'MM-dd-yyyy')-Log.log "
Set-Variable -Name sLogFile -Value (Join-Path -Path $sLogPath -ChildPath $sLogName)

Start-Log -LogPath $sLogPath -LogName $sLogName -ScriptVersion $sScriptVersion

Write-LogInfo -LogPath $sLogFile -Message '<description of what is going on>...' -ToScreen

Try
{
    get-fooboohoo
}
Catch
{
    Write-LogError -LogPath $sLogFile -Message $_.Exception -ExitGracefully -ToScreen
}

If ($?)
{
    Write-LogInfo -LogPath $sLogFile -Message 'Completed Successfully.'
    Write-LogInfo -LogPath $sLogFile -Message ' '
}

$Global:Error.Clear()

Stop-Log -LogPath $sLogFile

Import-Module -Name EZLog
$LogFilePath = "\\dd2\Logs$\Logging-$(Get-Date -Format 'MM-dd-yyyy').log"
$PSDefaultParameterValues = @{
    'Write-EZLog:LogFile'   = $LogFilePath
    'Write-EZLog:Delimiter' = ';'
    'Write-EZLog:ToScreen'  = $true
}

$levelNumber = Get-LevelNumber -Level $PSBoundParameters.Level
$invocationInfo = [Get-PSCallStack]($Script:Logging.CallerScope)

# Split-Path throws an exception if called with a -Path that is null or empty

[string] $fileName = [string]::Empty
if (-not [string]::IsNullOrEmpty($invocationInfo.ScriptName))
{
    $fileName = Split-Path -Path $invocationInfo.ScriptName -Leaf
}
$logMessage = [hashtable] @{
    timestamp    = [datetime]::now
    timestamputc = [datetime]::UtcNow
    level        = Get-LevelName -Level $levelNumber
    levelno      = $levelNumber
    lineno       = $invocationInfo.ScriptLineNumber
    pathname     = $invocationInfo.ScriptName
    filename     = $fileName
    caller       = $invocationInfo.Command
    message      = [string] $Message
    rawmessage   = [string] $Message
    body         = $Body
    execinfo     = $ExceptionInfo
    pid          = $PID
}

if ($PSBoundParameters.ContainsKey('Arguments'))
{
    $logMessage["message"] = [string] $Message -f $Arguments
    $logMessage["args"] = $Arguments
}

Remove-Module -Name Logging -Force
Import-Module -Name Logging

Add-LoggingTarget -Name File -Configuration @{
    Path              = "\\dd2\logs$\%{+%d-%m-%Y}-$env:COMPUTERNAME.log" # <Required> Sets the file destination (eg. 'C:\Temp\%{+%Y%m%d}.log' It supports template's like $Logging.Format)
    PrintBody         = $true # <N\R> Prints body message too
    PrintException    = $true # <N\R> Prints stacktrace
    Append            = $true # <N\R> Append to log file
    Level             = 'DEBUG' # <N\R> Sets the logging level for this target
    Format            = '[%{timestamputc:+%d-%m-%Y %T} %{level}] [InFile: %{filename}] [line: %{lineno}] %{message}' # <N\R> Sets the logging format for this target
    RotateAfterAmount = 7 # <N\R> Sets the amount of files after which rotation is triggered
}

get-fooboohoo

Write-Log -Level 'WARNING' -Message 'Hello, Powershell!'
Write-Log -Level 'WARNING' -Message 'Hello, {0}!' -Arguments 'Powershell'
Write-Log -Level 'WARNING' -Message 'Hello, {0}!' -Arguments 'Powershell' -Body @{source = 'Logging' }

# Write-Log -Level 'ERROR' -Message "$Error"

Write-Log -Level 'INFO' -Message "Test"

$Global:Error.Clear()

New-ScriptLog -LogType Memory -MessagesOnConsole @("Error", "Verbose")
Add-LoggingTarget -Name Console -Configuration @{
    $ColorMapping  = @{
        'DEBUG'   = 'Blue'
        'INFO'    = 'Green'
        'WARNING' = 'Yellow'
        'ERROR'   = 'Red'
    }
    Level          = 'INFO'
    Format         = '[%{filename}] [%{caller}] %{message}'
    PrintException = $true
}

Set-LoggingDefaultLevel -Level 'Verbose'
Set-LoggingDefaultFormat -Format '[%{timestamputc:+%d-%m-%Y %T} %{level}] [InFile: %{filename}] [line: %{lineno}] %{message}'
Add-LoggingTarget -Name File -Configuration @{
    Path              = '\\dd2\logs$\%{+%d-%m-%Y}-Errors.log'
    PrintBody         = $true
    PrintException    = $true
    Append            = $true
    RotateAfterAmount = 7
}

Set-LoggingCallerScope -CallerScope 2
Set-LoggingDefaultLevel -Level 'INFO'
Add-LoggingTarget -Name File -Configuration @{
    Path              = '\\dd2\logs$\%{+%d-%m-%Y}-MosesAutomation.log'
    PrintBody         = $false
    PrintException    = $false
    Append            = $true
    RotateAfterAmount = 7
}
New-ScriptLog -Path "\\dd2\DeploymentShare$\Logs" -BaseName "Verbose" -MessagesOnConsole "Verbose"

Write-Log -Level INFO -Message 'End Moses Automation Setup'

# <https://www.powershellgallery.com/packages/LogTools>

# <https://github.com/jesymoen/PowerShellLogTools>

# To enable logging of Write-Verbose, Write-Warning and Write-Error use the following

# - Create a logfile

# - Add-LogFileHooking

# - Run Initialize-LogOutput to initialize the logfile

# Check to see if the log directory exists, and if not creates it and returns it as a variable

$Global:LogDir = New-LogDirectory -Name "\\dd2\logs$"

# Create the logfile name

$ErrorLogfile = New-LogFileName -LogBaseName ErrorLogfile -LogDir $LogDir -Extension log

# Enable logfile hooking which will redirect the Write-Verbose, Write-Warning, Write-Error, and Write-Debug cmdlets

# Enable-LogFileHooking -LogFile $ErrorLogfile -ThrowExceptionOnWriteError

if ($ErrorLogfile)
{
    # Set a variable to indicate that logging is enabled
    $LoggingEnabled = $true
}
else
{
    # Set a variable to indicate that logging is not enabled
    $LoggingEnabled = $false
}

# Removes the LogFileHooking feature, including associated aliases that hook Write-Verbose, Write-Warning, Write-Debug and Write-Error, and global variables

if ($LoggingEnabled)
{
    # Removes the LogFileHooking feature, including associated aliases that hook Write-Verbose, Write-Warning, Write-Debug and Write-Error, and global variables.
    # Disable-LogFileHooking

    # Deletes old logfiles more than the value of 'RetentionDays' days old.
    Clear-LogFileHistory -Path $LogDir -RetentionDays 7
}
$LogDir = New-LogDirectory -Name "\\dd2\logs$"
$ErrorLogfile = New-LogFileName -LogBaseName ErrorLogfile -LogDir $LogDir -Extension log
Get-Variable -Name MyInvocation | Initialize-LogFile -Logfile $ErrorLogfile
Enable-LogFileHooking -LogFile $ErrorLogfile -ThrowExceptionOnWriteError

Get-ChildItem C:\test.txt

$Global:Error.Clear()
# do
do
{
$Process = Get-Process -Name notepad
if ($Process)
{
Write-Host "Running: $($Process.Name)"
Start-Sleep -Milliseconds 500
}
}
until (-not ($Process))
do
{
$Prompt = Read-Host -Prompt " "
if ([string]::IsNullOrEmpty($Prompt))
{
break
}
else
{
switch ($Prompt)
{
"Y" {}
"N" {}
Default {}
}
}
}
while ($Prompt -ne "N")
# while
while ($true)
{
$Process = Get-Process -Name notepad
if ($Process)
{
Write-Host "Running: $($Process.Name)"
Start-Sleep -Milliseconds 500
}
}
$Process = Get-Process -Name notepad
while
(
$($Process.Refresh()
$Process.ProcessName)
)
{
Write-Host "Running: $($Process.Name)"
Start-Sleep -Milliseconds 500
}
while ($true)
{
if ((Get-Process -Name notepad++ -ErrorAction Ignore) -and (-not (Get-Process -Name notepad -ErrorAction Ignore)))
{
# if notepad++ is running, and notepad is not, then run notepad.exe
Start-Process -FilePath "$env:SystemRoot\system32\notepad.exe"
}
elseif ((-not (Get-Process -Name notepad++ -ErrorAction Ignore)) -and (Get-Process -Name notepad -ErrorAction Ignore))
{
# notepad++ isn't runn, and notepad.exe is, then kill notepad.exe
Stop-Process -Name notepad -Force -ErrorAction Ignore
}
Start-Sleep -Milliseconds 500
}
# Method 1: Measure-Command Cmdlet
$elapsedTime = Measure-Command {
Start-Sleep -Seconds 5 # Example script block with a delay
Get-Process # Another example command
}
Write-Host "Measure-Command: Total time elapsed: $($elapsedTime.TotalMinutes) minutes"
# Method 2: Stopwatch Class
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Start-Sleep -Seconds 5 # Example script block with a delay
$stopwatch.Stop()
Write-Host "Stopwatch: Elapsed time: $($stopwatch.Elapsed.TotalSeconds) seconds"
# Method 3: Date Variables
$start = Get-Date
Start-Sleep -Seconds 5 # Example script block with a delay
$end = Get-Date
$elapsed = $end - $start
Write-Host "Date Variables: Elapsed time: $($elapsed.TotalSeconds) seconds"
# Method 5: Logging (Timestamps)
function Log-TimeStamp
{
$timestamp = Get-Date -Format "mm:ss"
Write-Host "[$timestamp] Script execution started."
# "[$timestamp] Script execution started." | Out-File "script_log.txt" -Append
Start-Sleep -Seconds 5 # Example script block with a delay
Write-Host "[$timestamp] Script execution completed."
}
Log-TimeStamp
# Note: Method 4 (Performance Counters) isn't included here as it's more advanced and specific to monitoring system metrics.
# Additional Notes:
# - Method 1 (Measure-Command) directly measures the script block.
# - Method 2 (Stopwatch) manually starts and stops timing.
# - Method 3 (Date Variables) calculates elapsed time using date/time variables.
# - Method 5 (Logging) logs timestamps to a file before and after script execution.
# Feel free to copy and use these examples as needed in your PowerShell scripts.

Powershell MessageBox Samples

[void][reflection.assembly]::loadwithpartialname('System.Windows.Forms')
[void][reflection.assembly]::loadwithpartialname('System.Drawing')
$notify = New-Object system.windows.forms.notifyicon
$notify.icon = [System.Drawing.SystemIcons]::Information
$notify.visible = $true
$notify.showballoontip(10000, 'Header Title', 'Hey look at me!!', [system.windows.forms.tooltipicon]::Info)

[System.Windows.MessageBox]::Show('Message', 'Title', [System.Windows.MessageBoxButton]::YesNoCancel, [System.Windows.MessageBoxImage]::Question)

$TitleBar = Split-Path -Path $PSCommandPath -Leaf

$Method1 = 'System.Windows.Forms.MessageBox'
$Method2 = 'Microsoft.VisualBasic.Interaction.MsgBox()'
$Method3 = 'WScript.Shell.Popup()'

$MessageBase = "This message box is brought to you by:`n{0}`nHere are some Unicode characters:`n😀 ā ی ± ≠ 👩"
$Message1 = $MessageBase -f $Method1
$Message2 = $MessageBase -f $Method2
$Message3 = $MessageBase -f $Method3

# Load System.Windows.Forms and enable visual styles
Write-Output 'Loading System.Windows.Forms'
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')

# Enable visual styles
# This command is required on Windows XP and later (yes, even on Windows 10)
# Without this, your message boxes look like something form Windows 95
Write-Output 'Enabling visual styles'
[System.Windows.Forms.Application]::EnableVisualStyles()

<# METHOD 1:

    System.Windows.Forms.MessageBox
    https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.messagebox

    Comforms with visual styles

    To get the details of parameter types:
        [enum]::GetValues([System.Windows.Forms.MessageBoxButtons])
        [enum]::GetValues([System.Windows.Forms.MessageBoxIcon])
    To get the details of the return type:
        [enum]::GetValues([System.Windows.Forms.DialogResult])
#>

Write-Output "Demonstrating method 1: $Method1"
$result = [System.Windows.Forms.MessageBox]::Show($'Testing', $TitleBar, 'OKCancel')
Write-Output $result


Add-Type -AssemblyName System.Windows.Forms

$MessageBoxIcon = [System.Windows.Forms.MessageBoxIcon]::Warning
$MessageBoxButtons = [System.Windows.Forms.MessageBoxButtons]::YesNo

$Result = [System.Windows.Forms.MessageBox]::Show('', 'Warning', $MessageBoxButtons, $MessageBoxIcon)

<# METHOD 2:
    Microsoft.VisualBasic.Interaction.MsgBox()
    https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualbasic.interaction.msgbox

    Comforms with visual styles

    To get the details of parameter type:
        [enum]::GetValues([Microsoft.VisualBasic.MsgBoxStyle])
    To get the details of the return type:
        [enum]::GetValues([Microsoft.VisualBasic.MsgBoxResult])
#>

Write-Output "Demonstrating method 2: $Method2"
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
$result = [Microsoft.VisualBasic.Interaction]::MsgBox($Message2, 'OKOnly,Information', $TitleBar)
Write-Output $result

<# METHOD 3 (DEPRECATED):

    WScript.Shell.Popup()
    https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/x83z1d9f(v=vs.84)

    Not affected by visual styles
    (Looks like something from Windows 95!)
#>
Write-Output "Demonstrating method 3: $Method3"
$MsgBox = New-Object -ComObject wscript.shell
$result = $MsgBox.Popup($Message3, 0, $TitleBar, 0 -bor 64)
Write-Output $result

System.Math

self-delete the script without asking for confirmation:

Remove-Item -Path $MyInvocation.MyCommand.Path -Force
<#
The Math class contains various basic math operations
You can execute methods like round a value
Another operation is square root
It even contains fields like Pi
#>

System.TimeZoneInfo
# The TimeZoneInfo class contains information about time zones on the system. You can use it to do things like look up time zones
[TimeZoneInfo]::GetSystemTimeZones()

# You can also convert a DateTime object to another time zone
$Date = Get-Date
$TimeZoneInfo = [TimeZoneInfo]::GetSystemTimeZones() | Where-Object Id -Eq 'Chatham Islands Standard Time'

System.UriBuilder
# The UriBuilder class is used to compose URIs without string concatenation. It ensures that you can produce valid URIs without having to worry about back slashes and encoding

$Builder = [UriBuilder]::new()
$Builder.Host = "www.google.com"
$Builder.Scheme = "https"
$Builder.Port = 443
$Builder.UserName = "adam"
$Builder.Password = "SuperSecret"
$Builder.Uri.AbsoluteUri

System.Collections.ArrayList
# The ArrayList class is actually used within the PowerShell engine a little bit and you'll see it show up occasionally. It's a useful class for quickly adding items to an array rather than the += operator

# If you're collecting many thousands of items, you may want to use an array list instead of an array
$ArrayList = [System.Collections.ArrayList]::new()
1..1000 | ForEach-Object { $ArrayList.Add($_) } | Out-Null
$ArrayList

System.ComponentModel.Win32Exception
# The Win32Exception is a good utility class for deducing what a Win32 error may mean. Try casting an exception code to the exception class to see the description of the code
[System.ComponentModel.Win32Exception]0x00005

System.IO.FileSystemWatcher
# The FileSystemWatcher class can be used to watch for changes to the file system. You can define filters for types of changes and also file extensions

# This example creates a file system watcher that watches the desktop for text files being created
$FileSystemWatcher = [System.IO.FileSystemWatcher]::new("C:\users\adamr\desktop", "*.txt")
$FileSystemWatcher.IncludeSubDirectories = $false

Register-ObjectEvent $FileSystemWatcher Created -SourceIdentifier FileCreated -Action {
    $Name = $Event.SourceEventArgs.Name
    $Type = $Event.SourceEventArgs.ChangeType
    Write-Host "'$Name' = $Type"
}

System.IO.Path
# The Path class is handy for inspecting, creating and dealing with cross-platform paths

# Unlike Join-Path, the Path class can including more than two paths when joining them

# It's also useful because it works cross-platform and will create paths that work on Unix and Windows systems

# It also helps when trying to determine whether a path is relative or absolute

System.IO.StreamReader
# Many types in .NET will return a stream. Streams are typically used when reading large data sets or data that isn't all available at once. The StreamReader class is helpful for translating these streams into strings. This is particularly useful when reading web request bodies in Windows PowerShell

try
{
    Invoke-RestMethod <http://localhost:5000/throws>
}
catch
{
    [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()).ReadToEnd()
}

System.Net.Dns
# The Dns class is useful for invoking the DNS client from .NET. You can use it to resolve IP Address and host names

System.Net.IPAddress
# The IPAddress class is useful for dealing with IPv4 and IPv6 addresses. You can use it to parse and inspect addresses

$Address = [System.Net.IpAddress]::Parse("127.0.0.1")
$Address.AddressFamily

System.Reflection.Assembly
# The Assembly class can be used to load assemblies from the file system or even from base64 strings encoded in the PowerShell script

System.Runtime.InteropServices.RuntimeInformation
# Since PowerShell is now cross-platform, it's useful to understand the platform that the script is running within. There are some variables available like $IsLinux and $IsWindows to determine this information but this class provides even more info

System.Security.SecureString
# The SecureString class is commonly used with PSCredential. It's not considered secure, especially on Unix systems, but it's still used quite often

# SecureString has a bit of a strange API and you need to append characters to it to produce a new one
$SS = [System.Security.SecureString]::new()
"Hello!".ToCharArray() | ForEach-Object { $SS.AppendChar($_) }

System.Text.Encoding
# The Encoding class is useful when dealing with conversion between the Roman alphabet and other alphabets. It’s also great for emojis

# You can also decode byte arrays to text
$Bytes = [System.Text.Encoding]::Ascii.GetBytes("Hello!")
[System.Text.Encoding]::Unicode.GetString($Bytes)

System.Text.StringBuilder
# The StringBuilder class can be used to create dynamic strings without much overhead. Strings in .NET are immutable which means that lot's of resources are required to perform string operations

# You can use the methods of the this class to quickly perform operations like concatenations

# See the below performance differences between standard string concatenation and string concatenation with StringBuilder

Measure-Command {
    $Str = ""
    for($i = 0; $i -lt 10000000; $i++)
    {
        $Str += "Str-{0}" -f $_
    }
    $Str
}

# Did not return after 30 minutes

Measure-Command {
    $SB = [System.Text.StringBuilder]::new()
    for($i = 0; $i -lt 10000000; $i++)
    {
        $SB.AppendFormat("Str-{0}", $i)
    }
    $SB.ToString()
}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 9
Milliseconds      : 960
Ticks             : 99602440
TotalDays         : 0.000115280601851852
TotalHours        : 0.00276673444444444
TotalMinutes      : 0.166004066666667
TotalSeconds      : 9.960244
TotalMilliseconds : 9960.244

# While basic concatenation will show about a 20% improvement with StringBuilder, if you use something like the script above, where formatting is involved, you'll notice an immense improvement

Recipe 2.1 - New ways to do old things

Run on SRV1

1. Ipconfig vs new cmdlets

Two variations on the old way

ipconfig.exe
ipconfig.exe /all

The new Way

Get-NetIPConfiguration

Related cmdlets - but not for the book

Get-NetIPInterface
Get-NetAdapter

2. Pinging a computer

The old way

Ping DC1.Reskit.Org -4

The New way

Test-NetConnection DC1.Reskit.Org

And some new things Ping does not do

Test-NetConnection DC1.Reskit.Org -CommonTCPPort SMB
$ILHT = @{InformationLevel = 'Detailed'}
Test-NetConnection DC1.Reskit.Org -port 389 @ILHT

3. Using Sharing folder from DC1

The old way to use a shared folder

net use X:  \\DC1.Reskit.Org\c$

The new way using an SMB cmdlet

New-SMBMapping -LocalPath 'Y:' -RemotePath \\DC1.Reskit.Org\c$

See what is shared the old way

net use

And the new way

Get-SMBMapping

4. - Sharing a folder from SRV1

Now share the old way

net share Windows=C:\windows

and the new way

New-SmbShare -Path C:\Windows -Name Windows2

And see what has been shared the old way

net share

and the new way

Get-SmbShare

5. Getting DNS Cache

The Old way to see the DNS Client Cache

ipconfig /displaydns

Vs

Get-DnsClientCache

6. Clear the dnsclient client cache the old way

Ipconfig /flushdns

Vs the new way

Clear-DnsClientCache

7. DNS Lookups

Nslookup DC1.Reskit.Org
Resolve-DnsName -Name DC1.Reskit.Org  -Type ALL

Get-SmbMapping x: | Remove-SmbMapping -force
Get-SmbMapping y: | Remove-SmbMapping -confirm:$false
Get-SMBSHARE Windows* | Remove-SMBShare

Padding and Parameters

My logging function hat a $Type parameter with different types of logging: Info, Success, Error, Warning.. The output in the console looked like this:

20.03.2023|04:00:33|Error|Test message [Error]
20.03.2023|04:00:33|Warning|Test message [Warning]
20.03.2023|04:00:33|Info|Test message [Info]
20.03.2023|04:00:33|Success|Test message [Success]

As you can see the selected type parameter was not beautiful aligned by the dividing dash/pipe to the message, which makes reading a bit harder in my opinion. So I decided to work with padding. Padding in a string as Method can add one or multiple extra characters to the left or the right of the string, like:

$Samplestrings = ("1","13","156")
$Samplestrings.foreach({
    $_.PadLeft(3,"0")
})

The output looks like this:

001
013
156

back to the starting topic my function looked a way like this:

function Write-Log {
    [CmdletBinding()]
    param (
        [string] $Message,
        [Validateset("Warning","Info","Error","Success")]
        $Type
    )
    
    begin {
        
    }
    
    process {
        Write-Host "$(Get-Date -format 'dd.MM.yyyy')|$(Get-Date -format 'hh:mm:ss')|$Type|$Message"
    }
    
    end {
        
    }
}

And if we now would align the divider properly, we have to find out what is the length of the longest string in the $type parameter to add dynamically whitespaces via padding. My attempt looks like this:

function Write-Log {
    [CmdletBinding()]
    param (
        [string] $Message,
        [Validateset("Warning","Info","Error","Success")]
        $Type
    )
    
    begin {
        $MaxLengthType = 0
        (Get-Variable -Name "Type").Attributes.ValidValues.foreach({
            if($MaxLengthType -lt $_.tostring().length){
                $MaxLengthType = [int]$_.tostring().length
            }
        })
        $TypeToDisplay = $Type.PadRight($MaxLengthType," ")
    }
    
    process {
        Write-Host "$(Get-Date -format 'dd.MM.yyyy')|$(Get-Date -format 'hh:mm:ss')|$TypeToDisplay|$Message"
    }
    
    end {
        
    }
}

Our console now looks like this:

20.03.2023|04:00:33|Error  |Test message [Error]
20.03.2023|04:00:33|Warning|Test message [Warning]
20.03.2023|04:00:33|Info   |Test message [Info]
20.03.2023|04:00:33|Success|Test message [Success]

It is a way more static and increases the readability a lot

https://devdojo.com/hcritter/padding-and-parameters

Yes, the `powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE` command specifically queries the power settings for the display idle timeout. This setting determines how long the display will remain on when the system is idle before it turns off.
### Querying Other Power Settings
If you want to query different power settings, you need to use different subcategories and settings. Here are a few examples:
- **Display Timeout**:
```powershell
powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE
```
- **Sleep Timeout**:
```powershell
powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE
```
- **Hard Disk Timeout**:
```powershell
powercfg /query SCHEME_CURRENT SUB_DISK DISKIDLE
```
- **Processor Power Management**:
```powershell
powercfg /query SCHEME_CURRENT SUB_PROCESSOR PROCTHROTTLEMAX
```
### Example: Querying Sleep Timeout
Here is an example of how to query and convert the sleep timeout values similarly to how you did with the display timeout:
```powershell
function Get-SleepTimeout {
[CmdletBinding()]
param ()
# Query the active power scheme settings for AC and DC sleep timeouts
$ACSleepTimeout = powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE | Select-String -Pattern 'Power Setting Index' | ForEach-Object { $_.Line -split '\s+' } | Select-Object -Last 1
$DCSleepTimeout = powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE | Select-String -Pattern 'Power Setting Index' | ForEach-Object { $_.Line -split '\s+' } | Select-Object -First 1
# Convert extracted values to integers
$ACSleepTimeout = [int]$ACSleepTimeout
$DCSleepTimeout = [int]$DCSleepTimeout
# Create a PSCustomObject with the extracted timeouts
$TimeoutObject = [PSCustomObject]@{
ACSleepTimeout = $ACSleepTimeout
DCSleepTimeout = $DCSleepTimeout
}
return $TimeoutObject
}
# Example usage
$SleepTimeout = Get-SleepTimeout
Write-Output "ACSleepTimeout: $($SleepTimeout.ACSleepTimeout)"
Write-Output "DCSleepTimeout: $($SleepTimeout.DCSleepTimeout)"
```
This script defines a function `Get-SleepTimeout` that queries the sleep timeouts for both AC and DC power states, converts the values to integers, and returns them as a custom object. You can modify this approach for other power settings by changing the `SUB_*` and setting names in the `powercfg` query command.

the powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE command specifically queries the power settings for the display idle timeout. This setting determines how long the display will remain on when the system is idle before it turns off

Querying Other Power Settings

If you want to query different power settings, you need to use different subcategories and settings. Here are a few examples

  • Display Timeout:

    powercfg /query SCHEME_CURRENT SUB_VIDEO VIDEOIDLE
  • Sleep Timeout:

    powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE
  • Hard Disk Timeout:

    powercfg /query SCHEME_CURRENT SUB_DISK DISKIDLE
  • Processor Power Management:

    powercfg /query SCHEME_CURRENT SUB_PROCESSOR PROCTHROTTLEMAX

Example: Querying Sleep Timeout

Here is an example of how to query and convert the sleep timeout values similarly to how you did with the display timeout:

function Get-SleepTimeout {
    [CmdletBinding()]
    param ()

    # Query the active power scheme settings for AC and DC sleep timeouts
    $ACSleepTimeout = powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE | Select-String -Pattern 'Power Setting Index' | ForEach-Object { $_.Line -split '\s+' } | Select-Object -Last 1
    $DCSleepTimeout = powercfg /query SCHEME_CURRENT SUB_SLEEP STANDBYIDLE | Select-String -Pattern 'Power Setting Index' | ForEach-Object { $_.Line -split '\s+' } | Select-Object -First 1

    # Convert extracted values to integers
    $ACSleepTimeout = [int]$ACSleepTimeout
    $DCSleepTimeout = [int]$DCSleepTimeout

    # Create a PSCustomObject with the extracted timeouts
    $TimeoutObject = [PSCustomObject]@{
        ACSleepTimeout = $ACSleepTimeout
        DCSleepTimeout = $DCSleepTimeout
    }

    return $TimeoutObject
}

# Example usage
$SleepTimeout = Get-SleepTimeout
Write-Output "ACSleepTimeout: $($SleepTimeout.ACSleepTimeout)"
Write-Output "DCSleepTimeout: $($SleepTimeout.DCSleepTimeout)"

This script defines a function Get-SleepTimeout that queries the sleep timeouts for both AC and DC power states, converts the values to integers, and returns them as a custom object. You can modify this approach for other power settings by changing the SUB_* and setting names in the powercfg query command.

PowerShell is fun :) PowerShell command-line tips

  1. Count the number of items found.
  2. Finding the cmdlet that you need.
  3. Display your command-line history.
  4. Searching your command-line history.
  5. Keep running a command until pressing CTRL-C/Break.
  6. Use Out-GridView to display and filter results.

Count the number of items found

When searching for items, users, or folders, for example, you sometimes just need to count them. You can copy/paste the results in notepad and see how many lines but you can also use this

C:\Users\HarmV> (Get-ChildItem -Path c:\temp -Filter *.exe -Recurse).count
39

You can wrap your command with parentheses and use the ‘(‘ and ‘)’ signs around it and add ".count" behind it. This way the output will not be shown on screen but it will just show you the amount found

Finding the cmdlet that you need

Sometimes you want to search for a cmdlet, but you're not sure if it exists or how it's formatted, you can search for it using

C:\Users\HarmV> Get-Help *json*
Name Category Module Synopsis
---- -------- ------ --------
ConvertFrom-Json Cmdlet Microsoft.PowerShell.Uti… …
ConvertTo-Json Cmdlet Microsoft.PowerShell.Uti… …
Test-Json Cmdlet Microsoft.PowerShell.Uti… …
ConvertTo-AutopilotConfiguration… Function WindowsAutoPilotIntune …
Write-M365DocJson Function M365Documentation …
Update-M365DSCExchangeResourcesS… Function Microsoft365DSC …
New-M365DSCConfigurationToJSON Function Microsoft365DSC …
Update-M365DSCSharePointResource… Function Microsoft365DSC …
Update-M365DSCResourcesSettingsJ… Function Microsoft365DSC …
C:\Users\HarmV> Get-Help *json* Name Category Module Synopsis ---- -------- ------ -------- ConvertFrom-Json Cmdlet Microsoft.PowerShell.Uti… … ConvertTo-Json Cmdlet Microsoft.PowerShell.Uti… … Test-Json Cmdlet Microsoft.PowerShell.Uti… … ConvertTo-AutopilotConfiguration… Function WindowsAutoPilotIntune … Write-M365DocJson Function M365Documentation … Update-M365DSCExchangeResourcesS… Function Microsoft365DSC … New-M365DSCConfigurationToJSON Function Microsoft365DSC … Update-M365DSCSharePointResource… Function Microsoft365DSC … Update-M365DSCResourcesSettingsJ… Function Microsoft365DSC …
C:\Users\HarmV> Get-Help *json*

Name                              Category  Module                    Synopsis
----                              --------  ------                    --------
ConvertFrom-Json                  Cmdlet    Microsoft.PowerShell.Uti… …
ConvertTo-Json                    Cmdlet    Microsoft.PowerShell.Uti… …
Test-Json                         Cmdlet    Microsoft.PowerShell.Uti… …
ConvertTo-AutopilotConfiguration… Function  WindowsAutoPilotIntune    …
Write-M365DocJson                 Function  M365Documentation         …
Update-M365DSCExchangeResourcesS… Function  Microsoft365DSC           …
New-M365DSCConfigurationToJSON    Function  Microsoft365DSC           …
Update-M365DSCSharePointResource… Function  Microsoft365DSC           …
Update-M365DSCResourcesSettingsJ… Function  Microsoft365DSC           …

By using the Get-Help cmdlet you can search using wildcard through all your installed modules, it will show you if it's a cmdlet or a function and in which module it's present

Display your command-line history

You can browse through your previous commands using the up-and-down arrow, but you can also show it using:

C:\Users\HarmV> get-history
1 2.910 Get-ChildItem -Path c:\temp -Filter *.exe -Recurse
2 0.172 (Get-ChildItem -Path c:\temp -Filter*.exe -Recurse).count
C:\Users\HarmV> get-history Id Duration CommandLine -- -------- ----------- 1 2.910 Get-ChildItem -Path c:\temp -Filter *.exe -Recurse 2 0.172 (Get-ChildItem -Path c:\temp -Filter*.exe -Recurse).count 3 0.036 Get-History 4 3.396 help *history* 5 1.116 help *json* 6 1.190 Get-Help *json*
C:\Users\HarmV> get-history

  Id     Duration CommandLine
  --     -------- -----------
   1        2.910 Get-ChildItem -Path c:\temp -Filter *.exe -Recurse
   2        0.172 (Get-ChildItem -Path c:\temp -Filter*.exe -Recurse).count
   3        0.036 Get-History
   4        3.396 help *history*
   5        1.116 help *json*
   6        1.190 Get-Help *json*

But that's just the history from your current session, the complete command-line history is saved in your profile in a text file. You can retrieve the path by using:

C:\Users\HarmV> Get-PSReadLineOption | Select-Object HistorySavePath
C:\Users\HarmV\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
C:\Users\HarmV> Get-PSReadLineOption | Select-Object HistorySavePath HistorySavePath --------------- C:\Users\HarmV\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
C:\Users\HarmV> Get-PSReadLineOption | Select-Object HistorySavePath

HistorySavePath
---------------

C:\Users\HarmV\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt

In this text file, you will find the complete history of every command you entered, my file is 14921 lines long

Searching your command-line history In the chapter above I showed you how to retrieve your history. If you want to search your history without opening the text file, you can use CTRL+R and start typing a part of the command that you are searching for:

C:\Users\HarmV> foreach ($line in $csv){
>> $groupname = $line.GroupName
>> $objectid = (Get-AzureADGroup | Where-Object {$_.DisplayName -eq $groupname}).ObjectId
>> Get-AzureADGroupMember -ObjectId $objectid | select DisplayName,UserPrincipalName | Export-Csv -Path "C:\Temp\Groups\testmembers.csv" -NoTypeInformation -Append
>> }
bck-i-search: get-azureadgroupm_

I started typing “Get-Azureadgroupm” and it retrieve it from my command-line history and it highlights the part found.

Keep running a command until pressing CTRL-C/Break

I'm a bit impatient sometimes and want to check a status a lot but don’t want to repeat the same command the whole time, you can use a while $true loop to keep on executing a command until you stop it by typing CTRL-C. For example:

while ($true) {
Clear-Host
Get-Date
Get-MoveRequest | Where-Object {$_.Status -ne "Suspended" -and $_.Status -ne "Failed"}
Start-Sleep -Seconds 600
}

In this example, the screen is cleared, and the date is shown so that you know from what timestamp the Get-MoveRequest (Exchange migration cmdlet) is. It will wait 10 minutes and keep on repeating the steps until you type CTRL-C to break it. You can use any command between the curly brackets and interval time you want.

Use Out-GridView to display and filter results

If PowerShell_ISE is installed on your system you can use the Out-GridView cmdlet to output results in a pop-up window with a filter field. You can do this by running:

Get-ChildItem -Path c:\temp -Recurse | Out-GridView

This will output all files in c:\temp and you can then filter the results by typing a few letters of the word you want to search for.

PowerShell and variables

Every script I write has variables in it, but there are different types of variables. This short blog post will show a few types you can use in your scripts.

What are variables?

You can store all types of values in PowerShell variables. For example, store the results of commands and elements used in commands and expressions, such as names, paths, settings, and values.

A variable is a unit of memory in which values are stored. In PowerShell, variables are represented by text strings that begin with a dollar sign ($), such as $a, $process, or $my_var.

Variable names aren't case-sensitive and can include spaces and special characters.

Array

This is something that I use a lot. An array is a list of items that you can use, for example:

$files=Get-ChildItem -Path d:\temp -Filter *.ps1
$files=Get-ChildItem -Path d:\temp -Filter*.ps1
$files=Get-ChildItem -Path d:\temp -Filter *.ps1
This will search for all*.ps1 files in d:\temp and store the found items in the $files array. You can check what type the variable is by adding ".GetType()" behind it. This looks like this:

The BaseType tells you that it's an Array, and the contents of the Array can be listed by running $files in our example:

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 29-8-2022 22:24 1481 ﲵ 365healthstatus.ps1
-a--- 25-2-2022 14:30 261 ﲵ Activation.ps1
-a--- 14-7-2022 12:26 4630 ﲵ AdminGroupChangeReport.ps1
-a--- 16-6-2022 13:28 746 ﲵ AdminGroups.ps1
-a--- 26-8-2021 12:49 5455 ﲵ AdminReport.ps1
-a--- 27-10-2021 10:08 14672 ﲵ AppleDEPProfile_Assign.ps1
-a--- 2-11-2021 15:24 9569 ﲵ applevpp_sync.ps1
-a--- 15-10-2020 14:19 2523 ﲵ BIOS_Settings_For_HP.ps1
-a--- 20-1-2022 15:47 142 ﲵ bitlocker.ps1
-a--- 20-1-2022 13:49 64 ﲵ bitlockerremediate.ps1
-a--- 20-1-2022 13:49 192 ﲵ bitlockertest.ps1
-a--- 22-5-2022 21:10 488 ﲵ calendar_events.ps1
C:\Users\HarmV> $files Directory: D:\Temp Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 29-8-2022 22:24 1481 ﲵ 365healthstatus.ps1 -a--- 25-2-2022 14:30 261 ﲵ Activation.ps1 -a--- 14-7-2022 12:26 4630 ﲵ AdminGroupChangeReport.ps1 -a--- 16-6-2022 13:28 746 ﲵ AdminGroups.ps1 -a--- 26-8-2021 12:49 5455 ﲵ AdminReport.ps1 -a--- 27-10-2021 10:08 14672 ﲵ AppleDEPProfile_Assign.ps1 -a--- 2-11-2021 15:24 9569 ﲵ applevpp_sync.ps1 -a--- 15-10-2020 14:19 2523 ﲵ BIOS_Settings_For_HP.ps1 -a--- 20-1-2022 15:47 142 ﲵ bitlocker.ps1 -a--- 20-1-2022 13:49 64 ﲵ bitlockerremediate.ps1 -a--- 20-1-2022 13:49 192 ﲵ bitlockertest.ps1 -a--- 22-5-2022 21:10 488 ﲵ calendar_events.ps1 ...
C:\Users\HarmV> $files

        Directory: D:\Temp

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         29-8-2022     22:24           1481 ﲵ  365healthstatus.ps1
-a---         25-2-2022     14:30            261 ﲵ  Activation.ps1
-a---         14-7-2022     12:26           4630 ﲵ  AdminGroupChangeReport.ps1
-a---         16-6-2022     13:28            746 ﲵ  AdminGroups.ps1
-a---         26-8-2021     12:49           5455 ﲵ  AdminReport.ps1
-a---        27-10-2021     10:08          14672 ﲵ  AppleDEPProfile_Assign.ps1
-a---         2-11-2021     15:24           9569 ﲵ  applevpp_sync.ps1
-a---        15-10-2020     14:19           2523 ﲵ  BIOS_Settings_For_HP.ps1
-a---         20-1-2022     15:47            142 ﲵ  bitlocker.ps1
-a---         20-1-2022     13:49             64 ﲵ  bitlockerremediate.ps1
-a---         20-1-2022     13:49            192 ﲵ  bitlockertest.ps1
-a---         22-5-2022     21:10            488 ﲵ  calendar_events.ps1
...

You can use the array in a ForEach loop to do something to each individual item in it, for example:

C:\Users\HarmV> foreach ($file in $files) {Write-Host $file.FullName}
D:\Temp\365healthstatus.ps1
D:\Temp\AdminGroupChangeReport.ps1
D:\Temp\AppleDEPProfile_Assign.ps1
D:\Temp\applevpp_sync.ps1
D:\Temp\BIOS_Settings_For_HP.ps1
D:\Temp\bitlockerremediate.ps1
D:\Temp\bitlockertest.ps1
D:\Temp\calendar_events.ps1
C:\Users\HarmV> foreach ($file in $files) {Write-Host $file.FullName} D:\Temp\365healthstatus.ps1 D:\Temp\Activation.ps1 D:\Temp\AdminGroupChangeReport.ps1 D:\Temp\AdminGroups.ps1 D:\Temp\AdminReport.ps1 D:\Temp\AppleDEPProfile_Assign.ps1 D:\Temp\applevpp_sync.ps1 D:\Temp\BIOS_Settings_For_HP.ps1 D:\Temp\bitlocker.ps1 D:\Temp\bitlockerremediate.ps1 D:\Temp\bitlockertest.ps1 D:\Temp\calendar_events.ps1
C:\Users\HarmV> foreach ($file in $files) {Write-Host $file.FullName}
D:\Temp\365healthstatus.ps1
D:\Temp\Activation.ps1
D:\Temp\AdminGroupChangeReport.ps1
D:\Temp\AdminGroups.ps1
D:\Temp\AdminReport.ps1
D:\Temp\AppleDEPProfile_Assign.ps1
D:\Temp\applevpp_sync.ps1
D:\Temp\BIOS_Settings_For_HP.ps1
D:\Temp\bitlocker.ps1
D:\Temp\bitlockerremediate.ps1
D:\Temp\bitlockertest.ps1
D:\Temp\calendar_events.ps1

If you want to select a specific item from the array, for example, the second one, you can use: (It starts counting at zero, so the second item is one )

C:\Users\HarmV> $files[1]
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 25-2-2022 14:30 261 ﲵ Activation.ps1
C:\Users\HarmV> $files[1] Directory: D:\Temp Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 25-2-2022 14:30 261 ﲵ Activation.ps1
C:\Users\HarmV> $files[1]

        Directory: D:\Temp

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         25-2-2022     14:30            261 ﲵ  Activation.ps1
You can create an array with your own items by running this:

$array=@( "Item 1" "Item 2" "Item 3" )
$array=@(
 "Item 1"
 "Item 2"
 "Item 3"
)

You can query/view it like this:

C:\Users\HarmV> $array.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
C:\Users\HarmV> $array[1]
C:\Users\HarmV> $array.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array C:\Users\HarmV> $array Item 1 Item 2 Item 3 C:\Users\HarmV> $array[1] Item 2
C:\Users\HarmV> $array.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

C:\Users\HarmV> $array
Item 1
Item 2
Item 3
C:\Users\HarmV> $array[1]
Item 2

Environment variables

These are always available and will contain system or user-specific settings. Examples are: (Type $env: followed by Tab)

__COMPAT_LAYER NUMBER_OF_PROCESSORS ProgramFiles(x86)
ALLUSERSPROFILE OneDrive ProgramW6432
APPDATA OneDriveCommercial PSModulePath
ChocolateyInstall OneDriveConsumer PUBLIC
ChocolateyLastPathUpdate OS SystemDrive
COLUMNS PARSER_FILES_PATH SystemRoot
CommonProgramFiles Path TEMP
CommonProgramFiles(x86) PATHEXT TMP
CommonProgramW6432 POSH_THEMES_PATH USERDOMAIN
COMPUTERNAME POWERSHELL_DISTRIBUTION_CHANNEL USERDOMAIN_ROAMINGPROFILE
ComSpec PROCESSOR_ARCHITECTURE USERNAME
DriverData PROCESSOR_IDENTIFIER USERPROFILE
HOMEDRIVE PROCESSOR_LEVEL windir
HOMEPATH PROCESSOR_REVISION WSLENV
LOCALAPPDATA ProgramData WT_PROFILE_ID
LOGONSERVER ProgramFiles WT_SESSION
C:\Users\HarmV> $env:__COMPAT_LAYER NUMBER_OF_PROCESSORS ProgramFiles(x86) ALLUSERSPROFILE OneDrive ProgramW6432 APPDATA OneDriveCommercial PSModulePath ChocolateyInstall OneDriveConsumer PUBLIC ChocolateyLastPathUpdate OS SystemDrive COLUMNS PARSER_FILES_PATH SystemRoot CommonProgramFiles Path TEMP CommonProgramFiles(x86) PATHEXT TMP CommonProgramW6432 POSH_THEMES_PATH USERDOMAIN COMPUTERNAME POWERSHELL_DISTRIBUTION_CHANNEL USERDOMAIN_ROAMINGPROFILE ComSpec PROCESSOR_ARCHITECTURE USERNAME DriverData PROCESSOR_IDENTIFIER USERPROFILE HOMEDRIVE PROCESSOR_LEVEL windir HOMEPATH PROCESSOR_REVISION WSLENV LOCALAPPDATA ProgramData WT_PROFILE_ID LOGONSERVER ProgramFiles WT_SESSION
C:\Users\HarmV> $env:
__COMPAT_LAYER                   NUMBER_OF_PROCESSORS             ProgramFiles(x86)
ALLUSERSPROFILE                  OneDrive                         ProgramW6432
APPDATA                          OneDriveCommercial               PSModulePath
ChocolateyInstall                OneDriveConsumer                 PUBLIC
ChocolateyLastPathUpdate         OS                               SystemDrive
COLUMNS                          PARSER_FILES_PATH                SystemRoot
CommonProgramFiles               Path                             TEMP
CommonProgramFiles(x86)          PATHEXT                          TMP
CommonProgramW6432               POSH_THEMES_PATH                 USERDOMAIN
COMPUTERNAME                     POWERSHELL_DISTRIBUTION_CHANNEL  USERDOMAIN_ROAMINGPROFILE
ComSpec                          PROCESSOR_ARCHITECTURE           USERNAME
DriverData                       PROCESSOR_IDENTIFIER             USERPROFILE
HOMEDRIVE                        PROCESSOR_LEVEL                  windir
HOMEPATH                         PROCESSOR_REVISION               WSLENV
LOCALAPPDATA                     ProgramData                      WT_PROFILE_ID
LOGONSERVER                      ProgramFiles                     WT_SESSION

You can use this as an example to show the location of your personal or work OneDrive folders:

C:\Users\HarmV> $env:OneDriveCommercial
C:\Users\HarmV\OneDrive - NEXXT
C:\Users\HarmV> $env:OneDriveConsumer
C:\Users\HarmV> $env:OneDriveCommercial C:\Users\HarmV\OneDrive - NEXXT C:\Users\HarmV> $env:OneDriveConsumer C:\Users\HarmV\OneDrive
C:\Users\HarmV> $env:OneDriveCommercial
C:\Users\HarmV\OneDrive - NEXXT
C:\Users\HarmV> $env:OneDriveConsumer
C:\Users\HarmV\OneDrive

It's basically system variables you can use in scripts so that you don't hardcode specific paths. It might be that c:\users\public is remapped to d:\users\public, and your scripts will fail if you don't use the $env:public variable.

Hash table

A hash table is a data structure of key and value pairs. You can create one using the following:

$hashtable=@{ "Android" = "Google" "iOS" = "Apple" "MacOS"= "Apple" "Windows" = "Microsoft" }
$hashtable=@{
 "Android" = "Google"
 "iOS" = "Apple"
 "MacOS"= "Apple"
 "Windows" = "Microsoft"
}

This will look like this:

C:\Users\HarmV> $hashtable
C:\Users\HarmV> $hashtable Name Value ---- ----- MacOS Apple iOS Apple Windows Microsoft Android Google
C:\Users\HarmV> $hashtable

Name                           Value
----                           -----
MacOS                          Apple
iOS                            Apple
Windows                        Microsoft
Android                        Google
You can search for a certain value by using the following to get all values that contain Apple:

C:\Users\HarmV> $hashtable.GetEnumerator().Where({$_.Value -contains 'Apple'})
C:\Users\HarmV> $hashtable.GetEnumerator().Where({$_.Value -contains 'Apple'}) Name Value ---- ----- MacOS Apple iOS Apple
C:\Users\HarmV> $hashtable.GetEnumerator().Where({$_.Value -contains 'Apple'})

Name                           Value
----                           -----
MacOS                          Apple
iOS                            Apple

Int32/64

You can store numbers in variables, for example:

C:\Users\HarmV> $number=1
C:\Users\HarmV> $number.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
C:\Users\HarmV> $number=1 C:\Users\HarmV> $number.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Int32 System.ValueType
C:\Users\HarmV> $number=1
C:\Users\HarmV> $number.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

You can do math with this as well, for example:

C:\Users\HarmV> $number*8
C:\Users\HarmV> $number*8 8
C:\Users\HarmV> $number*8
8

You can also add 1 to an existing number variable. I use this to show progress in a script. For example:

$files=Get-ChildItem -Path d:\temp -Filter *.ps1
foreach ($file in $files) {
Write-Host ("[{0}/{1}] Found {2}" -f $count, $files.count, $file.fullname)
$count=1 $files=Get-ChildItem -Path d:\temp -Filter*.ps1 foreach ($file in $files) { Write-Host ("[{0}/{1}] Found {2}" -f $count, $files.count, $file.fullname) $count++ }
$count=1
$files=Get-ChildItem -Path d:\temp -Filter *.ps1
foreach ($file in $files) {
Write-Host ("[{0}/{1}] Found {2}" -f $count, $files.count, $file.fullname)
$count++
}

This will show an output that looks like this:

[1/49] Found D:\Temp\365healthstatus.ps1
[2/49] Found D:\Temp\Activation.ps1
[3/49] Found D:\Temp\AdminGroupChangeReport.ps1
[4/49] Found D:\Temp\AdminGroups.ps1
[5/49] Found D:\Temp\AdminReport.ps1
[6/49] Found D:\Temp\AppleDEPProfile_Assign.ps1
[7/49] Found D:\Temp\applevpp_sync.ps1
[8/49] Found D:\Temp\BIOS_Settings_For_HP.ps1
[9/49] Found D:\Temp\bitlocker.ps1
[10/49] Found D:\Temp\bitlockerremediate.ps1
[11/49] Found D:\Temp\bitlockertest.ps1
[12/49] Found D:\Temp\calendar_events.ps1
[1/49] Found D:\Temp\365healthstatus.ps1 [2/49] Found D:\Temp\Activation.ps1 [3/49] Found D:\Temp\AdminGroupChangeReport.ps1 [4/49] Found D:\Temp\AdminGroups.ps1 [5/49] Found D:\Temp\AdminReport.ps1 [6/49] Found D:\Temp\AppleDEPProfile_Assign.ps1 [7/49] Found D:\Temp\applevpp_sync.ps1 [8/49] Found D:\Temp\BIOS_Settings_For_HP.ps1 [9/49] Found D:\Temp\bitlocker.ps1 [10/49] Found D:\Temp\bitlockerremediate.ps1 [11/49] Found D:\Temp\bitlockertest.ps1 [12/49] Found D:\Temp\calendar_events.ps1
[1/49] Found D:\Temp\365healthstatus.ps1
[2/49] Found D:\Temp\Activation.ps1
[3/49] Found D:\Temp\AdminGroupChangeReport.ps1
[4/49] Found D:\Temp\AdminGroups.ps1
[5/49] Found D:\Temp\AdminReport.ps1
[6/49] Found D:\Temp\AppleDEPProfile_Assign.ps1
[7/49] Found D:\Temp\applevpp_sync.ps1
[8/49] Found D:\Temp\BIOS_Settings_For_HP.ps1
[9/49] Found D:\Temp\bitlocker.ps1
[10/49] Found D:\Temp\bitlockerremediate.ps1
[11/49] Found D:\Temp\bitlockertest.ps1
[12/49] Found D:\Temp\calendar_events.ps1

String

A string is a simple object with a value, for example:

$string="Hello world"
This is displayed as:

C:\Users\HarmV> $string Hello world
C:\Users\HarmV> $string
Hello world

And you can see it's an object:

C:\Users\HarmV> $string.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
C:\Users\HarmV> $string.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True String System.Object
C:\Users\HarmV> $string.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
You can combine two strings in your output like this:

C:\Users\HarmV> $string2="!"
C:\Users\HarmV> Write-Host $string$string2
C:\Users\HarmV> $string2="!" C:\Users\HarmV> Write-Host $string$string2 Hello world!
C:\Users\HarmV> $string2="!"
C:\Users\HarmV> Write-Host $string$string2
Hello world!

Or join two strings together like this using -join with the space separator using " "

C:\Users\HarmV> $string1="Good"
C:\Users\HarmV> $string2="Evening"
C:\Users\HarmV> $string1,$string2 -join " "
C:\Users\HarmV> $string1="Good" C:\Users\HarmV> $string2="Evening" C:\Users\HarmV> $string1,$string2 -join " " Good Evening

C:\Users\HarmV> $string1="Good"
C:\Users\HarmV> $string2="Evening"
C:\Users\HarmV> $string1,$string2 -join " "
Good Evening

But you can also split a string using -split using the ";" character as a delimiter, for example:

C:\Users\HarmV> $string="Hello;world"
C:\Users\HarmV> $string -split ";"
C:\Users\HarmV> $string="Hello;world" C:\Users\HarmV> $string -split ";" Hello world
C:\Users\HarmV> $string="Hello;world"
C:\Users\HarmV> $string -split ";"
Hello
world
This will output the string in two lines. You can combine those again by using -join:

C:\Users\HarmV> $string -split ";" -join " "
C:\Users\HarmV> $string -split ";" -join " " Hello world
C:\Users\HarmV> $string -split ";" -join " "
Hello world

You can also select a certain range of characters from a string using SubString, for example:

C:\Users\HarmV> $string="Hello world"
C:\Users\HarmV> $string.SubString(0,5)
C:\Users\HarmV> $string="Hello world" C:\Users\HarmV> $string.SubString(0,5) Hello
C:\Users\HarmV> $string="Hello world"
C:\Users\HarmV> $string.SubString(0,5)
Hello

And you can search/replace words in a string using the replace method, for example:

C:\Users\HarmV> $string="Hello world"
C:\Users\HarmV> $string.Replace('Hello','Goodbye')
C:\Users\HarmV> $string="Hello world" C:\Users\HarmV> $string.Replace('Hello','Goodbye') Goodbye world
C:\Users\HarmV> $string="Hello world"
C:\Users\HarmV> $string.Replace('Hello','Goodbye')
Goodbye world

5 PowerShell Script Examples To Inspire You to Get Scripting

Creating and Updating Registry Keys and Values Each application and operating system on your Windows computer is registered in a central location, the Windows Registry. The Windows Registry is composed of values and keys, where keys being the containers for the values.

PowerShell has many built-in commands to help you create, update and modify registry keys and values. To make changes to the registry, listed below are three different PowerShell commands. Let's cover some examples of how each of these PowerShell cmdlets works.

New-Item - Creates new registry keys.
New-ItemProperty - Creates new registry values.
Set-ItemProperty - Changes registry key values.
The example script below defines a list of registry keys, checks to see if each key exists. If so, it then updates the registry values inside. If not, it creates the keys and then creates new registry values inside of those keys.

## Defines three registry key paths in an array
$tls10 = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server', 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client'

## Checks to see if all of the registry keys in the array exists
$tls10check = ($tls10 | Test-Path) -notcontains $false

## If all of the registry keys exist
if ($tls10check -eq $True){
 ## Updates four different DWORD registry values to either 0 or 1
 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'Enabled' -value '0' -Type 'DWORD'
 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'DisabledByDefault' -value '1' -Type 'DWORD'
 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'Enabled' -value '0' -Type 'DWORD'
 Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'DisabledByDefault' -value '1' -Type 'DWORD'
} else { ## If at least one of the registry keys do not exist
 ## Creates the missing registry keys skipping the confirmation (Force)
 New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -Force
 New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -Force
 
 ## Creates four different DWORD registry values setting the value to either 0 or 1
 New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'Enabled' -value '0' -Type 'DWORD'
 New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -name 'DisabledByDefault' -value '1' -Type 'DWORD'
 New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'Enabled' -value '0' -Type 'DWORD'
 New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client' -name 'DisabledByDefault' -value '1' -Type 'DWORD'
}

Starting a Windows Service (If Not Running)

Once you're done editing the registry, lets move right along to managing Windows services.Starting a Windows Service In the below PowerShell script example, you'll see a great example of performing some comparison logic followed by an action. When run, this script will get the Status of the EventLog service. If the Status is anything but Running, it will write some text to the console and start the service.

If the service is already started, it will tell you so and perform no further actions.

## Define the service name in a variable
$ServiceName = 'EventLog'

## Read the service from Windows to return a service object
$ServiceInfo = Get-Service -Name $ServiceName

## If the server is not running (ne)
if ($ServiceInfo.Status -ne 'Running') {
 ## Write to the console that the service is not running
 Write-Host 'Service is not started, starting service'
 ## Start the service
 Start-Service -Name $ServiceName
 ## Update the $ServiceInfo object to reflect the new state
 $ServiceInfo.Refresh()
 ## Write to the console the Status property which indicates the state of the service
 Write-Host $ServiceInfo.Status
} else { ## If the Status is anything but Running
 ## Write to the console the service is already running
 Write-Host 'The service is already running.'
}

## Define the service name in a variable
$ServiceName = 'EventLog'

## Read the service from Windows to return a service object
$ServiceInfo = Get-Service -Name $ServiceName

## If the server is not running (ne)
if ($ServiceInfo.Status -ne 'Running') {
 ## Write to the console that the service is not running
 Write-Host 'Service is not started, starting service'
 ## Start the service
 Start-Service -Name $ServiceName
 ## Update the $ServiceInfo object to reflect the new state
 $ServiceInfo.Refresh()
 ## Write to the console the Status property which indicates the state of the service
 Write-Host $ServiceInfo.Status
} else { ## If the Status is anything but Running
 ## Write to the console the service is already running
 Write-Host 'The service is already running.'
}

Finding CIM/WMI Classes

CIM is a handy repository of information in Windows, and PowerShell can, by default, query it. Using a combination of CIM cmdlets, you can gather all kinds of handy information from CIM. CIM data is broken out in CIM classes. CIM classes hold categories of Windows information. Perhaps you're looking for some hardware information and discovered that the CIM class has some variation of System in the class name. Using the Get-CimClass cmdlet, you can find all classes matching a particular pattern.

To retrieve one or more CIM classes via the Get-CimClass cmdlet, specify the ClassName parameter with the exact class name or search pattern on a PowerShell command prompt. If you don't know the whole class name, you can use a wildcard (*). Below, you'll see the command to find all CIM classes matching the pattern "Win32_*System".

Get-CimClass -ClassName Win32_*System

Querying WMI for Computer Information

Once you've found the CIM class you'd like to query, PowerShell now has another cmdlet called Get-CimInstance to help you query information from that class.

Below you'll find another great PowerShell example script, this time demonstrating a real-world case of querying CIM. This example is querying the Win32_OperatingSystem CIM class on two remote computers at once and creating a CSV file with a few select properties returned from that query.

After finding your desired -ClassName, perhaps you wish to filter the information further where the property names are separated by commas.

## Query the Win32_OperatingSystem CIM instance on both the serv1 and serv2 computers
Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName Serv1,Serv2 |`
## Limit the output to only a few select propeties
Select-Object -Property BuildNumber,BuildType,OSType,ServicePackMajorVersion,ServicePackMinorVersion | `
## Send each CIM instance object to a CSV file called C:\Folders\Computers.csv
Export-CSV C:\Folder\Computers.csv -NoTypeInformation -Encoding UTF8 -Verbose

When the script finishes, you'll find a CSV file called Computers.csv in the C:\Folder directory looking something like this:

BuildNumber,BuildType,OSType,ServicePackMajorVersion,ServicePackMinorVersion
19042,Multiprocessor Free,18,0,0
19012,Multiprocessor Free,18,0,0

Installing Applications

Now that you know how to gather computer information, you can now use that information to tell whether those computers are compatible with certain applications, for example.

Manually installing software on a single computer may be doable, but if you have many computers to install software on, that task soon becomes untenable. To help out, you can create a PowerShell script to install software (if your software supports it) silently.

Perhaps you're working on a Microsoft Installer (MSI) package, and you'd like to install the software silently. You can install MSI packages via the msiexec.exe utility. This utility isn't a PowerShell command, but you can invoke it with PowerShell.

Maybe you have an MSI called package.msi in the C:\folder directory. To silently install software, you must first know what switches that installer requires to do so. To find the available switches, run the MSI and provide a /? switch. The /? should display each available parameter, as shown below.

C:\Folder\package.msi /?

Once you know the switches the installer package needs, it's now time to invoke the msiexec.exe utility. Since msiexec.exe is not a PowerShell command, you must invoke it as an external process. One of the easiest ways to invoke processes with PowerShell is using the Start-Process cmdlet.

The Start-Process has two parameters you'll need to use in this example; Name and Wait. In this below example, you'll see that Start-Process is invoking the msiexec.exe utility by using the Name parameter and waiting (Wait) for the process to finish before releasing control back to the console.

Since msiexec.exe needs a few parameters to install the software silently, the example uses the ArgumentList to provide each parameter that msiexec.exe needs to install the software silently.

## Invoke the msiexec.exe process passing the /i argument to indicate installation
## the path to the MSI, /q to install silently and the location of the log file
## that will log error messages (/le).
Start-Process -Name 'msiexec.exe' -Wait -ArgumentList '/i "C:\Folder\package.msi" /q /le "C:\Folder\package.log"'

Handling Errors with a Try, Catch, and Finally Statement

To sum up this PowerShell script example post, let's end on a topic you can (and should) apply to any PowerShell script; error handling. Error handling "catches" unexpected errors found in your script, making your script more robust and able to execute with fewer hiccups.

Error handling is a big topic that could be explained in an entire book but let's only cover the basics; the try, catch, and finally statements. Try/catch blocks are blocks of code that PowerShell "monitors" for hard-terminating errors or exceptions.

If code inside a try/catch block produces a hard-terminating error, that exception will be "caught" by a block and specific code run against it. Once the script is finished, either returning an exception or completing successfully, PowerShell will process the finally block.

In the below example, the script creates a File Transfer Protocol (FTP) script file that connects to an FTP server, attempts to upload a file to an FTP server, and then removes the FTP script file once complete.

You'll see in the example the "functional" code has been wrapped in a try, catch and finally block. For example, if the Out-File cmdlet returns a hard-terminating error, PowerShell will catch that error and return Error: the error/exception message and then exit with a code of 1.

Once the script successfully creates the FTP script file, it then attempts to invoke the ftp.exe utility, which executes the commands inside of the FTP script file. If _that

Notice that you're creating the FTP script file via the Out-File cmdlet with the first try statement. If the try statement failed, the catch statement below would catch the errors. Then the $($_.Exception.Message) property followed by an exit 1 will end the script, displaying the error status.

But if the first try statement succeeds, the following try statement will run the generated FTP script. When the FTP script runs successfully, you will see log output with the successful connection to the FTP server and file download.

Then regardless of the results of the try and catch statements, the finally statement will run and remove the FTP script file.

## Create the try block and create any code inside.
try 
{ 
 ## Create the FTP script file using a here string (https://devblogs.microsoft.com/scripting/powertip-use-here-strings-with-powershell/)
 ## If Out-File creates the FTP script, it then invokes ftp.exe to execute
 ## the script file. 
 $Script = @"
 open localhost
 username
 password
 BINARY
 CD remotefolder
 LCD C:\folder
 GET remote.file
 BYE
"@
 $Script | Out-File "C:\Folder\ftp.txt" -Encoding ASCII
 ftp -s:C:\folder\ftp.txt
} 
catch
{
 ## If, at any time, for any code inside of the try block, returns a hard-terminating error
 ## PowerShell will divert the code to the catch block which writes to the console
 ## and exits the PowerShell console with a 1 exit code.
 Write-Host "Error: $($_.Exception.Message)"
 exit 1
}
finally
{
 ## Regardless if the catch block caught an exception, remove the FTP script file
 Remove-Item -Path "C:\folder\ftp.txt"
}

## When the code inside of the try/catch/finally blocks completes (error or not),
## exit the PowerShell session with an exit code of 0
exit 0

If an exception is caught anywhere inside the try block, you'll see an error message ($_.Exception.Message) indicating what went wrong.

Using PowerShell to Get the Windows Version

https://adamtheautomator.com/powershell-to-get-the-windows-version-2/

Before taking on complex stuff with PowerShell, start with the basics, like getting your system information. With PowerShell, you can quickly get the Window version via the systeminfo command.

Open your PowerShell, and run the following command to get detailed information about your system, including the operating system (OS) version.

.\\systeminfo

Below, you can see detailed information about the OS, including the current version number, 10.0.19044 N/A Build 19044.

Selecting Specific Properties to Get the Windows Version

Great! You have just retrieved detailed system information. But if you look closely, the command is short, yet the output seems a lot.

If you only want specific property values, the Get-ComputerInfo command is one of the quickest methods of getting specific system information, like your Windows version.

Run the below to get the Windows version, product name, and version of Windows Kernel and your system hardware’s Operating Hardware Abstraction (OHA).

Below, piping the Select-Object command to the Get-ComputerInfo command lets you retrieve only select properties.

Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsHardwareAbstractionLayerVersion

Retrieving the Windows Version via the System.Environment Class

The System.Environment class also has a property called OSVersion, which contains information about the current OS.

Run the following command to call the OSVersion.Version property from the System.Environment class. The double colon (::) symbol is used to call static methods from a class.

[System.Environment]::OSVersion.Version

As you can see below, the output displays the OSVersion information as follows:

PROPERTY VALUE DESCRIPTION
Major 10 Stands for Windows version 10 (Windows 10).
Minor 0 There are two types of Windows releases, major and minor. Major releases are the "big" updates like the Creator update, and minor releases are smaller cumulative updates.
Build 19044 The number used to check the Windows version. In this case, it is 1909. The code name for this version is 21H2 https://learn.microsoft.com/en-us/windows/release-health/release-information#historyTable_1, which stands for Windows 10 November 2019 Update and is the eighth major update to Windows 10, released on November 12, 2019.
Revision 0 Denotes a sub-version of the build.

POWERSHELL PROFILE

Windows PowerShell 5.1

Profile Path
Current User - Current Host $Home\[My ]Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Current User - All Hosts $Home\[My ]Documents\WindowsPowerShell\Profile.ps1
All Users - Current Host $PSHOME\Microsoft.PowerShell_profile.ps1
All Users - All Hosts $PSHOME\Profile.ps1
💡 The $Home variable references the current users home directory while the $PSHOME variable references the installation path of PowerShell.

PowerShell 7.x

Profile Path
Current User - Current Host - Windows $Home\[My ]Documents\Powershell\Microsoft.Powershell_profile.ps1
Current User - Current Host - Linux/macOS ~/.config/powershell/Microsoft.Powershell_profile.ps1
Current User - All Hosts - Windows $Home\[My ]Documents\Powershell\Profile.ps1
Current User - All Hosts - Linux/macOS ~/.config/powershell/profile.ps1
All Users - Current Host - Windows $PSHOME\Microsoft.Powershell_profile.ps1
All Users - Current Host - Linux/macOS /usr/local/microsoft/powershell/7/Microsoft.Powershell_profile.ps1
All Users - All Hosts - Windows $PSHOME\Profile.ps1
All Users - All Hosts - Linux/macOS /usr/local/microsoft/powershell/7/profile.ps1
Explanation:
# Example 1: Extracts the file extension using [System.IO.Path]::GetExtension($path) and removes it from the file name using [System.IO.Path]::GetFileNameWithoutExtension($path).
# Example 2: Uses -replace '_.*', '' to remove everything after the first underscore in a string.
# Combined Example: Demonstrates both concepts together, extracting the file name without extension from a path and then cleaning it by removing everything after the first underscore.
# Example 1: Extracting and Removing the File Extension
# Given path
$path = "C:\Users\JohnDoe\Documents\Report.docx"
# Extract the file extension
$extension = [System.IO.Path]::GetExtension($path)
# Remove the file extension from the path
$filenameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($path)
Write-Host "Given path: $path"
Write-Host "File extension: $extension"
Write-Host "File name without extension: $filenameWithoutExtension"
Write-Host
# Example 2: Using -replace to Clean Up a String
# Given string
$originalString = "Microsoft.LanguageExperiencePackhe-IL_8wekyb3d8bbwe"
# Remove everything after the first underscore
$cleanedString = $originalString -replace '_.*', ''
Write-Host "Given string: $originalString"
Write-Host "Cleaned string: $cleanedString"
Write-Host
# Combined Example: Extracting, Removing Extension, and Cleaning String
# Given path
$path = "C:\Users\JohnDoe\Documents\Report_8wekyb3d8bbwe.docx"
# Extract and remove the file extension
$filenameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($path)
# Clean up the string by removing everything after the first underscore
$cleanedFilename = $filenameWithoutExtension -replace '_.*', ''
Write-Host "Given path: $path"
Write-Host "File name without extension: $filenameWithoutExtension"
Write-Host "Cleaned file name: $cleanedFilename"
# Key Points
# Flexibility: Split-Path -Leaf is versatile and can handle both full file paths and filenames.
# Regex Replacement: Using -replace '\.[^.]*$', '' ensures that only the extension is removed, preserving the rest of the filename.
# These examples illustrate how Split-Path -Leaf can be used effectively to manipulate filenames and paths in PowerShell. Adjust them based on your specific scenarios and requirements.
# Explanation
# Full File Path: With $path = "C:\Users\JohnDoe\Documents\Report.docx", Split-Path -Leaf $path returns "Report.docx", which is the filename with extension.
# Remove Extension: To remove the extension from $filename, we use -replace '\.[^.]*$', ''. This regex pattern ('\.[^.]*$') matches the last dot (\.) followed by any characters that are not dots ([^.]*) until the end of the string ($), effectively removing the extension.
# Result: $filenameWithoutExtension will contain "Report", which is the filename without the .docx extension.
# Removing Extension Using Split-Path -Leaf
# Example with full file path
$path = "C:\Users\JohnDoe\Documents\Report.docx"
# Get the filename using Split-Path -Leaf
$filename = Split-Path -Path $path -Leaf
# Remove the file extension
$filenameWithoutExtension = $filename -replace '\.[^.]*$', ''
Write-Host "Given path: $path"
Write-Host "Filename (using Split-Path -Leaf): $filename"
Write-Host "File name without extension: $filenameWithoutExtension"
# Using Split-Path -Leaf with Filenames
# Even if you only have a filename (without a path), you can still use Split-Path -Leaf to handle it. PowerShell treats the filename as if it were a leaf in the current directory structure.
# Example with filename only
$filename = "Report.docx"
# Get the filename using Split-Path -Leaf (simulating a full path)
$leaf = Split-Path -Path $filename -Leaf
# Remove the file extension
$filenameWithoutExtension = $leaf -replace '\.[^.]*$', ''
Write-Host "Given filename: $filename"
Write-Host "Filename (using Split-Path -Leaf): $leaf"
Write-Host "File name without extension: $filenameWithoutExtension"

Resolve DNS and IP addresses with PowerShell

Name to IP Address (DNS Forward)

[system.net.dns]: :GetHostAddresses('graef.io')
[system.net.dns]: :GetHostAddresses('graef.io').IPAddressToString

IP Address to Name (DNS Reverse)

[System.Net.Dns]::GetHostbyAddress('85.13.135.42')

HostName              Aliases AddressList
--------              ------- -----------
graef.io {}      {85.13.135.42}
Resolve-DnsName graef.io

Name      Type   TTL   Section    IPAddress
----      ----   ---   -------    ---------
graef.io  AAAA   72711 Answer     2a01:488:42:1000:50ed:84e8:ff91:1f91
graef.io  A      72711 Answer     80.237.132.232

Resolve-DnsName 80.237.132.232

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
232.132.237.80.in-addr.arpa    PTR    32738 Answer     graef.io
# Split the name from the path
Split-Path -Path file.ext -Leaf
# Split the path from the name
Split-Path -Path file.ext -Parent
# Split the last folder name from the path
Get-Item -Path file.ext | Split-Path -Parent | Split-Path -Parent | Split-Path -Leaf
# Split the drive letter
Split-Path -Path "D:\file.mp3" -Qualifier
# In PowerShell, there are three types of quotes that can be used to define strings:
# Double-quoted strings: Double-quoted strings (") allow for variable expansion and escape sequences.
"Hello, $name!"
# Single-quoted strings: Single-quoted strings (') treat the content literally, without variable expansion or escape sequences.
'Hello, $name!'
# Here-strings: Here-strings (@" "@ or @' '@) allow multiline string literals and preserve line breaks and formatting.
$message = @"
This is a multiline
string using a here-string.
It preserves line breaks and formatting.
"@
# These quotes can be used interchangeably depending on your requirements. Double-quoted strings are commonly used when you need to include variable values within the string. Single-quoted strings are useful when you want to treat the content literally without variable expansion. Here-strings are beneficial for multiline strings that require preserving line breaks and formatting.

In PowerShell, both ${} and $() are used for variable interpolation, but they serve slightly different purposes and are used in different contexts. Here's a breakdown of each

1. ${} (Curly Braces)

Purpose: ${} is used for variable name resolution, especially when dealing with complex variable names or when the variable name is part of a longer string

Usage:

When you need to clearly define a variable name within a string, especially if the variable is part of a longer expression or adjacent to other text. Useful when the variable name is followed by characters that might otherwise be interpreted as part of the variable name.

Example:

$OperatingName = "Windows 11 Pro"
$BuildNumber = 22621

# Using curly braces to ensure the variable is correctly interpreted
Write-Output "${OperatingName} Build ${BuildNumber}"

In this example, ${OperatingName} and ${BuildNumber} are used to clearly define where the variable names start and end.

$() (Subexpression Operator) Purpose: $() is used to evaluate expressions inside of strings or other contexts where the result of the expression needs to be inserted. It allows for more complex expressions and commands to be executed and their results inserted into strings.

Usage:

When you need to include the result of an expression or a command within a string. Useful for more complex calculations or cmdlet results that need to be embedded in strings.

Example:

$OperatingName = "Windows 11 Pro"
$BuildNumber = 22621

# Using subexpression to include the result of an expression
Write-Output "$($OperatingName) Build $($BuildNumber)"

In this example, $($OperatingName) and $($BuildNumber) are evaluated as expressions, and their results are inserted into the string.

Key Differences:

Complex Expressions: $() allows for the inclusion of complex expressions and commands, whereas ${} is mainly for simple variable names.

$CurrentDate = Get-Date
Write-Output "Today's date is $($CurrentDate.ToShortDateString())"

Here, $($CurrentDate.ToShortDateString()) executes the method ToShortDateString() and includes the result in the output.

Variable Resolution: ${} is used primarily to clarify and resolve variable names, especially when they are part of more complex strings or adjacent to other text.

$Version = "2024"
Write-Output "Version ${Version}Release"  # Correctly outputs "Version 2024Release"

In summary:

Use ${} for clear variable resolution, especially when the variable is next to other characters. Use $() for embedding the result of expressions or cmdlet outputs within strings.

It's useful to know how to use PowerShell variables and object properties in a double-quoted string as part of PowerShell'’'s string expansion

As a simple example we cab expand the $name variable inside the $sentence variable like so

$name = "John" $sentence = "My name is $name" Write-Host $sentence

output

My name is John

This is easy enough to grasp. Note that PowerShell expansion does NOT work inside single quotes like so

$name = "John" $sentence = 'My name is $name' Write-Host $sentence

output

My name is $name

We can use PowerShell expansion to write the PowerShell version to a variable and output it in a similar way like so

$version = $PSVersionTable.PSVersion $sentence = "Powershell version is $version" Write-Host $sentence

output

Powershell version is 5.1.19041.1682

However, if we wanted to inject the PowerShell version directly we can see that the following does not work

$sentence = "Powershell version is $PSVersionTable.PSVersion" Write-Host $sentence

output

Powershell version is System.Collections.Hashtable.PSVersion

Instead, what we need to do when expanding object properties in PowerShell is to enclose it in $() like so

$sentence = "Powershell version is $($PSVersionTable.PSVersion)" Write-Host $sentence

output

Powershell version is 5.1.19041.1682

A final alternative is to use the string format (-f) operator like so – here we use placeholders for variable names such as { 0 } { 1 } and { 2 } and after the -f parameter we specify a comma-delimited array of values to substitute in

$sentence = "Powershell version is {0}" -f $PSVersionTable.PSVersion Write-Host $sentence

output

Powershell version is 5.1.19041.1682

A slightly more complex version might be

$sentence = "Powershell major version is {0} and minor version is {1}" -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor Write-Host $sentence

output

Powershell major version is 5 and minor version is 1

Visual Studio Code Snippets - the Definitive VS Code Snippet Guide for Beginners

If you want to track down the source file yourself, the built-in snippets live inside each individual language extension directory. The file is located at «app root»\resources\app\extensions\«language»\snippets\«language».code-snippets on Windows. The location is similar for Mac and Linux. To create the snippets file, run the 'Preferences: Configure User Snippets' command, which opens a quickpick dialog as below. Your selection will open a file for editing. Example Here is a markdown snippet that comes with VS Code.

{
 "Insert heading level 1": {
 "prefix": "heading1",
 "body": ["# ${1:${TM_SELECTED_TEXT}}$0"],
 "description" : "Insert heading level 1"
 }
}

This snippet inserts a level 1 heading which wraps the markdown around the current selection (if there is one). A snippet has the following properties:

  1. "Insert heading level 1"is the snippet name. This is the value that is displayed in the IntelliSense suggestion list if no description is provided.
  2. The prefix property defines the trigger phrase for the snippet. It can be a string or an array of strings (if you want multiple trigger phrases). Substring matching is performed on prefixes, so in this case, typing "h1" would match our example snippet.
  3. The body property is the content that is inserted into the editor. It is an array of strings, which is one or more lines of content. The content is joined together before insertion.
  4. The description property can provide more information about the snippet. It is optional.
  5. The scope property allows you to target specific languages, and you can supply a comma-separated list in the string. It is optional. Of course, it is redundant for a languagespecific snippet file. The body of this snippet has 2 tab stops and uses the variable ${TM_SELECTED_TEXT} . Let's get into the syntax to understand this fully. Snippet syntax VS Code's snippet syntax is the same as the TextMate snippet syntax. However, it does not support 'interpolated shell code' and the use of the \u transformation.

The body of a snippet supports the following features

  1. Tab Stops Tab stops are specified by a dollar sign and an ordinal number e.g. $1 . $1 will be the first location, $2 will the second location, and so on. $0 is the final cursor position, which exits the snippet mode. For example, let's say we want to make an HTML div snippet and we want the first tab stop to be between the opening and closing tags. We also want to allow the user to tab outside of the tags to finish the snippet. Then we could make a snippet like this:
 "Insert div": {
 prefix: "div",
 body: ["<div>","$1","</div>", "$0"]
 }

Mirrored Tab Stops There are times when you need to provide the same value in several places in the inserted text. In these situations you can re-use the same ordinal number for tab stops to signal that you want them mirrored. Then your edits are synced. A typical example is a for loop which uses an index variable multiple times. Below is a JavaScript example of a for loop.

 "For Loop": {
 "prefix": "for",
 "body": [
 "for (let ${1:index} = 0; ${1:index} < ${2:array}.length; ${1:index}++) {",
 "\tconst ${3:element} = ${2:array}[${1:index}];",
 "\t$0",
 "}"
 ]
}
  1. Placeholders Placeholders are tab stops with default values. They are wrapped in curly braces, for example ${1:default} . The placeholder text is selected on focus such that it can be easily edited. Placeholders can be nested, like this: ${1:first ${2:second}} .

  2. Choices Choices present the user with a list of values at a tab stop. They are written as a commaseparated list of values enclosed in pipe-characters e.g. ${1|yes,no|} . This is the code for the markdown example shown earlier for inserting a task list. The choices are'x' or a blank space.

 "Insert task list": {
 "prefix": "task",
 "body": ["- [${1| ,x|}] ${2:text}", "${0}"]
}

Variables There is a good selection of variables you can use. You simply prefix the name with a dollar sign to use them, for example $TM_SELECTED_TEXT . For example, this snippet will create a block comment for any language with today's date:

{
 "Insert block comment with date": {
 prefix: "date comment",
 body: ["${BLOCK_COMMENT_START}",
 "${CURRENT_YEAR}/${CURRENT_MONTH}/${CURRENT_DATE} ${1}",
 "${BLOCK_COMMENT_END}"]
 }
}

You can specify a default for a variable if you wish, like ${TM_SELECTED_TEXT:default} . If a variable does not have a value assigned, the default or an empty string is inserted. If you make a mistake and include a variable name that is not defined, the name of the variable is transformed into a placeholder. The following workspace variables can be used:

TM_SELECTED_TEXT : The currently selected text or the empty string, TM_CURRENT_LINE : The contents of the current line, TM_CURRENT_WORD : The contents of the word under cursor or the empty string, TM_LINE_INDEX : The zero-index based line number, TM_LINE_NUMBER : The one-index based line number, TM_FILENAME : The filename of the current document, TM_FILENAME_BASE : The filename of the current document without its extensions, TM_DIRECTORY : The directory of the current document, TM_FILEPATH : The full file path of the current document, CLIPBOARD : The contents of your clipboard, WORKSPACE_NAME : The name of the opened workspace or folder. The following time-related variables can be used: CURRENT_YEAR : The current year, CURRENT_YEAR_SHORT : The current year's last two digits, CURRENT_MONTH : The month as two digits (example '07'), CURRENT_MONTH_NAME : The full name of the month (example 'July'), CURRENT_MONTH_NAME_SHORT : The short name of the month (example 'Jul'), CURRENT_DATE : The day of the month, CURRENT_DAY_NAME : The name of day (example 'Monday'), CURRENT_DAY_NAME_SHORT : The short name of the day (example 'Mon'), CURRENT_HOUR : The current hour in 24-hour clock format, CURRENT_MINUTE : The current minute, CURRENT_SECOND : The current second, CURRENT_SECONDS_UNIX : The number of seconds since the Unix epoch. The following comment variables can be used. They honour the syntax of the document's language: BLOCK_COMMENT_START : For example, in HTML, LINE_COMMENT : For example, // in JavaScript.

  1. Transformations Transformations can be applied to a variable or a placeholder. If you are familiar with regular expressions (regex), most of this should be familiar. The format of a transformation is: ${«variable or placeholder»/«regex»/«replacement string»/«flags»} . It is similar to String.protoype.replace() in JavaScript. The "parameters" do the following: «regex» : This is a regular expression that is matched against the value of the variable or placeholder. The JavaScript regex syntax is supported. «replacement string» : This is the string you want to replace the original text with. It can reference capture groups from the «regex» , perform case formatting (using the special functions: /upcase , /downcase , and /capitalize ), and perform conditional insertions. See TextMate Replacement String Syntax for more in-depth information. «flags» : Flags that are passed to the regular expression. The JavaScript regex flags can be used: g : Global search, i : Case-insensitive search, m : Multi-line search, s : Allows . to match newline characters, u : Unicode. Treat the pattern as a sequence of Unicode code points, y : Perform a "sticky" search that matches starting at the current position in the target string. To reference a capture group, use $n where n is the capture group number. Using $0 means the entire match. This can be a bit confusing since tab stops have the same syntax. Just remember that if it is contained within forward slashes, then it is referencing a capture group. The easiest way to understand the syntax fully is to check out a few examples.
| SNIPPET BODY | INPUT | OUTPUT| EXPLANATION |
| ------------ | ----- | ----- | ----------- |
| ["${TM_SELECTED_TEXT/^.+$/• $0/gm}"] | line1 | line2 | • line1 | • line2 | Put a bullet point before each non-empty line of the selected text.
| ["${TM_SELECTED_TEXT/^(\\w+)/${1:/capitalize}/}"] | the cat is on the mat. | The cat is on the mat. | Capitalize the first word of selected text.
| ["${TM_FILENAME/.*/${0:/upcase}/}"] | example.js | EXAMPLE.JS | Insert the filename of the current file uppercased.
| ["[","${CLIPBOARD/^(.+)$/'$1',/gm}","]"] | line1 line2 | ['line1', 'line2',] | Turn the contents of the clipboard into a string array. | Each non-empty line is an element.

As you can see from the second example above, metacharacter sequences must be escaped, for example insert \w for a word character.

Placeholder Transformations Placeholder transforms do not allow a default value or choices! Maybe it is more suitable to call them tab stop transformations. The example below will uppercase the text of the first tab stop.

 "Uppercase first tab stop": {
 "prefix": "up",
 "body": ["${1/.*/${0:/upcase}/}", "$0"]
 }

You can have a placeholder and perform a transformation on a mirrored instance. The transformation will not be performed on the initial placeholder.? Would you use this behaviour somewhere?I find it confusing initially, so it may have the same affect on others.

 "Uppercase second tab stop instance only": {
 "prefix": "up",
 "body": ["${1:title}", "${1/(.*)/${1:/upcase}/}", "$0"]
 }

How do I assign Keyboard Shortcuts for snippets? By adding your shortcuts to keybindings.json . You can open the file by running the

'Preferences: Open Keyboard Shortcuts File (JSON)' command. For example, to add a shortcut for the built-in markdown snippet"Insert heading level 1":

{
 "key": "ctrl+m ctrl+1",
 "command": "editor.action.insertSnippet",
 "when": "editorTextFocus && editorLangId == markdown",
 "args": {
 "langId": "markdown",
 "name": "Insert heading level 1"
 }
}

You define a shortcut by specifying the key combination you want to use, the command ID, and an optional when clause context for the context when the keyboard shortcut is enabled. Through the args object, you can target an existing snippet by using the langId and name properties. The langId argument is the language ID of the language that the snippets were written for. The name is the snippet's name as it is defined in the snippet file.

You can define an inline snippet if you wish using the snippet property.

 {
 "key": "ctrl+k 1",
 "command": "editor.action.insertSnippet",
 "when": "editorTextFocus",
 "args": {
 "snippet": "${BLOCK_COMMENT_START}${CURRENT_YEAR}/${CURRENT_MONTH}/${CURRENT_DATE} ${1} ${BLOCK}
 }
}

You can use the Keyboard Shortcuts UI also, but it does not have the ability to add a new shortcut. Another downside of the UI is that it does not show the args object, which makes it more

Here is an example of how to include What-If capabilities in a simple PowerShell function

function New-Directory {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    if ($PSCmdlet.ShouldProcess($Path, 'Create directory')) {
        New-Item -ItemType Directory -Path $Path
    }
}

In this example, we define a function called "New-Directory" that creates a new directory at the specified path. We've included the

[CmdletBinding()]

attribute at the beginning of the function definition, which enables a set of common parameters, including -WhatIf.

We've also added the

[SupportsShouldProcess()]

parameter to the CmdletBinding attribute. This enables the function to support the What-If feature. The $PSCmdlet.ShouldProcess() method is used to determine whether to execute the code or not, based on whether the -WhatIf parameter was used or not.

When the function is called, you can use the -WhatIf parameter to preview the changes that would occur if the function were to run:

PS C:\> New-Directory -Path C:\Temp -WhatIf
What if: Performing the operation "Create directory" on target "C:\Temp".

This shows that if the New-Directory function were to run, it would create a new directory at C:\Temp. However, because we used the -WhatIf parameter, it only shows us a preview of what would happen, without actually executing the command.

If you were to run the command without the -WhatIf parameter, it would create the directory:

PS C:\> New-Directory -Path C:\Temp
PS C:\>

Powershell While Loop

https://adamtheautomator.com/powershell-while-loop/

Executing Single Condition while Loops

One of the simplest types of while loop is the single condition type. Unlike the If statement, this type of loop executes a set of given commands so long as a specified condition evaluates to true. When a 'False' is returned, the loop breaks, and the execution continues with the rest of the script

Below is the syntax for a single condition while loop

The first thing you will notice is the use of parentheses. The condition must be enclosed in parentheses, while the code block is a set of PowerShell commands that executes while the condition is true

The second thing to note is that the condition must return a Boolean value, which evaluates to either True or False

while (condition)
{
 # Code block to execute
}

In the code below, the if statement checks the value of $i. If the value of $i is 5, the continue keyword skips over the remaining code in the loop and continues with the next iteration. If the value of $i is 8, the break keyword exits the loop and continues with the rest of the script. Otherwise, the while loop prints (Write-Host) and increments the $i variable's value by 1

# Declares an $array of 10 items
$array = 1..10
# Declares the $i variable with the initial value of 0
$i = 0
# Sets the While look to execute until the condition is met
while ($i -lt $array.Count)
{
 # Checks if $i equals 5
  if ($i -eq 5)
  {
  # If yes, increment $i by 1
    $i++
  # Continue with the next iteration
    continue
  }
 # Checks if $i equals 8
  if ($i -eq 8)
  {
  # If yes, break the While loop and continue with the rest of the script
    break
  }
 # Prints the current value of $i
  Write-Host "Processing item $i"
 # Increments $1 by 1
  $i++
}

Executing PowerShell While Loop with Built-in Variables ($true/$false)

The previous example, using conditions in the while loop, works fine. But you can also use PowerShell's built-in variables, such as $true and $false, to create a While loop

The syntax below executes until $true is no longer $true. In other words, the loop runs forever. But you must always include a way to break out of an infinite loop. Otherwise, you are forever stuck. You will learn more about how to break from a loop using break and continue later in this tutorial

while($true)
{
 # Code block to execute
}

Execute the code block below, which runs forever, printing the value of $i to the console

# Declares the $i variable with the initial value of 0
$i = 0

# Sets the While loop to run while the condition is $true
while($true)
{
 # Increments the $i value by 1
 $i++

 # Prints the $i variable's current value
 Write-Host $i
}

Now, press Ctrl+C to break out of the loop. This loop consumes many system resources, so be careful when using them

Executing Multi-Condition While Loops

In addition to single-condition while loops, you can also create multi-condition while loops. Like the single condition While loop, the conditions must return a Boolean value, either True or False

Below is the syntax for a multi-condition while loop, similar to the syntax for a single-condition while loop. The main difference is that you can include multiple conditions separated by the following operators

  • AND (-and)- Both conditions must be true
  • OR (-or) (Either condition can be true)
# AND operator
while (condition1 -AND condition2)
{
 # Code block to execute
}

# OR operator
while (condition1 -OR condition2)
{
 # Code block to execute
}

Execute the code below, which loops while $val is not equal (-ne) to 3 -and $i is not equal (-ne) to 5

When both variables' values reach their respective conditions, the loop breaks, and execution continues with the rest of the script

# Declares the $val and $i variables with initial values of 0
$val = 0
$i = 0

# Sets the While loop to execute until $val is equal to 3 and $i is equal to 5
while ($val -ne 3 -and $i -ne 6)
{
 # Increments $val by 1
  $val++
 # Increments $i by 2
  $i += 2
 # Prints $val and $i variables' current value
  Write-Host "$val, $i"
}

Executing a while loop with AND operator

Now, execute the below code, which asks the user for their age, stored in the $age variable

If the user enters a number either less than (-lt) 1 or is not a number (-nomatch), the user is prompted again to enter a valid number. This behavior is useful in giving users multiple chances to enter valid input

# Prompts the users to enter their age
$age = Read-Host "Please Enter Your Age"

# Sets the While loop to run until the user provides a valid input
while ($age -notmatch "\\d+" -or $age -lt 1)
{
 # Re-prompts the user to enter a valid age number
  $age = Read-Host "Please Enter Your Valid Age"
}

# Prints the valid age input
Write-Host "Your age is $age

In the output below, you can see the user was prompted to enter their age three times, as follows:

  • The first time, the user entered ten, which is not a number.
  • The second time, the user entered 0, which is below 1.
  • The third time, the user entered 10, which is a valid age.

Using BREAK and CONTINUE Keywords in While Loops You have seen how while loops add flexibility to your PowerShell script. But to better control, your While loops’ execution, add the break and continue keywords.

For example, if you only want to process a certain number of items in an array, you can use the BREAK keyword to exit the loop when the desired number of items has been processed.

These keywords function as follows:

KEYWORD FUNCTION
break Immediately exits the loop and continues execution with the rest of the script.
continue Skips over the remaining code block in the current iteration of the loop and continues with the next iteration.

Execute the code below, which loops through an array of 10 items.

In the code below, the if statement checks the value of $i. If the value of $i is 5, the continue keyword skips over the remaining code inthe loop and continues with the next iteration. If the value of $i is 8, the break keyword exits the loop and continues with the rest of the script.

Otherwise, the while loop prints (Write-Host) and increments the $i variable's value by 1.

# Declares an $array of 10 items
$array = 1..10
# Declares the $i variable with the initial value of 0
$i = 0
# Sets the While look to execute until the condition is met
while ($i -lt $array.Count)
{
 # Checks if $i equals 5
  if ($i -eq 5)
  {
  # If yes, increment $i by 1
    $i++
  # Continue with the next iteration
    continue
  }

 # Checks if $i equals 8
  if($i -eq 8)
  {
  # If yes, break the While loop and continue with the rest of the script
    break
  }
 # Prints the current value of $i
  Write-Host "Processing item $i"
 # Increments $1 by 1
  $i++
}

As you can see in the output below, the loop skipped over the fifth and eighth items in the array. The while loop processed all other items in the array and exited after reaching the eighth item.

Limiting the Time a While Loop Runs

Typically, you may want to limit the amount of time a loop runs. Perhaps you are trying to connect to a remote server. If so, you can give the server time to respond before timing out and exiting the loop by using the Start-Sleep cmdlet inside your while loop.

The Start-Sleep cmdlet pauses the execution of the script for a specified amount of time.

Execute the code below to get and store the current date and time Get-Date in the $startTime variable. The while loop runs while the current date/time is less than 10 seconds from the value stored in $startTime.

As the while loop runs, a message prints while the Start-Sleep cmdlet pauses the execution of the script for 1 second.

The code block below is just a boilerplate for what you would actually use in practice. You can put more in the code inside the loop as needed.

# Get and store the current date/time
$startTime = Get-Date
# Sets the While loop to run while the current date/time is less than 10 seconds

 # from the value stored in $startTime.
while ((Get-Date) -lt ($startTime.AddSeconds(10)))
{
 # Prints a message
  Write-Host "Waiting for server to respond..."
 # Pauses the script for one second
  Start-Sleep -Seconds 1
}

Various Ways to Collect Windows Version

WindowsRelease

function Get-WindowsRelease
{
    $CurrentOSInfo = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $WindowsRelease = $CurrentOSInfo.GetValue('ReleaseId')
    if ($WindowsRelease -eq "2009"){$WindowsRelease = $CurrentOSInfo.GetValue('DisplayVersion')}

    return $WindowsRelease
}

Windows UBR

function Get-WindowsUBR
{
    $CurrentOSInfo = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $BuildUBR_CurrentOS = $($CurrentOSInfo.GetValue('CurrentBuild'))+"."+$($CurrentOSInfo.GetValue('UBR'))

    return $BuildUBR_CurrentOS
}

Window Build

function Get-WindowsBuild
{
    $CurrentOSInfo = Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    return $($CurrentOSInfo.GetValue('CurrentBuild'))
}

OSVersion

[System.Environment]::OSVersion

Function Get-WindowsVersion

function Get-WindowsVersion
{
    [CmdletBinding()]
    Param
    (
        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            ValueFromPipeline = $true
        )]
        [string[]]
        $ComputerName = $env:COMPUTERNAME
    )


    Begin
    {
        $Table = New-Object System.Data.DataTable
        $Table.Columns.AddRange(@("ComputerName", "Windows Edition", "Version", "OS Build"))
    }
    Process
    {
        Foreach ($Computer in $ComputerName)
        {
            $Code = {
                $ProductName = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName).ProductName
                Try
                {
                    $Version = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ReleaseID -ErrorAction Stop).ReleaseID
                }
                Catch
                {
                    $Version = "N/A"
                }
                $CurrentBuild = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name CurrentBuild).CurrentBuild
                $UBR = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name UBR).UBR
                $OSVersion = $CurrentBuild + "." + $UBR

                $TempTable = New-Object System.Data.DataTable
                $TempTable.Columns.AddRange(@("ComputerName", "Windows Edition", "Version", "OS Build"))
                [void]$TempTable.Rows.Add($env:COMPUTERNAME, $ProductName, $Version, $OSVersion)

                Return $TempTable
            }

            If ($Computer -eq $env:COMPUTERNAME)
            {
                $Result = Invoke-Command -ScriptBlock $Code
                [void]$Table.Rows.Add($Result.Computername, $Result.'Windows Edition', $Result.Version, $Result.'OS Build')
            }
            Else
            {
                Try
                {
                    $Result = Invoke-Command -ComputerName $Computer -ScriptBlock $Code -ErrorAction Stop
                    [void]$Table.Rows.Add($Result.Computername, $Result.'Windows Edition', $Result.Version, $Result.'OS Build')
                }
                Catch
                {
                    $_
                }
            }

        }
    }
    End
    {
        Return $Table
    }
}
# Set global variable for the operating system
Set-Variable -Name 'OperatingSystem' -Value (Get-CimInstance -ClassName Win32_OperatingSystem) -Scope Global -Force
# Determine the Windows version by build number
$BuildNumber = [int]$OperatingSystem.BuildNumber
$OperatingName = $OperatingSystem.Caption -replace '[^\p{L}\d\s]', ''
switch ($BuildNumber)
{
{ $_ -ge 26100 } # Check the specific build first
{
Set-Variable -Name 'WindowsVersion' -Value '11-24H2' -Scope Global -Force
}
{ $_ -ge 22000 -and $_ -lt 26100 } # General Windows 11 condition
{
Set-Variable -Name 'WindowsVersion' -Value '11' -Scope Global -Force
}
{ $_ -ge 19041 -and $_ -le 19048 } # Windows 10 condition
{
Set-Variable -Name 'WindowsVersion' -Value '10' -Scope Global -Force
}
default
{
Clear-Host
Write-Host "The current Windows version ($OperatingName) is not supported." -ForegroundColor Red
Pause -Prompt 'Press any key to continue...'
Write-Host 'The script has been canceled!'
exit
}
}
# Set global variable for the operating system
Set-Variable -Name 'OperatingSystem' -Value (Get-CimInstance -ClassName Win32_OperatingSystem) -Scope Global -Force
# Determine the Windows version
# Get the build number as an integer
$BuildNumber = [int]$OperatingSystem.BuildNumber
# Get and clean the operating system name
$OperatingName = $OperatingSystem.Caption -replace '[^\p{L}\d\s]', ''
switch ($BuildNumber)
{
{ $_ -ge 19041 -and $_ -le 19048 }
{
Set-Variable -Name 'WindowsVersion' -Value '10' -Scope Global -Force
}
{ $_ -ge 22000 }
{
Set-Variable -Name 'WindowsVersion' -Value '11' -Scope Global -Force
}
default
{
Clear-Host
Write-Host "The current Windows version Operating System {0} is not supported." -f $OperatingName -ForegroundColor Red
Pause -Prompt 'Press any key to continue...'
Write-Host 'The script has been canceled!'
exit
}
}

WMIObject vs CIMInstance

Using WMIObject

Get-WMIObject win32_bios

Using CIMInstance

Get-CIMInstance win32_bios
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment