PowerShell regex use case

PowerShell regex use case

If you’ve ever needed to extract specific information from a log file or validate user input in a PowerShell script, you’ve probably wished for a magic tool that could do the heavy lifting. Well, that’s exactly where regular expressions (regex) come in.

Regex is one of those tools that feels intimidating at first, but once you get the hang of it, you’ll start seeing patterns everywhere. It’s like suddenly being able to read the Matrix, except instead of dodging bullets, you’re filtering out bad email addresses or pulling MAC addresses from a system log.

Why Use Regex in PowerShell?

PowerShell has built-in support for regex, making it an incredibly powerful tool for system administrators, developers, and anyone who works with structured or unstructured text. Whether you’re:

  • Validating user input (like email addresses in a script)
  • Extracting important data from logs (like MAC or IP addresses)
  • Searching for patterns in massive amounts of text

Regex allows you to do all of this with just a few carefully crafted expressions. It’s like having a Swiss Army knife for text processing.

What We’ll Cover in This Guide

In this post, we’ll walk through three practical PowerShell regex use cases:

  1. Email Addresses – How to validate email input and extract emails from a document
  2. MAC Addresses – How to validate MAC addresses and find them in logs
  3. IP Addresses – How to check if an IP is valid and pull all IPs from a file

Before we dive into these examples, we’ll go over some regex basics, including common syntax and flags that make regex so powerful. By the end, you’ll not only understand how regex works in PowerShell but also feel confident using it in your own projects.

Let’s get started!

Understanding Regex Flags and Syntax

Regular expressions might look like a confusing mess of symbols at first, kind of like trying to understand a cat’s behavior. One minute it’s purring in your lap, the next it’s knocking your coffee off the table. But once you start recognizing the patterns—like how a tail flick means back away, human—it all starts to make sense.

Regex is the same way. At first glance, it looks like a secret code of slashes, dots, and brackets. But once you learn the building blocks, you start seeing patterns everywhere, and suddenly, text manipulation in PowerShell becomes effortless. PowerShell has native support for regex, meaning you can use it to search, validate, and extract information with just a few well-placed symbols—kind of like bribing a cat with treats to do what you want.

Character Classes: Defining What to Match

Character classes allow you to specify what kind of characters should match. Instead of listing every possibility, regex provides shorthand notations.

Common Character Classes in PowerShell Regex

Character ClassDescriptionExample Match
\dMatches any digit (0-9)123 in abc123
\wMatches any word character (A-Z, a-z, 0-9, _)hello in hello_123
\sMatches any whitespace (space, tab, newline)The space in "Hello World"
.Matches any character (except newline)H or ! in "Hi!"

Example in Powershell:

"This is a test 123" -match "\d+"  # Matches "123"

Quantifiers: Controlling Repetitions

Quantifiers define how many times a pattern should repeat.

Common Quantifiers

QuantifierDescriptionExample Match
*Matches 0 or more timesaaa in "aaaaa"
+Matches 1 or more timesabc in "abc"
?Matches 0 or 1 time (optional)a in "a" or empty string
{n}Matches exactly n times333 in "333"
{n,}Matches at least n times111 in "11111"
{n,m}Matches between n and m times55 in "5555" (if {2,3})

Example in PowerShell:

"This is 55555" -match "\d{2,3}"  # Matches "555"

Anchors: Defining Position in Text

Anchors don’t match actual characters but instead define where a match should occur.

Common Anchors

AnchorDescriptionExample Match
^Matches the start of a stringHello in "Hello world"
$Matches the end of a stringworld in "Hello world"
\bMatches a word boundarycat in "cat dog" but not "scatter"

Example in PowerShell:

"This is a test" -match "^This"  # Matches "This"

Escaping Special Characters

Some characters in regex have special meanings (like . or *). If you want to match them literally, you need to escape them with a backslash \.

Common Special Characters That Need Escaping

  • . (dot) → Matches any character, so use \. to match a literal dot.
  • * (asterisk) → Use \* to match an actual asterisk.
  • ? (question mark) → Use \? to match a literal question mark.

Example in PowerShell:

"This is version 1.0.1" -match "1\.0\.1"  # Matches "1.0.1"

How to Use Regex in PowerShell

PowerShell provides multiple ways to work with regex:

  1. -match Operator – Checks if a string matches a pattern.
  2. -replace Operator – Replaces matched patterns in a string.
  3. [regex]::matches() – Extracts all matches from a string.

Example of Finding All Matches in a String:

$text = "Emails: test@example.com, admin@company.net"
$pattern = "[\w\.-]+@[\w\.-]+\.\w+"
$matches = [regex]::Matches($text, $pattern)

$matches.Value  # Outputs: test@example.com, admin@company.net

Extracting and Validating Email Addresses with PowerShell

When working with PowerShell scripts, validating user input is crucial—especially when dealing with email addresses. You don’t want users submitting "notanemail@oops" or "hello@.com" and breaking your workflow. Thankfully, regex makes it easy to verify if an email address is properly formatted and even extract all emails from a document.

Before we get into validating and extracting emails, let’s break down the regex pattern that makes it all work.

Breaking Down the Email Regex

Email validation might seem simple at first—just look for an “@” symbol, right? But things get complicated fast. A valid email address follows these rules:

  1. A username section, which can include letters, numbers, dots, dashes, and underscores.
  2. An “@” symbol separating the username from the domain.
  3. A domain name that includes letters and numbers.
  4. A top-level domain (TLD) like .com, .net, .org, etc.

A regex pattern that matches most valid email addresses looks like this:

[\w\.-]+@[\w\.-]+\.\w+

Let’s break it down piece by piece:

Username Section: [\w\.-]+

  • \w → Matches letters, numbers, and underscores (a-z, A-Z, 0-9, _).
  • . and - → Allows dots and dashes in the username.
  • + → Ensures one or more of these characters exist.

“@” Symbol: @

  • The @ is a literal character—every valid email must have it.

Domain Name: [\w\.-]+

  • Just like the username, the domain allows letters, numbers, dots, and dashes.

Top-Level Domain (TLD): \.\w+

  • \. → Matches a literal dot before the TLD.
  • \w+ → Ensures at least one letter (like .com or .net).

Example Matches:

  • user@example.com
  • john.doe123@company.co.uk
  • admin-test@my-site.net

Invalid Matches:

  • @example.com (No username)
  • user@.com (Invalid domain name)
  • user@domain (No TLD)

This regex is great for general email validation, but in PowerShell, we need to apply it properly. Next, we’ll use this pattern in a ValidateSet to enforce correct email input in scripts.

Using Regex for PowerShell ValidateSet

PowerShell has built-in ways to enforce valid input in scripts, and one way to do this is by using regex inside a function. The function below checks if an email matches our regex pattern. If it’s valid, it returns $true; if not, it gives a reason why.

PowerShell Email Validation Function

function Test-EmailAddress {
    param (
        [string]$Email
    )

    $pattern = "^[\w\.-]+@[\w\.-]+\.\w+$"

    if (-not $Email) {
        return "False - No email provided."
    }

    if ($Email -match $pattern) {
        return "True"
    }
    
    # Now let's figure out why it failed  
    if ($Email -notmatch "@") {
        return "False - Missing '@' symbol."
    }
    
    if ($Email -match "@\." -or $Email -match "@$") {
        return "False - Invalid placement of '@' or '.'"
    }

    if ($Email -notmatch "\.\w+$") {
        return "False - Missing top-level domain (e.g., .com, .net)."
    }

    return "False - Invalid email format."
}

How It Works

  1. Checks if an email is provided – If the input is empty, it immediately returns a failure message.
  2. Validates using regex – If the input matches the regex pattern, it returns True.
  3. Identifies why the email is invalid – It checks for common issues like missing @, misplaced dots, or a missing TLD.

This function is useful for scripts that require valid emails before proceeding. Next, we’ll explore how to extract all emails from a document using regex!

Finding All Email Addresses in a Document

If you’re working with logs, reports, or any large text files, regex is your best friend for extracting structured data like email addresses. Instead of manually scanning through lines of text, PowerShell can do the heavy lifting in seconds.

Here’s a script that:

  1. Reads a file line by line.
  2. Uses regex to find all email addresses.
  3. Outputs the results to the console (or optionally saves them to another file).

PowerShell Script to Extract Emails from a File

function Get-EmailAddressesFromFile {
    param (
        [string]$FilePath
    )

    if (-not (Test-Path $FilePath)) {
        Write-Host "Error: File not found at path: $FilePath"
        return
    }

    $pattern = "[\w\.-]+@[\w\.-]+\.\w+"
    $emails = @()

    Get-Content $FilePath | ForEach-Object {
        $matches = [regex]::Matches($_, $pattern)
        if ($matches.Count -gt 0) {
            $emails += $matches.Value
        }
    }

    if ($emails.Count -eq 0) {
        Write-Host "No email addresses found in the file."
    } else {
        Write-Host "Found $($emails.Count) email addresses:"
        $emails | Sort-Object -Unique
    }
}

# **Example Usage**
Get-EmailAddressesFromFile -FilePath "C:\path\to\your\file.txt"

How It Works

  1. Checks if the file exists – Avoids errors if the file path is incorrect.
  2. Reads the file line by line – Prevents loading large files into memory at once.
  3. Uses regex to find emails – Looks for matches in each line.
  4. Stores and displays unique results – Avoids duplicates and outputs all found emails.

Example File (C:\path\to\your\file.txt)

Hello John, please contact us at support@example.com.
You can also reach admin@company.net for further assistance.
But we dont want to see a snail@the white house. 
However, we might @ your friend .com
How about a cheesecake@cheesecakefactory.com?

Script Output

Found 3 email addresses:
admin@company.net
cheesecake@cheesecakefactory.com
support@example.com

This script is super useful for extracting emails from logs, reports, or even messy text files. Next up, we’ll apply these same techniques to MAC addresses!

Extracting and Validating MAC Addresses with PowerShell

MAC addresses (Media Access Control addresses) are unique hardware identifiers assigned to network interfaces. If you’re dealing with network logs, device configurations, or security audits, you may need to validate or extract MAC addresses.

A valid MAC address follows one of these formats:

  • Colon-separated: 00:1A:2B:3C:4D:5E
  • Hyphen-separated: 00-1A-2B-3C-4D-5E
  • No separator: 001A2B3C4D5E

But Cisco devices often use a dot-separated format:

  • Cisco-style: 001A.2B3C.4D5E

Now, let’s break down the regex pattern that will help us match MAC addresses.

Breaking Down the MAC Address Regex

MAC addresses consist of six groups of two hexadecimal digits (0-9 and A-F), separated by colons, hyphens, or nothing at all. Here’s a regex pattern that matches all common formats:

([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2}|([A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4}

Breaking It Down

  1. First Half: Standard MAC Formats
    • ([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2} → Matches colon, hyphen, or no separator formats.
  2. Second Half: Cisco’s Dot-Format
    • ([A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4} → Matches two groups of four hex digits separated by dots.

Example Matches:

  • 00:1A:2B:3C:4D:5E
  • 00-1A-2B-3C-4D-5E
  • 001A2B3C4D5E
  • 001A.2B3C.4D5E (Cisco format!)

Invalid Matches:

  • 00:1A:2B:3C:4D (Only five pairs)
  • 00:1G:2B:3C:4D:5E (Invalid hex digit G)
  • 00::1A:2B:3C:4D:5E (Double colon is not valid)

Using Regex for PowerShell ValidateSet (Including Cisco Format)

We’ll now create a PowerShell function that checks if a given MAC address is valid. If it’s valid, it returns True; if not, it returns False with a reason why.

Updated PowerShell MAC Address Validation Function

function Test-MacAddress {
    param (
        [string]$MacAddress
    )

    $pattern = "^([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2}$|^([A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4}$"

    if (-not $MacAddress) {
        return "False - No MAC address provided."
    }

    if ($MacAddress -match $pattern) {
        return "True"
    }

    # Identify why it failed
    if ($MacAddress -notmatch "^[A-Fa-f0-9]+$" -and $MacAddress -notmatch "[:-\.]") {
        return "False - Contains invalid characters."
    }

    if ($MacAddress -notmatch "([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2}" -and $MacAddress -notmatch "([A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4}") {
        return "False - Incorrect format. Should be XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, XXXXXXXX, or XXXX.XXXX.XXXX (Cisco)."
    }

    return "False - Invalid MAC address format."
}

How It Works

  1. Checks if a MAC address is provided – Ensures input isn’t empty.
  2. Uses regex for validation – If the input matches the pattern, it’s valid.
  3. Identifies specific errors – Helps users understand why an input is invalid.

Now that we can validate a single MAC address, let’s move on to extracting all MAC addresses from a file!

Finding All MAC Addresses in a Document

This next script scans a file and extracts all MAC addresses using regex. It works just like our email extraction script, but with a MAC address pattern.

PowerShell Script to Extract MAC Addresses from a File

function Get-MacAddressesFromFile {
    param (
        [string]$FilePath
    )

    if (-not (Test-Path $FilePath)) {
        Write-Host "Error: File not found at path: $FilePath"
        return
    }

    $pattern = "([A-Fa-f0-9]{2}[:-]?){5}[A-Fa-f0-9]{2}|([A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4}"
    $macAddresses = @()

    Get-Content $FilePath | ForEach-Object {
        $matches = [regex]::Matches($_, $pattern)
        if ($matches.Count -gt 0) {
            $macAddresses += $matches.Value
        }
    }

    if ($macAddresses.Count -eq 0) {
        Write-Host "No MAC addresses found in the file."
    } else {
        Write-Host "Found $($macAddresses.Count) MAC addresses:"
        $macAddresses | Sort-Object -Unique
    }
}

# **Example Usage**
Get-MacAddressesFromFile -FilePath "C:\path\to\your\file.txt"

How It Works

  1. Checks if the file exists – Prevents errors if the file path is wrong.
  2. Reads the file line by line – Efficient for large files.
  3. Uses regex to extract MAC addresses – Searches each line for matches.
  4. Stores and displays unique results – Removes duplicate addresses for cleaner output.

Example File (C:\path\to\your\file.txt)

Device 1: 00:1A:2B:3C:4D:5E  
Device 2: 00-1A-2B-3C-4D-5E  
Cisco Router: 001A.2B3C.4D5E  
Error Log: Invalid MAC -> 00:1A:2B:3G:4D:5E  

Script Output

Found 3 MAC addresses:
00:1A:2B:3C:4D:5E
00-1A-2B-3C-4D-5E
001A.2B3C.4D5E

This script is super useful for network admins who need to extract MAC addresses from logs or reports. Next up, we’ll do the same for IP addresses!

Extracting and Validating IP Addresses with PowerShell

IP addresses are everywhere—logs, configs, audit reports—you name it. If you’re working with networking or security, you’ll often need to validate or extract IPs from text files.

We’re going to cover IPv4 and IPv6, because while IPv4 is still dominant, IPv6 is becoming more common.

What Makes an IP Address Valid?

IPv4 Format

  • Consists of four octets (groups of numbers) separated by dots: 192.168.1.1
  • Each octet must be between 0 and 255

IPv6 Format

  • Consists of eight groups of hexadecimal numbers (0-9, A-F), separated by colons: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  • Can contain compressed notation (e.g., :: represents consecutive zero blocks)

Breaking Down the IP Address Regex

To match both IPv4 and IPv6, we’ll use two separate regex patterns.

IPv4 Regex Pattern

\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b

Breaking It Down

  • 25[0-5] → Matches 250-255
  • 2[0-4][0-9] → Matches 200-249
  • 1[0-9]{2} → Matches 100-199
  • [1-9]?[0-9] → Matches 0-99
  • \. → Ensures each octet is separated by a dot
  • {3} → Ensures exactly three dots appear

IPv6 Regex Pattern

\b([A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b

Breaking It Down

  • [A-Fa-f0-9]{1,4} → Matches hexadecimal numbers (1-4 digits)
  • : → Ensures each section is separated by a colon
  • {7} → Ensures exactly seven colons appear

⚠️ This regex does not handle compressed IPv6 addresses (::), but we’ll take care of that in PowerShell logic!

Using Regex for PowerShell ValidateSet

Now, let’s create a PowerShell function to validate both IPv4 and IPv6 addresses.

PowerShell IP Address Validation Function

function Test-IPAddress {
    param (
        [string]$IPAddress
    )

    $ipv4Pattern = "^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$"
    $ipv6Pattern = "^([A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}$"

    if (-not $IPAddress) {
        return "False - No IP address provided."
    }

    if ($IPAddress -match $ipv4Pattern) {
        return "True (IPv4)"
    }

    if ($IPAddress -match $ipv6Pattern -or $IPAddress -match "^(::|([A-Fa-f0-9]{1,4}:){1,6}:?([A-Fa-f0-9]{1,4})?)$") {
        return "True (IPv6)"
    }

    return "False - Invalid IP address format."
}

How It Works

  1. Checks for input – Ensures an IP address was provided.
  2. Matches against IPv4 regex – If valid, returns True (IPv4).
  3. Matches against IPv6 regex – If valid, returns True (IPv6).
  4. Handles compressed IPv6 (::) – Using additional PowerShell logic.
  5. Returns an error if invalid – Helps troubleshoot incorrect formats.

Now, let’s move on to extracting all IPs from a document!

Finding All IP Addresses in a Document

This script scans a file and extracts all IPv4 and IPv6 addresses.

PowerShell Script to Extract IP Addresses from a File

function Get-IPAddressesFromFile {
    param (
        [string]$FilePath
    )

    if (-not (Test-Path $FilePath)) {
        Write-Host "Error: File not found at path: $FilePath"
        return
    }

    $ipv4Pattern = "\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b"
    $ipv6Pattern = "\b([A-Fa-f0-9]{1,4}:){1,7}[A-Fa-f0-9]{1,4}\b"
    $ipAddresses = @()

    Get-Content $FilePath | ForEach-Object {
        $matchesIPv4 = [regex]::Matches($_, $ipv4Pattern)
        $matchesIPv6 = [regex]::Matches($_, $ipv6Pattern)

        if ($matchesIPv4.Count -gt 0) {
            $ipAddresses += $matchesIPv4.Value
        }
        if ($matchesIPv6.Count -gt 0) {
            $ipAddresses += $matchesIPv6.Value
        }
    }

    if ($ipAddresses.Count -eq 0) {
        Write-Host "No IP addresses found in the file."
    } else {
        Write-Host "Found $($ipAddresses.Count) IP addresses:"
        $ipAddresses | Sort-Object -Unique
    }
}

Example File (C:\path\to\your\file.txt)

Server 1: 192.168.1.1  
Server 2: 255.255.255.255  
Router: 2001:db8:85a3::8a2e:370:7334  
Log Entry: Invalid IP -> 999.999.999.999  

Script Output

Found 3 IP addresses:
192.168.1.1
255.255.255.255
2001:db8:85a3::8a2e:370:7334

Regex Exploration in PowerShell

At this point, you’ve seen how regex can validate, extract, and manipulate data in PowerShell. Whether it’s emails, MAC addresses (including Cisco formats), or IPs (both IPv4 and IPv6), you now have practical tools to handle real-world scenarios.

Why Keep Learning Regex?

Regex is one of those skills that pays off the more you use it. The same way a system admin gets better at troubleshooting networks over time, you’ll get faster at spotting patterns and writing efficient expressions.

Here are some great ways to keep sharpening your regex skills:

  • Practice on Real Logs – Take a firewall log, an Apache log, or an email report and extract useful data.
  • Use Online Regex Tools – Websites like regex101.com let you test regex patterns with real-time explanations.
  • Experiment with PowerShell – Try using -match, -replace, and [regex]::Matches() in your daily scripts.
  • Challenge Yourself – Create a script that finds phone numbers, dates, or even URLs in a document.
  • Accept Growth – A few years ago, I wrote SHD – Set Mac Structure – The Random Admin, and I am glad to say I have grown a lot since then. It feels good.

Final Thought

Regex is a skill, not magic. It may look complex at first, but like learning any new language, it becomes second nature with practice. The best way to improve? Find a problem and solve it with regex.

So, what’s your next PowerShell regex use case going to be?

What can we learn as a person today?

When a script fails, we don’t just throw our hands up and quit—we debug it. We check the logs, isolate the issue, and find a fix. But when our own minds start feeling overwhelmed, anxious, or burned out, we often just push through, hoping the problem resolves itself. What if we approached our mental health like we approach troubleshooting code? Debugging isn’t just for PowerShell scripts—it can work for stress, too.

Find the errors

The first step in debugging stress is identifying the error messages. In IT, a recurring issue in the logs might mean something deeper is wrong, and the same goes for mental health. Are you feeling exhausted every morning? Snapping at coworkers over small things? Losing focus even on tasks you usually enjoy? These could be your mind’s version of Event ID 1000: Application Crash. Instead of ignoring the warning signs, acknowledge them—just like you would in a system check.

Analyze the variables

Next, we analyze the variables. Just like a misconfigured setting can break a script, small changes in your routine can make or break your mental well-being. Are you sleeping enough? Eating well? Taking breaks? IT professionals are notorious for skipping meals, working through exhaustion, and staying up late chasing down problems. But just like an unstable system needs a reboot, your brain needs rest. Run a self-check—what’s missing from your routine that could improve stability?

Implement something

Finally, implement a fix and test the results. Maybe you start with a simple -replace—swapping out caffeine overload for proper hydration or scheduling actual breaks instead of “just five more minutes.” Maybe you automate self-care reminders, like setting a PowerShell script to remind you every hour to step away from the screen. And if the issue persists? Just like with a stubborn bug, escalate it—talk to a friend, mentor, or even a therapist. There’s no shame in calling in extra support when needed.

In IT, we don’t assume things will “just work”—we test, refine, and optimize. Treat your mental health the same way. Debug your stress, adjust your variables, and don’t be afraid to run an upgrade on your self-care routine. The best systems run smoothly when properly maintained—and that includes you.

Speed test with PowerShell

Speed test with PowerShell

Often times I need to run a speed test on a remote machine. Doing a speed test tends to help you understand what’s going on with the end user’s computer. For example, currently, my ISP is having issues. I am getting around 6mbps down and 75 Mbps up. If I called in and said I couldn’t watch training videos… that’s why. so it’s essential to know the speeds. That’s why we want to do a speed test with Powershell.

The Script

#Tests if C:Temp Exists, If it doesn't, makes it. 
$CTemp = "c:\Temp"
if (!(Test-Path $CTemp)) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
if (!(Test-Path "$CTemp\SpeedTest")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }

#Download the Speed Test Tool. 
$URL = "https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-win64.zip"
$DownloadPath = "$CTemp\SpeedTest.Zip"
if (!(Test-Path $DownloadPath)) { Invoke-WebRequest -Uri $URL -OutFile $DownloadPath }

#Expand the Zip File
Expand-Archive $DownloadPath -DestinationPath "$CTemp\Speedtest" -Force

$test = & "C:\temp\Speedtest\speedtest.exe" --accept-license

$DownloadSpeed = [regex]::match(($test | where-object { $_ -like "*Download:*" }).trim(), '[0-9]+\.?[0-9]*').value
$uploadSpeed = [regex]::match(($test | where-object { $_ -like "*Upload:*" }).trim(), '[0-9]+\.?[0-9]*').value
$ISP = ($test | where-object { $_ -like "*ISP:*" }).trim().split(":")[1].trim()
$server = ($test | where-object { $_ -like "*Server:*" }).trim().split(":")[1].trim()
$SpeedTestURL = ($test | where-object { $_ -like "*Result URL:*" }).trim().split(" ")[2].trim()

[PSCustomObject]@{
    Server     = $server
    ISP        = $ISP
    Download   = $DownloadSpeed
    Upload     = $uploadSpeed
    ResultsURL = $SpeedTestURL
}

The Breakdown of the Speed test with PowerShell

As we enter this script, you can see my laziness already. The first thing I do is make a string called ctemp for the “c:\temp”. This way I don’t have to type “c:\temp” repeatedly. Next, we test the path to see if c:\temp exists. If it doesn’t, we make it. We do this with the new-item command with the name as temp and the item type as a directory. We do the same thing with c:\temp\speedtest for later. If you really want to get fancy, you can replace the C: with an $env:systemdrive.

Building the folders

$CTemp = "c:\Temp"
if (!(Test-Path $CTemp)) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
if (!(Test-Path "$CTemp\SpeedTest")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }

The next step is to download the speed test application from ookla. We need the URL and where it’s going to be downloaded. That’s why the URL has the URL needed. We want to call the file speedtest.zip. Next, we test to see if we already downloaded it. If we didn’t, we download the file using the invoke web request and the out file tags.

Downloading the app

$URL = "https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-win64.zip"
$DownloadPath = "$CTemp\SpeedTest.Zip"
if (!(Test-Path $DownloadPath)) { Invoke-WebRequest -Uri $URL -OutFile $DownloadPath }

Once we have the file downloaded, we need to extract the Speed test files. We use the expand archive command to force the download to the c:\temp\speedtest folder that we made at the beginning. I use the force flag here to overwrite anything just in case the file was already there.

Expanding the drive

#Expand the Zip File
Expand-Archive $DownloadPath -DestinationPath "$CTemp\Speedtest" -Force

Next, we run the Speed test with PowerShell. In PowerShell, you can run apps directly inside the shell and input that data directly into the shell by using the & sign. With this software, we need to accept the license if we want to do this script without human interaction. Finally, we want to push its output into a variable. Since it’s a test, let us call it a test.

Running the Speed test with PowerShell

$test = & "$CTemp\Speedtest\speedtest.exe" --accept-license

The variable $test contains a few strings with the information we need. It’s time to work with those strings. The string contains the server, isp, download, upload, and jitter information. Here is an example output of what this little program produces:

Speedtest by Ookla

     Server: Piedmont Redacted
        ISP: Spectrum Business
    Latency:    22.46 ms   (0.12 ms jitter)

   Download:   166.84 Mbps (data used: 217.5 MB)

     Upload:   194.09 Mbps (data used: 224.6 MB)
Packet Loss:     0.0%
 Result URL: redacted

The first item I want to extract is the download. The code is a little complex so lets start from the inside. We start off with our $test variable. We want to find the string line that contains “Download:” and then trim that up.

($test | where-object { $_ -like "*Download:*" }).trim()

Next, we need to wrap this inside a regex match. We are using the .net structure here using the [regex]:match. Don’t use matches, it will give you additional information. The match uses our variable and the regex to match with. The regex is the hardest part. So let’s take it apart.

The Regex

'[0-9]+\.?[0-9]*'

The first part [0-9] is searching for characters 0 – 9. However, it only looks at the first digit. The + looks for the rest until we reach a “.”. The “\” is before the “.” because “.” is a used variable in regex. If we stop here, we only get the first part of the download speed. We don’t get anything past the “.”. So we add the “?”. Once we do this it allows us to continue. At this point, we look for another [0-9]. Once again, it’s just the first character. We want everything past that so we add the “*”. Now we have the first match. This is the download speed. Once we have the regex information we ask for only the value. We do this with the upload speed as well.

$ISP = ($test | where-object { $_ -like "*ISP:*" }).trim().split(":")[1].trim()
$server = ($test | where-object { $_ -like "*Server:*" }).trim().split(":")[1].trim()
$SpeedTestURL = ($test | where-object { $_ -like "*Result URL:*" }).trim().split(" ")[2].trim()

Now, we need to work with The ISP. We search for the “ISP:” inside our test variable. Like before, we trim up the string. This removes the spaces at the start and end. Then we split this string with the “:”. The split is here because it makes sense. We select the second object from that split as it contains the text. The first, which is also 0, doesn’t contain the text. Now we have the string, we once again trim it up. There you go. That’s the ISP. We do the same thing with the server as the ISP. The results URL is a little different. We split the space and then select the third object.

Displaying the information

Finally, we create a PS custom object and place each piece of useful information into it. When you launch the script, this information will return and you can see it inside your rmm feedback. From here, you can add this information to your RMM tool in custom fields if you wish. Since I use Ninja at the moment, I will go over that later.

That’s it, Speed Test with PowerShell is at your fingertips. I hope you enjoy it.

Additional Resources

URLs With PowerShell

URLs With PowerShell

Regex… Even the PowerShell Masters struggle with it from time to time. I helped a friend of mine with some URL chaos. The URL he had was a software download and the token was inside the URL. Yeah, it was weird but totally worth it. There were many different ways to handle this one. But the Matches was the best way to go. What was interesting about this interaction was I could later in another script. Unlike my other posts, this one’s format is going to be a little different. Just following along.

The URL (Example)

"https://.download.software.net/windows/64/Company_Software_TKN_0w6xBqqzwvw3PWkY87Tg301LTa2zRuPo09iBxamALBfs512rSgomfRROaohiwgJx9YH7bl9k72YwJ_riGzzD3wEFfXQ7jFZyi5USZfLtje2H68w/MSI/installer"

Here is the string example we are working with. Inside the software installer, we have the name of the software, “Company_Software_” and the token, “0w6xBqqzwvw3PWkY87Tg301LTa2zRuPo09iBxamALBfs512rSgomfRROaohiwgJx9YH7bl9k72YwJ_riGzzD3wEFfXQ7jFZyi5USZfLtje2H68w” The question is how do you extract the token from this URL? Regex’s Matches we summon you!

Matches is one of the regex’s powerhouse tools. It’s a simple method that allows us to search a string for a match of our Regex. This will allow us to pull the token from URLs with PowerShell. First, it’s best to link to useful documentation. Here is the official Microsoft documentation. Sometimes it’s helpful. Another very useful tool can be found here.

PowerShell’s Regex can be expressed in many different ways. The clearest and most concise way is to use the -match flag.

$String -match $Regex

This of course produces a true or false statement. Then if you call the $matches variable, you will see all of the matches. Another way of doing this is by using the type method.

[regex]::Matches($String, $Regex)

This method is the same as above, but sometimes it makes more sense when dealing with complex items. The types method is the closest to the “.net” framework.

The Regex

Next, let’s take a look at the Regex itself. We are wanting to pull everything between TKN_ and the next /. This was a fun one.

'_TKN_([^/]+)'

The first part is the token. We want our search to start at _TKN_. This clears out all of the following information automatically: https://.download.software.net/windows/64/Company_Software. A next part is a group. Notice the (). This creates a group for us to work with. Inside this group, we are searching for all the characters inside []. We are looking for Everything starting at where we are, the TKN_ to a matching /. We want all the information so we place a greedy little +. This selects our token. This regex produces two matches. One with the word _TKN_ and one without. Thus we will want to capture the second output, aka index of 1.

$String -match '_TKN_([^/]+)'
$Matches[1]

Another way to go about this is the method-type model.

$Token = [regex]::Matches($String, '_TKN_([^/]+)') | ForEach-Object { $_.Groups[1].Value }

It same concept, instead this time we are able to parse the output and grab from group one.

Replace Method

Another way to go about this is by using replace. This method is much easier to read for those without experience in regex. I always suggest making your scripts readable to yourself in 6 months. Breaking up the string using replace makes sense in this light. The first part of the string we want to pull out is everything before the _TKN_ and the TKN itself. The second part is everything after the /. Thus, we will be using two replaces for this process.

$String -replace(".*TKN_",'')

Here we are removing everything before and the TKN_ itself. The .* ensures this. Then we wrap this code up and run another replace.

$Token = ($String -replace(".*TKN_",'')) -replace('/.*','')

Now we have our token. This method is easier to follow.

Conclusion

In Conclusion, parsing URLs With PowerShell can be tough, but once you get a hang of it, life gets easier. Use the tools given to help understand what’s going on.

Additional Regex Posts

Image created with midjourney AI.

Playing with Logs – Regex

Playing with Logs – Regex

Recently I was playing with some sql event viewer logs. These logs don’t have properties that we can pull from to make life easier. So, everything has to be parsed through the string. Which isn’t bad, but it’s a challenge to think about. Here is what one of the strings looks like:

Login failed for user 'db_admin'. Reason: Password did not match that for the login provided. [CLIENT: 10.0.80.55]

I wanted three items from this list, the username, the reason, and the IP address. The username is inside the single quotes. The reason goes from the word reason to the [ for client. The IP address is inside the brackets for the client. Lets get started. First lets get the data.

$test = invoke-command -ComputerName servername -ScriptBlock {Get-WinEvent -FilterHashTable @{logname='application';providername='MSSQLSERVER';Keywords='4503599627370496'}}

Grabing Everything between double single qoutes.

Now we have the data, it’s time to get the username. As I said, the username is found inside the single quotes. So we want to select the string with a pattern that pulls the double single quotes.

($t.message | Select-String -Pattern "'.*?'" -AllMatches).Matches.Value -replace "'",""

‘.*?’

This bad boy here grabs everything between the single quotes. The ‘—‘ is the boundaries. the . says to match any character except the terminators. Yeah, we don’t want skynet. Then the X? tells it to match the previous token between 0 and forever times. The select starts off with the first and then moves to the next character. which is a wild card. Then we search all wildcards forever until the next with the *? characters.

We select every item in the string that matches that pattern using the -AllMatches. Then we grab the Matches by using the ().Matches. We want those values so we select the value from the matches. ().Matches.Value. This still selects the double single quotes and we really don’t want this. So we simply remove them by using the -replace command. We replace the ‘ by saying -replace “‘”,””. Looks a little confusing but it works.

Grabbing Text after a word and before a symbol.

The next part is to grab the reason. This is basically grabbing everything after a single word and before something different. The logic is the same as before, but this time we are grabbing it based off a word.

Reason =  (($t.Message | Select-String -Pattern "Reason:.*\[" -AllMatches).Matches.Value -replace ' \[','') -replace 'Reason: ',''

“Reason:.*\[“

In this instance we are searching for the word Reason:. Once we find that word, we select the first object in front of it using a wild card again. The wild card is a . like before. Then we tell it to continue searching using the * until we reach special character of [. Notice the \ is before the [. The reason for this is because in the world of Regex the bracket, [, is a special character used for searching. Thus this code is saying, start with the word reason: and search everything until you reach the square bracket.

Once we have the pattern we select all the matches like before with the -allmatches. We then select the matches and the values using the ().matches.value commands. From there we want to remove the square bracket and the word reason:. We do that with replace commands to remove the word reason we use -replace (“Reason: “,”) and to remove the extra space and square bracket we us -replace (‘ \[‘,”). Notice once again, the \ before the square bracket.

Pulling an IP address from a string

The next thing we need is the IP address of the log. The IP address is located in the square brackets with the word client inside of it. The key here is not to search those brackets. We want the IP address of any and all strings. We want all the IP addresses and not just one.

IPAddress = ($t.message |  Select-String -Pattern "\d{1,3}(\.\d{1,3}){3}" -AllMatches).Matches.Value

\d{1,3}(\.\d{1,3}){3}

This one is much more complex than the last one. The first thing we do is look for is up to three digits side by side. \d means digits. The {1,3} means between 1 and 3. We do this until we reach a . mark. Then we repeat the process again 3 times. We use the () to create a group. Inside that group, we have the \. which is the decimal point followed by the \d{1,3} again. Saying after the decimal point looks for up to three digits again. Finally, we tell the code to do this 3 times with the {3} tag at the end of the group.

Like before we use the -allmatches flag to get all the matches and pipe it out using the ().Matches.value method. But wait! This only pulls the IP address format, not the IP address. This works for an IP address of 512.523.252.1 which we all know is an invalid IP address. To do that we have to dive much deeper into regex. This next part is complex.

Validate an IP address

The above gives an idea of what we want to look for, a starting point, here is the big fish that we need to break down. This code is a bit longer. This code is broken up between whatif structures. Which is pretty cool. It’s going to take some effort to explain it all. So we will take one group at a time with each whatif | statement. Remember each group represented with () is for a single octet. I am going to try my best at explaining this one. I’m not fully sure, but once again, I will try.

^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-5][0-9]|[01]?[0-9][0-9]?)$

25[0-5]|

Our first what if statement is the 25[0-5]. The maximum number for an octet is 255. In fact, 255.255.255.255 is the broadcast address. In this what-if statement, we are looking at matching the first two characters of an octet as 25 then we are looking for any number after that as a 0 to 5. So, anything like 256 would be ignored.

2[0-4][0-9]|

The next one is for the middle of the 255 range. This tests to see if the range is 2xy. The x is between 0 and 4 and the y is 0 and 9. I’ll be honest, When I changed out the 4 with a 5, the value of 256 was accepted when it shouldn’t have been. There is something special later on on this one.

[01]?[0-9][0-9]?

Now we are matching if the first character is a 1. So, this will cover the 100-199 ranges. If there is a 1 there, if not, then it will cover the 0-99. The ? matches the previous item between 0 and one times. So the previous item is a 1 or a number. This creates the nice 0-199 range.

\.){3}

The \ symbol is our break symbol to break the regex processing on the next item which is our . symbol. Normally . means a wild card. In this case it means a period or decimal point. Then we close the group with our closing parentheses, ). Now we have made a group that determines if an object is between 1 and 255 with a period at the end, we need to do this 3 times. There is multiple ways to do this. We can repeat the code 3 times or we can just say {3}. That’s what we did here.

(25[0-5]|2[0-5][0-9]|[01]?[0-9][0-9]?)

Finally, we repeat the code again. This time without the \. at the end. The reason we do this is that the last octet doesn’t have a period at the end. This matches between 1-255 without the period.

Grabbing Mac Addresses from a string

Another item I pull from logs is mac addresses. Most of the time these are security logs from a firewall. However, it’s important to be able to pull a match address. The big thing between a mac address and an IP address is the mac address requires letters and numbers. They also come in all forms of delimiters. the most common are :, ., , and a space for the truly evil people. Thus, you have to address each of these items. Here is the code:

([0-9a-fA-F]{2}[: \.-]){5}([0-9a-fA-F]{2})

[0-9a-fA-F]{2}

The first part of the code is looking for the numbers 0 – 9. For example 0F:69:0F:FE:00:01 is the code. The first section is 0F. The 0 – 9 helps us find the 0. The next part a – f helps us find a,b,c,d,e, and f. The A – F helps us find A,B,C,D,E, and F. This way we don’t have to worry about case sensitivity when searching for the logs as some logs don’t capitalize the letters. Finally, we are going to search this two times before our next symbol.

[: \.-]

This next part is the search for the different symbols. Notice we have the common ones in there. The first is the standard colon :. It is followed by a space and then the escape character because the period is a wild card character. Which is then followed by the hyphen as ipconfig /all gives you hypens. It’s all within the search brackets.

(){5}

We then close up the group with our () marks. This will search for at least one of those that match. We want 5 items in a row for a mac address. Mac addresses contain 6 sections. So, the next code is important to find that 6th. We search for the 5 in the row by the {5} mark.

([0-9a-fA-F]{2})

We repeat the code over again. This way we get that last section of the mac address. This time tho, we don’t have the search for the unique symbols as the last section of a mac address doesn’t have one.

Putting these in a script

If you read my blog often, you know I like functions. Here are two functions you can add to your tool bag.

Get-SHDIPFromString

Function Get-SHDIPFromString{
    [cmdletbinding()]
    Param (
        [string]$String
    )
    foreach ($string in $string) {
        ($String | Select-String -Pattern "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" -AllMatches).Matches.Value
    }
}

Example:

PS> Get-SHDIPFromString -String "This message contains 192.168.25.5 as an IP address." 
192.168.25.5  

Get-SHDMacFromString

function Get-SHDMacFromString {
    [cmdletbinding()]
    Param (
        [string[]]$String
    )
    foreach ($string in $String) {
        ($String | Select-String -Pattern '(?<mac>([0-9a-fA-F]{2}[: \.-]){5}([0-9a-fA-F]{2}))' -AllMatches).matches.value
    }
}

Example:

PS> Get-SHDMacFromString -String "Physical Address. . . . . . . . . : 11-35-AF-FE-11-A1"
11-35-AF-FE-11-A1