Powershell Scripting Fundamentals

Variables

> $i = 1
> $string = "Hello World!"
> $this_is_a_variable = "test"
> Get-Date
Monday, November 2, 2020 6:43:59 PM
> $date = Get-Date
> Write-Host "Today is" $date
Today is 11/2/2020 6:44:40 PM

Data types

> $x = 4
> $string = "Hello World!"
> $date = Get-Date
> $x.GetType().Name
Int32
> $string.GetType().Name
String
> $date.GetType().Name
DateTime

Casting variables

> $number = "4"
> $number.GetType().Name
String
> $number + 2
42
> ($number + 2).GetType().Name
String
> $int_number = [int]$number
> $int_number.GetType().Name
Int32
> $int_number + 2
6

Automatic variables

Get-ChildItem -Path C:\ -Directory -Force -ErrorAction
SilentlyContinue | ForEach-Object {
Write-Host $_.FullName
}

Environment variables

> $env:PSModulePath
C:\Users\PSSec\Documents\WindowsPowerShell\Modules;C:\Program Files\
WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\
Modules

Reserved words and language keywords

> Get-Help about_reserved_words
> Get-Help about_Language_Keywords

To learn more about a certain language keyword, you can use Get-Help:

> Get-Help break

Some reserved words (such as if, for, foreach, and while) have their own help articles. To read them, add about_ as a prefix:

> Get-Help about_If

Variable scope

$script:ModuleRoot = $PSScriptRoot
# Sets the scope of the variable $ModuleRoot to script

Scope modifier

Using the scope modifier, you can configure the scope in which your variables will be available. Here is an overview of the most commonly used scope modifiers:

  • global: Sets the scope to global. This scope is effective when PowerShell starts or if you create a new session.

  • local: This is the current scope. The local scope can be the global scope, the script scope, or any other scope.

  • script: This scope is only effective within the script that sets this scope. It can be very useful if you want to set a variable only within a module that should not be available after the function was called.

function Set-Variables {
    $local_variable = "Hello, I'm a local variable."
    $script:script_variable = "Hello, I'm a script variable."
    $global:global_variable = "Hello, I'm a global variable."

    Write-Host "##########################################################"
    Write-Host "This is how our variables look in the function, where we defined the variables - in a LOCAL SCOPE:"
    Write-Host "  Local: " $local_variable
    Write-Host "  Script: " $script_variable
    Write-Host "  Global: " $global_variable    
}

Set-Variables

Write-Host "##########################################################"
Write-Host "This is how our variables look in the same script - in a SCRIPT SCOPE:"
Write-Host "  Local: " $local_variable
Write-Host "  Script: " $script_variable
Write-Host "  Global: " $global_variable

Operators

Arithmetic operators

> $a = 3; $b = 5; $result = $a + $b
> $result
8
> $a = 3; $b = 5; $result = $b - $a
> $result
2
> $a = 3; $b = 5; $result = $a * $b
> $result
15
> $a = 12; $b = 4; $result = $a / $b
> $result
3
> 7%2
1
> 8%2
0
> 7%4
3
> $a = 3; $b = 5; $c = 2
> $result = ($a + $b) * $c
> $result
16

Comparison operators

> $a = 1; $b = 1; $a -eq $b
True
> $a = 1; $b = 2; $a -eq $b
False
> "A", "B", "C", "D" -lt "C"
A
B
> "A","B","C" -eq "A"
A
> $a = 1; $b = 2; $a -ne $b
True
> $a = 1; $b = 1; $a -ne $b
False
> "Hello World!" -ne $null
True
> "A","B","C" -ne "A"
B
C
> $a = 1; $b = 2; $a -le $b
True
> $a = 2; $b = 2; $a -le $b
True
> $a = 3; $b = 2; $a -le $b
False
> "A","B","C" -le "A"
A
> $a = 1; $b = 2; $a -ge $b
False
> $a = 2; $b = 2; $a -ge $b
True
> $a = 3; $b = 2; $a -ge $b
True
> "A","B","C" -ge "A"
A
B
C
> $a = 1; $b = 2; $a -lt $b
True
> $a = 2; $b = 2; $a -lt $b
False
> $a = 3; $b = 2; $a -lt $b
False
> "A","B","C" -lt "A" # results in no output
> $a = 1; $b = 2; $a -gt $b
False
> $a = 2; $b = 2; $a -gt $b
False
> $a = 3; $b = 2; $a -gt $b
True
> "A","B","C" -gt "A"
B
C
> "PowerShell" -like "*owers*"
True
> "PowerShell", "Dog", "Cat", "Guinea Pig" -like "*owers*"
PowerShell
> "PowerShell" -notlike "*owers*"
False
> "PowerShell", "Dog", "Cat", "Guinea Pig" -notlike "*owers*"
Dog
Cat
Guinea Pig
> "PowerShell scripting and automation for Cybersecurity" -match
"shell\s*(\d)"
False
> "Cybersecurity scripting in PowerShell 7.3" -match "shell\
s*(\d)"
True
> "Cybersecurity scripting in PowerShell 7.3" -notmatch "^Cyb"
False
> "PowerShell scripting and automation for Cybersecurity"
-notmatch "^Cyb"
True

Assignment operators

> $a = 1; $a
1
> $a = 1; $a += 2; $a
3
> $a
3
> $a -= 1; $a
2
> $a
2
> $a *= 3; $a
6
> $a
6
> $a /= 2; $a
3
> $a
3
> $a %= 2; $a
1
> $a= 1; $a++; $a
2
> $a = 10; $a--; $a
9

Logical operators

> $a = 1; $b = 2
> if (($a -eq 1) -and ($b -eq 2)) {Write-Host "Condition is
true!"}
Condition is true!
> $a = 2; $b = 2
> if (($a -eq 1) -or ($b -eq 2)) {Write-Host "Condition is
true!"}
Condition is true!
$path = $env:TEMP + "\TestDirectory"
if( -not (Test-Path -Path $path )) {
New-Item -ItemType directory -Path $path
}
if (!(Test-Path -Path $path)) {
New-Item -ItemType directory -Path $path
}
> $a = 1; $b = 2; ($a -eq 1) -xor ($b -eq 1)
True
> ($a -eq 1) -xor ($b -eq 2)
False
> ($a -eq 2) -xor ($b -eq 1)
False

Control structures

A control structure is some kind of programmatic logic that assesses conditions and variables and decides which defined action will be taken if a certain condition is met

If/elseif/else

if (<condition>)
{
<action>
}
elseif (<condition 2>)
{
<action 2>
}
...
else
{
<action 3>
}
> if (1+2 -eq 3) { Write-Host "Good job!" }
Good job!
> if (1+2 -eq 5) { Write-Host "Something is terribly wrong!" }
# returns no Output
$color = "green"
if ($color -eq "blue") {
Write-Host "The color is blue!"
}
elseif ($color -eq "green"){
Write-Host "The color is green!"
}
# returns: The color is green!
$color = "red"
if ($color -eq "blue") {
Write-Host "The color is blue!"
}
elseif ($color -eq "green"){
Write-Host "The color is green!"
}
else {
Write-Host "That is also a very beautiful color!"
}
# returns: That is also a very beautiful color!

Switch

switch (<value to test>) {
<condition 1> {<action 1>}
<condition 2> {<action 2>}
<condition 3> {<action 3>}
...
default {}
}
$color = Read-Host "What is your favorite color?"
switch ($color) {
    "blue" { Write-Host "I'm BLUE, Da ba dee da ba di..." }
    "yellow" { Write-Host "YELLOW is the color of my true love's
        hair." }
    "red" { Write-Host "Roxanne, you don't have to put on the RED
        light..." }
    "purple" { Write-Host "PURPLE rain, purple rain!" }
    "black" { Write-Host "Lady in BLACK... she came to me one
        morning, one lonely Sunday morning..." }
    default { Write-Host "The color is not in this list." }
}
switch -Regex ($userInput) {
    "^[A-Z]" { "User input starts with a letter." }
    "^[0-9]" { "User input starts with a number." }
    default { "User input doesn't start with a letter or number." }
}

Loops and iterations

ForEach-Object

> $path = $env:TEMP + "\baselines"
> Get-ChildItem -Path $path | ForEach-Object {Write-Host $_}
Office365-ProPlus-Sept2019-FINAL.zip
Windows 10 Version 1507 Security Baseline.zip
Windows 10 Version 1607 and Windows Server 2016 Security Baseline.zip
Windows 10 Version 1803 Security Baseline.zip
Windows 10 Version 1809 and Windows Server 2019 Security Baseline.zip
Windows 10 Version 1903 and Windows Server Version 1903 Security
Baseline - Sept2019Update.zip
Windows 10 Version 1909 and Windows Server Version 1909 Security
Baseline.zip
Windows 10 Version 2004 and Windows Server Version 2004 Security

Foreach

$path = $env:TEMP + "\baselines"
$items = Get-ChildItem -Path $path
foreach ($file in $items) {
Write-Host $file
}
$path = $env:TEMP + "\baselines"
$items = Get-ChildItem -Path $path
$items.foreach({
Write-Host "Current item: $_"
})

while

while(($input = Read-Host -Prompt "Choose a command (type in 'help'
for an overview)") -ne "quit"){
    switch ($input) {
        "hello" {Write-Host "Hello World!"}
        "color" {Write-Host "What's your favorite color?"}
        "help" {Write-Host "Options: 'hello', 'color', 'help' 'quit'"}
    }
}

for

> for ($i=1; $i -le 5; $i++) {Write-Host "i: $i"}
i: 1
i: 2
i: 3
i: 4
i: 5

do-until/do-while

do{
<action>
}
<while/until><condition>

break

> for ($i=1; $i -le 10; $i++) {
Write-Host "i: $i"
if ($i -eq 3) {break}
}
i: 1
i: 2
i: 3

continue

> for ($i=1; $i -le 10; $i++) {
if (($i % 2) -ne 0) {continue}
Write-Host "i: $i"
}
i: 2
i: 4
i: 6
i: 8
i: 10

Naming conventions

Cmdlets and functions both follow the schema verb-noun, such as Get-Help or Stop-Process. So, if you write your own functions or cmdlets, make sure to follow the name guidelines and recommendations.

Finding the approved verbs

Get-Verb | Sort-Object Verb
> Get-Verb re*
Verb Group
---- -----
Redo Common
Remove Common
Rename Common
Reset Common
Resize Common
Restore Data
Register Lifecycle
Request Lifecycle
Restart Lifecycle
Resume Lifecycle
Repair Diagnostic
Resolve Diagnostic
Read Communications
Receive Communications
Revoke Security
> Get-Verb | Where-Object Group -eq Security

Verb Group
---- -----
Block Security
Grant Security
Protect Security
Revoke Security
Unblock Security
Unprotect Security

PowerShell profiles

PowerShell profiles are configuration files that allow you to personalize your PowerShell environment. These profiles can be used to customize the behavior and environment of PowerShell sessions. They are scripts that are executed when a PowerShell session is started, allowing users to set variables, define functions, create aliases, and more.

  • All Users, All Hosts ($profile.AllUsersAllHosts): This profile applies to all users for all PowerShell hosts.

  • All Users, Current Host ($profile.AllUsersCurrentHost): This profile applies to all users for the current PowerShell host.

  • Current User, All Hosts ($profile.CurrentUserAllHosts): This profile applies to the current user for all PowerShell hosts.

  • Current User, Current Host ($profile.CurrentUserCurrentHost): This profile applies only to the current user and the current PowerShell host.

To access the file path of one particular profile, such as the one for CurrentUserCurrentHost, you can use the variable that is defined in $profile.CurrentUserCurrentHost:

> $profile.CurrentUserCurrentHost
C:\Users\pssecuser\Documents\PowerShell\Microsoft.PowerShell_profile.
ps1

Use the following code snippet to check whether the file already exists; if it does not yet, the file is created:

if ( !( Test-Path $profile.CurrentUserCurrentHost ) ) {
New-Item -ItemType File -Path $profile.CurrentUserCurrentHost
}

Finally, add the commands, functions, or aliases to the user profile:

> Add-Content -Path $profile -Value “New-Alias -Name Get-Ip -Value
‘ipconfig.exe’”

Understanding PSDrives in PowerShell

PowerShell includes a feature called PowerShell drives (PSDrives). PSDrives in PowerShell are similar to filesystem drives in Windows, but instead of accessing files and folders, you use PSDrives to access a variety of data stores. These data stores can include directories, registry keys, and other data sources, which can be accessed through a consistent and familiar interface.

> Get-ChildItem Env:\*path*

There are several built-in PSDrives in PowerShell, including the following:

  • Alias: Provides access to PowerShell aliases

  • Environment: Provides access to environment variables

  • Function: Provides access to PowerShell functions

  • Variable: Provides access to PowerShell variables

  • Cert: Provides access to certificates in the Windows certificate store

  • Cert:\CurrentUser: Provides access to certificates in the current user’s certificate store

  • Cert:\LocalMachine: Provides access to certificates in the local machine’s certificate store

  • WSMan: Provides access to Windows Remote Management (WinRM) configuration data

  • C: and D: (and other drive letters): Used to access the filesystem, just like in Windows Explorer

  • HKCU: Provides access to the HKEY_CURRENT_USER registry hive

  • HKLM: Provides access to the HKEY_LOCAL_MACHINE registry hive

Making your code reusable

In this section, we will explore the concept of making your code reusable in PowerShell. Reusability is an important aspect of coding that allows you to create a function, cmdlet, or module once and use it multiple times without having to rewrite the same code again and again. Through this, you can save time and effort in the long run.

Cmdlets

A cmdlet (pronounced as commandlet) is a type of PowerShell command that performs a specific task and can be written in C# or in another .NET language.

> Get-Command new-item
CommandType     Name        Version     Source
-----------     ----        -------     ------
Cmdlet         New-Item     3.1.0.0     Microsoft.PowerShell.Management

To find out all cmdlets that are currently installed on the machine you are using, you can leverage Get-Command with the CommandType parameter:

Get-Command -CommandType Cmdlet

Functions

function Verb-Noun {
<#
<Optional help text>
#>
param (
[data_type]$Parameter
)
<...Code: Function Logic...>
}

Once the function is loaded into the session, it needs to be called so that it will be executed:

Verb-Noun -Parameter "test"

Parameters

function Invoke-Greeting {
    param (
    [string]$Name
    )
    Write-Output "Hello $Name!"
}
> Invoke-Greeting -Name "Fares"
Hello Fares!
> Invoke-Greeting
Hello !

cmdletbinding

cmdletbinding is a feature in PowerShell that allows you to add common parameters (such as -Verbose, -Debug, or -ErrorAction) to your functions and cmdlets without defining them yourself. This can make your code more consistent with other PowerShell commands and easier to use for users.

One way to use cmdletbinding is to declare a parameter as mandatory, positional, or in a parameter set, which can automatically turn your function into a cmdlet with additional common parameters. For example, if you want to make the -Name parameter mandatory in your function, you can add [Parameter(Mandatory)] before the parameter definition, like this:

function Invoke-Greeting {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory)]
        $Name
    )
    Write-Output "Hello $Name!"
}

This will automatically add the [] section to the output of Get-Command, and you will see all the common parameters that are also available in many other cmdlets, such as Verbose, Debug, ErrorAction, and others.

SupportsShouldProcess

If a function makes changes, you can use SupportsShouldProcess to add an additional layer of protection to your function. By adding [CmdletBinding(SupportsShouldProcess)], you can enable the -WhatIf and -Confirm parameters in your function, which help users understand the effect of their actions before executing the function. To use SupportsShouldProcess effectively, you will also need to call ShouldProcess() for each item being processed. Here’s an example of what your code could look like:

function Invoke-Greeting {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        $Name
         )
    foreach ($item in $Name) {
        if ($PSCmdlet.ShouldProcess($item)) {
            Write-Output "Hello $item!"
        }
    }
}

With this code, the function can be executed with the -Confirm parameter to prompt the user for confirmation before processing each item, or with the -WhatIf parameter to display a list of changes that would be made without actually processing the items.

> Get-Command -Name Invoke-Greeting -Syntax
Invoke-Greeting [[-Name] <Object>] [-WhatIf] [-Confirm]
[<CommonParameters>]

Accepting input via the pipeline

It is also possible to configure parameters to accept user input to use it in our code. In addition to accepting input from the user, we can also accept input from the pipeline. This can be done in two ways: by value or by property name.

function Invoke-Greeting {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline,
        ValueFromPipelineByPropertyName)]
        [string]$Name
    )
    process {
        Write-Output "Hello $Name!"
        }
}

You can now pass input by value to this function, as shown in the following example:

> "Alice","Bob" | Invoke-Greeting
Hello Alice!
Hello Bob!

But it also works to pass input by property name, as the following code snippet demonstrates:

> [pscustomobject]@{Name = "Miriam"} | Invoke-Greeting
Hello Miriam!

Comment-based help

Writing comment-based help for your functions is crucial; others might reuse your function or if you want to adjust or reuse the function yourself some months after you wrote it, having good comment- based help will simplify the usage:

<#
.SYNOPSIS
<Describe the function shortly.>
.DESCRIPTION
<More detailed description of the function.>
.PARAMETER Name
<Add a section to describe each parameter, if your function has one or
more parameters.>
.EXAMPLE
<Example how to call the funtion>
<Describes what happens if the example call is run.>
#>

Error handling

If you are not sure whether your command will succeed, use try and catch:

try {
    New-PSSession -ComputerName $Computer -ErrorAction Stop
}
catch {
    Write-Warning -Message "Couldn't connect to Computer: $Computer"
}

Aliases

An alias is some kind of a nickname for a PowerShell command, an alternate name. You can set aliases to make your daily work easier – for example, if you are repeatedly working with the same long and complicated command, setting an alias and using it instead will ease your daily work.

PS C:\> cd 'C:\tmp\PSSec\'
PS C:\tmp\PS Sec>
PS C:\> Set-Location 'C:\tmp\PSSec\'
PS C:\tmp\PS Sec>

To see all available cmdlets that have the word Alias in their name, you can leverage Get-Command:

Next, let’s have a closer look at how to work with aliases, using the Get-Alias, New-Alias, Set-Alias, Export-Alias, and Import-Alias cmdlets.

Get-Alias

To see all aliases that are currently configured on the computer you are working on, use the Get-Alias cmdlet:

New-Alias

You can use New-Alias to create a new alias within the current PowerShell session:

> New-Alias -Name Get-Ip -Value ipconfig
> Get-Ip
Windows IP Configuration
Ethernet adapter Ethernet:
Connection-specific DNS Suffix . : mshome.net
IPv4 Address. . . . . . . . . . . : 10.10.1.10
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.10.1.1

Set-Alias

Set-Alias can be used to either create or change an alias.

So if you want to change, for example, the content of the formerly created Get-Ip alias to Get-NetIPAddress, you would run the following command:

> Set-Alias -Name Get-Ip -Value Get-NetIPAddress

Export-Alias

Export one or more aliases with Export-Alias – either as a .csv file or as a script:

Export-Alias -Path "alias.csv"

Using this command, we first export all aliases to a .csv file:

Export-Alias -Path "alias.ps1" -As Script

The -As Script parameter allows you to execute all currently available aliases as a script that can be executed:

Export-Alias -Path "alias.ps1" -Name Get-Ip -As Script

Import-Alias

You can use Import-Alias to import aliases that were exported as .csv:

> Set-Alias -Name Get-Ip -Value Get-Iponfig
> Export-Alias -Name Get-Ip -Path Get-Ip_alias.csv

Import the file to make the alias available in your current session:

> Import-Alias -Path .\Get-Ip_alias.csv
> Get-Ip
Windows IP Configuration
Ethernet adapter Ethernet:
Connection-specific DNS Suffix . : mshome.net
IPv4 Address. . . . . . . . . . . : 10.10.1.10
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 10.10.1.1

Modules

Modules are a collection of PowerShell commands and functions that can be easily shipped and installed on other systems. They are a great way to enrich your sessions with other functionalities.

All modules that are installed on the system can be found in one of the PSModulePath folders, which are part of the Env:\ PSDrive:

> Get-Item -Path Env:\PSModulePath
Name Value
---- -----
PSModulePath C:\Users\PSSec\Documents\WindowsPowerShell\Modules;
C:\Program Files\WindowsPowerShell\Modules;
C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules

Working with modules

To use a module efficiently, the following sections will help you to make the module available, to find out how to work with it, and to finally remove or unload it.

Finding and installing modules

> Find-Module -Name EventList

Once you have found the desired module, you can download and install it to your local system using Install-Module:

> Install-Module <modulename>

If you have already installed a module for which a newer version exists, update it with Update- Module:

> Update-Module <modulename> -Force

To see which repositories are available on your system, use the following:

> Get-PSRepository

To find out which modules are already available in the current session, you can use Get-Module:

> Get-Module
> Get-Module -ListAvailable

Find out which commands are available by using Get-Command:

> Get-Command -Module <modulename>

And if you want to know more about the usage of a command that is available in a module, you can use Get-Help. You can see how important it is to write proper help pages for your function:

Get-Help -Full Get-WindowsUpdateLog

If you have, for example, an old version loaded in your current session and you want to unload it, Remove-Module unloads the current module from your session:

> Remove-Module <modulename>

Creating your own modules

When working more intensively with PowerShell modules, you might also come across many different files, such as files that end with .psm1, .psd1, .ps1xml, or .dll, help files, localization files, and many others.

.psm1

The .psm1 file contains the scripting logic that your module should provide. Of course, you can also use it to import other functions within your module.

.psd1 – the module manifest

The .psd1 file is the manifest of your module. If you only create a PowerShell script module, this file is not mandatory, but it allows you to control your module functions and include information about the module.

Creating a basic PowerShell module can be as simple as writing a script containing one or more functions, and saving it with a .psm1 file extension.

First, we define the path where the module should be saved in the $path variable and create the MyModule folder if it does not exist yet. We then use the New-ModuleManifest cmdlet to create a new module manifest file named MyModule.psd1 in the MyModule folder. The -RootModule parameter specifies the name of the PowerShell module file, which is MyModule.psm1.

Using the Set-Content cmdlet, we create the MyModule.psm1 file and define the Invoke- Greeting function, which we wrote earlier in this chapter:

$path = $env:TEMP + "\MyModule\"
if (!(Test-Path -Path $path)) {
    New-Item -ItemType directory -Path $path
}
New-ModuleManifest -Path $path\MyModule.psd1 -RootModule MyModule.psm1
Set-Content -Path $path\MyModule.psm1 -Value {
    function Invoke-Greeting {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory=$true)]
            [string]$Name
        )
        "Hello, $Name!"
    }
}
> Import-Module MyModule
> Invoke-Greeting -Name "Fares"

Last updated