Tag Archives: ForEach-Object

Explore the Command and Ensure the Efficiency

The best part of this entire process—the writing about PowerShell for coming up on nine years—has been the realization of my own mistakes while using PowerShell, and then sharing those. I have an upcoming post on this, but in between preparing and publishing that, I, yes me, made another mistake.

Let’s start with what I wrote first, which is included below. It’s a simple Get-ADComputer command. By now, we’ve all likely written plenty of these. Looks great right? Does it though?

Get-ADComputer -Filter * -Properties OperatingSystem,Description |
    Where-Object -Property OperatingSystem -like '*Server*' |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

Before we move forward and determine what I did wrong, let’s use Measure-Command to see how long the execution takes for me on my machine.

Measure-Command -Expression {
    Get-ADComputer -Filter * -Properties OperatingSystem,Description |
        Where-Object -Property OperatingSystem -like '*Server*' |
        Select-Object -Property Name, OperatingSystem |
        Sort-Object -Property OperatingSystem, Name
}
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 4
Milliseconds      : 105
Ticks             : 41055467
TotalDays         : 4.75179016203704E-05
TotalHours        : 0.00114042963888889
TotalMinutes      : 0.0684257783333333
TotalSeconds      : 4.1055467
TotalMilliseconds : 4105.5467

Four seconds. That doesn’t seem too long, but I’m guessing it is based on the way in which the command was written. I never really trust a single test, so let’s set up the above Measure-Command command to run 10 times consecutively using ForEach-Object.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter * -Properties OperatingSystem,Description |
            Where-Object -Property OperatingSystem -like '*Server*' |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      5          370
      5          213
      5          311
      5          839
      4          500
      6          234
      5           50
      5          239
      4          656
      5          421

Multiple tests and this is closer to a full five seconds per invocation. I don’t know about you, but five seconds in PowerShell is an eternity. We use PowerShell for accuracy, sure, but we use it for efficiency, as well. The quicker the better; we shouldn’t limit ourselves. If you find a mistake then fix it.

Now, let’s use the Filter parameter the way it was intended to be used. Piping to Where-Object is always going to be slower than using the filtering options provided by the commands we use. Here’s our base command corrected.

Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

And here it is wrapped up to be tested 10 times such as we did previously.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      1          501
      0          922
      0          907
      0          895
      0          930
      0          906
      1          790
      1          534
      1          284
      0          937

Maybe you’re not worried about those lost seconds. If those aren’t that important, then let it be the possibility that someone’s going to see your failure to be efficient. If I saw this—and again I made this mistake first—I would be concerned that the person writing this code didn’t better explore the command(s) they’re using. If you’re going to use a command, then know it well enough to know how well you’re using it.

Put the Alias Description Property to Work II

In part I of Put the Alias Description Property to Work, I showed and explained how to add a description property to each of my personal, profile script-created aliases. This allowed me to use a basic function, that filtered the Get-Alias results to return only my aliases. In this post, we will take this one step further and we will add descriptions to the built-in aliases in PowerShell. If you remember, we determined that most of the built-in aliases do not include a source property. Therefore, sorting or filtering on that property is of little value.

The source property is not a settable property, meaning that no matter how hard I try, I cannot edit it. The Description property, as we learned, is open for me to make changes. This is also true with built-in aliases and not just the aliases I create. Here is what we are going to do. All the aliases resolve to a command, and all but four of those commands have a source property. We are going to acquire a command’s source and set it as the description of its alias. This might make more sense with some examples, so let’s start there.

But before we do, let’s look at the default properties returned by Get-Alias.

Get-Alias | Get-Random
CommandType     Name                Version    Source
-----------     ----                -------    ------
Alias           pwd -> Get-Location 

Get-Alias returns CommandType, Name, Version, and Source. Kind of. Name is really a property called DisplayName. It may be better to know that now. In the first, real example, we are also going to use Get-Random to randomly select an alias for us. Then, we will return its ResolvedCommandName, DisplayName, Description, and Options properties.

$Alias = Get-Alias | Get-Random
Get-Alias -Name $Alias |
    Select-Object ResolvedCommandName,DisplayName,Description,Options
ResolvedCommandName DisplayName               Description  Options
------------------- -----------               -----------  -------
Connect-PSSession   cnsn -> Connect-PSSession             ReadOnly

Notice that currently, its Description property is blank. In the next example, we will use the value stored in $Alias and run it through a ForEach-Object loop. Take a look at the PowerShell now, and I will do that recently experimented with, line-by-line explanation further below.

Get-Alias -Name $Alias | ForEach-Object {
    if ($_.Description -eq '') {
        $Params = @{
            Name = $_.Name
            Value = $_.ResolvedCommandName
            Description = if ((Get-Command -Name $_.ResolvedCommandName).Source) {
                ((Get-Command -Name $_.ResolvedCommandName).Source)
            } else {'None'}
            Option = $_.Options
            Force = $true
        }
        Set-Alias @Params -Verbose
    }
}

VERBOSE: Performing the operation "Set Alias" on target "Name: cnsn Value: Connect-PSSession".

Line 1: Pipe our single alias into a ForEach-Object loop.
Line 2: Continue into an if construct if the alias’ description is empty. If it is not, we would move on to the next alias if there were more than just one.
Line 3: Create a hash table and store it in the $Params variable.
Line 4, 5: Add the name and the resolved command name into the Name and Value keys, respectively.
Line 6 – 8: Add a description to the Description key based on whether or not there is a source. If there is a source, add it, if there is not a source add the string None.
Line 9, 10: Add the options and $true value into the Option and Force keys, respectively.
Line 12: Invoke the Set-Alias command, splatting the $Params hash table as parameters and associated parameter values.

Let’s rerun the command we did just a minute ago.

Get-Alias -Name $Alias |
    Select-Object ResolvedCommandName,DisplayName,Description,Options
ResolvedCommandName DisplayName               Description                Options
------------------- -----------               -----------                -------
Connect-PSSession   cnsn -> Connect-PSSession Microsoft.PowerShell.Core ReadOnly

We have taken the source of the command Connect-PSSession and placed its value into the description of an alias that resolves to Connect-PSSession. It is not genius, but it is something!

Moving on, the next command pipes out all the known aliases and the values in each of the included properties. I will only include the first ten, as there are almost 150.

Get-Alias |
    Select-Object ResolvedCommandName,DisplayName,Description,Options
ResolvedCommandName DisplayName           Description              Options
------------------- -----------           -----------              -------
Where-Object        ? -> Where-Object                   ReadOnly, AllScope
ForEach-Object      % -> ForEach-Object                 ReadOnly, AllScope
Add-Content         ac -> Add-Content                             ReadOnly
Clear-Host          c -> Clear-Host        tommymaynard               None
Get-Content         cat -> Get-Content                                None
Set-Location        cd -> Set-Location                            AllScope
Set-Location        chdir -> Set-Location                             None
Clear-Content       clc -> Clear-Content                          ReadOnly
Clear-Host          clear -> Clear-Host                               None
Clear-History       clhy -> Clear-History                         ReadOnly

This should look familiar. There are a couple of differences in this code block compared to the one above. One, we are piping in all of the aliases (not just one), and two, there is no Verbose parameter included when the Set-Alias command is invoked. We do not need to output the change made to each alias. Yeah, no thanks.

Get-Alias | ForEach-Object {
    if ($_.Description -eq '') {
        $Params = @{
            Name = $_.Name
            Value = $_.ResolvedCommandName
            Description = if ((Get-Command -Name $_.ResolvedCommandName).Source) {
                ((Get-Command -Name $_.ResolvedCommandName).Source)
            } else {'None'}
            Option = $_.Options
            Force = $true
        }
        Set-Alias @Params
    }
}

This is the same command we saw earlier. I have only included the first ten here, as well. Notice the changes made to the Description property for each alias. Now I can easily see which alias goes with which source.

Get-Alias |
    Select-Object ResolvedCommandName,DisplayName,Description,Options
ResolvedCommandName DisplayName           Description                                Options
------------------- -----------           -----------                                -------
Where-Object        ? -> Where-Object     Microsoft.PowerShell.Core       ReadOnly, AllScope
ForEach-Object      % -> ForEach-Object   Microsoft.PowerShell.Core       ReadOnly, AllScope
Add-Content         ac -> Add-Content     Microsoft.PowerShell.Management           ReadOnly
Clear-Host          c -> Clear-Host       tommymaynard                                  None
Get-Content         cat -> Get-Content    Microsoft.PowerShell.Management               None
Set-Location        cd -> Set-Location    Microsoft.PowerShell.Management           AllScope
Set-Location        chdir -> Set-Location Microsoft.PowerShell.Management               None
Clear-Content       clc -> Clear-Content  Microsoft.PowerShell.Management           ReadOnly
Clear-Host          clear -> Clear-Host   None                                          None
Clear-History       clhy -> Clear-History Microsoft.PowerShell.Core                 ReadOnly

Like all worthy PowerShell code, I am going to wrap this in a function, and for the foreseeable future, copy this into my profile script. There is one change in this version that should be mentioned. The Scope parameter is now being included inside the $Params hash table with the Global value. Like the other parameters and parameters values in this hash table, it will be splatted onto the Set-Alias cmdlet when it is invoked. While the Scope parameter is not required with Get-Alias, it absolutely is with Set-Alias. We want the aliases that exist outside the function to be the ones we modify.

function Set-AliasDescription
Get-Alias | ForEach-Object {
    if ($_.Description -eq '') {
        $Params = @{
            Name = $_.Name
            Value = $_.ResolvedCommandName
            Description = if ((Get-Command -Name $_.ResolvedCommandName).Source) {
                ((Get-Command -Name $_.ResolvedCommandName).Source)
            } else {'None'}
            Option = $_.Options
            Force = $true
            Scope = 'Global'
        }
        Set-Alias @Params
    }
}
Set-AliasDescription

With the descriptions set, we can filter such as in the next two examples. Remember, the Get-Alias Description property is not displayed by default.

Get-Alias | Where-Object -Property Description -eq 'None'
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           clear -> Clear-Host
Alias           cls -> Clear-Host
Alias           man -> help
Alias           md -> mkdir
Get-Alias | Where-Object -Property Description -like '*core'
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           ? -> Where-Object
Alias           % -> ForEach-Object
Alias           clhy -> Clear-History
Alias           cnsn -> Connect-PSSession
Alias           dnsn -> Disconnect-PSSession
Alias           etsn -> Enter-PSSession
Alias           exsn -> Exit-PSSession
Alias           foreach -> ForEach-Object
Alias           gcm -> Get-Command
Alias           ghy -> Get-History
Alias           gjb -> Get-Job
Alias           gmo -> Get-Module
Alias           gsn -> Get-PSSession
Alias           h -> Get-History
Alias           history -> Get-History
Alias           icm -> Invoke-Command
Alias           ihy -> Invoke-History
Alias           ipmo -> Import-Module
Alias           nmo -> New-Module
Alias           nsn -> New-PSSession
Alias           oh -> Out-Host
Alias           r -> Invoke-History
Alias           rcjb -> Receive-Job
Alias           rcsn -> Receive-PSSession
Alias           rjb -> Remove-Job
Alias           rmo -> Remove-Module
Alias           rsn -> Remove-PSSession
Alias           sajb -> Start-Job
Alias           spjb -> Stop-Job
Alias           where -> Where-Object
Alias           wjb -> Wait-Job

Put the Alias Description Property to Work

I use a small handful of my own PowerShell aliases and of course some of those included with PowerShell natively. There is a best practice around aliases in PowerShell, and that is to not use them in anything that is going to live longer than a one-time use. Therefore, do not use them in scripts, functions, and modules (except in the case where your module exports aliases). I would recommend not even using them in forum posts or emails or work-only documentation. Keep them away from your own personal documentation, too. You might know what spsv, ndr, rcjb, and shcm mean today, at this moment, but you may not a few months from now. They have a place, however. Use them in your console, or shell, to speed up what would otherwise be a longer, manually entered command. Let’s check my $PROFILE script and see how many of my own PowerShell aliases I have.

Get-Content -Path $PROFILE | Select-String -Pattern '-alias'
Set-Alias -Name c -Value Clear-Host
Set-Alias -Name psrh -Value Open-PSReadLineHistoryFile
Set-Alias -Name sel -Value Select-Object
Set-Alias -Name wa -Value Watch-Apartment
Set-Alias -Name psgal -Value Show-PSGalleryProjectJob
'l','link' | ForEach-Object {Set-Alias -Name $_ -Value Find-Link}
Set-Alias -Name cts -Value Convert-TextToSpeech

Based on the above command and its results, I have seven. Even that is too many to remember when they are not being used often. Well, I forgot one recently and I was annoyed I had to open my profile script file and search for -alias. Sure, I could have used the above Get-Content command I wrote and tried out a minute ago, but why? Why not avoid ever having to search for my aliases from inside a source file again. They exist in the PowerShell session; why not search there? I am already in the session.

My first thought was, why does Microsoft not indicate which aliases are built-in? There is a source property. It is just too bad that not many aliases include that information. It would be much simple to filter aliases that way, removing those that are not a part of a Microsoft.<something>.<something> module. But, if you return this property there is often nothing. To begin with, there are currently 147 aliases on my machine.

(Get-Alias).Count
147

Only seven include a value in their source property.

Get-Alias | Where-Object Source -ne ''
CommandType     Name                    Version    Source
-----------     ----                    -------    ------
Alias           fhx -> Format-Hex       7.0.0.0    Microsoft.PowerShell.Utility
Alias           gcb -> Get-Clipboard    7.0.0.0    Microsoft.PowerShell.Management
Alias           gin -> Get-ComputerInfo 7.0.0.0    Microsoft.PowerShell.Management
Alias           gtz -> Get-TimeZone     7.0.0.0    Microsoft.PowerShell.Management
Alias           scb -> Set-Clipboard    7.0.0.0    Microsoft.PowerShell.Management
Alias           stz -> Set-TimeZone     7.0.0.0    Microsoft.PowerShell.Management

Weird right? One command alias is from the Microsoft.PowerShell.Utility module and six are from the Microsoft.PowerShell.Management module. It is not like there are only one and six aliases, respectively, from each entire module. There are plenty more commands and aliases, as well. Take a look; I have included both modules. We will begin with the Microsoft.PowerShell.Utility module.

((Get-Command -Module Microsoft.PowerShell.Utility).Name |
    ForEach-Object {Get-Alias -Definition $_ -ErrorAction SilentlyContinue}).Count
43
(Get-Command -Module Microsoft.PowerShell.Utility).Name |
    ForEach-Object {Get-Alias -Definition $_ -ErrorAction SilentlyContinue}
CommandType     Name                        Version    Source
-----------     ----                        -------    ------
Alias           clv -> Clear-Variable
Alias           compare -> Compare-Object
Alias           diff -> Compare-Object
Alias           dbp -> Disable-PSBreakpoint
Alias           ebp -> Enable-PSBreakpoint
Alias           epal -> Export-Alias
Alias           epcsv -> Export-Csv
Alias           fc -> Format-Custom
Alias           fhx -> Format-Hex           7.0.0.0    Microsoft.PowerShell.Utility
Alias           fl -> Format-List
Alias           ft -> Format-Table
Alias           fw -> Format-Wide
Alias           gal -> Get-Alias
Alias           gerr -> Get-Error
Alias           gm -> Get-Member
Alias           gbp -> Get-PSBreakpoint
Alias           gcs -> Get-PSCallStack
Alias           gu -> Get-Unique
Alias           gv -> Get-Variable
Alias           group -> Group-Object
Alias           ipal -> Import-Alias
Alias           ipcsv -> Import-Csv
Alias           iex -> Invoke-Expression
Alias           irm -> Invoke-RestMethod
Alias           iwr -> Invoke-WebRequest
Alias           measure -> Measure-Object
Alias           nal -> New-Alias
Alias           nv -> New-Variable
Alias           ogv -> Out-GridView
Alias           rbp -> Remove-PSBreakpoint
Alias           rv -> Remove-Variable
Alias           select -> Select-Object
Alias           sls -> Select-String
Alias           sal -> Set-Alias
Alias           sbp -> Set-PSBreakpoint
Alias           set -> Set-Variable
Alias           sv -> Set-Variable
Alias           shcm -> Show-Command
Alias           sort -> Sort-Object
Alias           sleep -> Start-Sleep
Alias           tee -> Tee-Object
Alias           echo -> Write-Output
Alias           write -> Write-Output

And continue with the Microsoft.PowerShell.Management module.

((Get-Command -Module Microsoft.PowerShell.Management).Name |
    ForEach-Object {Get-Alias -Definition $_ -ErrorAction SilentlyContinue}).Count
62
(Get-Command -Module Microsoft.PowerShell.Management).Name |
    ForEach-Object {Get-Alias -Definition $_ -ErrorAction SilentlyContinue}
CommandType     Name                         Version    Source
-----------     ----                         -------    ------
Alias           ac -> Add-Content
Alias           clc -> Clear-Content
Alias           cli -> Clear-Item
Alias           clp -> Clear-ItemProperty
Alias           cvpa -> Convert-Path
Alias           copy -> Copy-Item
Alias           cp -> Copy-Item
Alias           cpi -> Copy-Item
Alias           cpp -> Copy-ItemProperty
Alias           dir -> Get-ChildItem
Alias           gci -> Get-ChildItem
Alias           ls -> Get-ChildItem
Alias           gcb -> Get-Clipboard         7.0.0.0    Microsoft.PowerShell.Management
Alias           gin -> Get-ComputerInfo      7.0.0.0    Microsoft.PowerShell.Management
Alias           cat -> Get-Content
Alias           gc -> Get-Content
Alias           type -> Get-Content
Alias           gi -> Get-Item
Alias           gp -> Get-ItemProperty
Alias           gpv -> Get-ItemPropertyValue
Alias           gl -> Get-Location
Alias           pwd -> Get-Location
Alias           gps -> Get-Process
Alias           ps -> Get-Process
Alias           gdr -> Get-PSDrive
Alias           gsv -> Get-Service
Alias           gtz -> Get-TimeZone          7.0.0.0    Microsoft.PowerShell.Management
Alias           ii -> Invoke-Item
Alias           mi -> Move-Item
Alias           move -> Move-Item
Alias           mv -> Move-Item
Alias           mp -> Move-ItemProperty
Alias           ni -> New-Item
Alias           mount -> New-PSDrive
Alias           ndr -> New-PSDrive
Alias           popd -> Pop-Location
Alias           pushd -> Push-Location
Alias           del -> Remove-Item
Alias           erase -> Remove-Item
Alias           rd -> Remove-Item
Alias           ri -> Remove-Item
Alias           rm -> Remove-Item
Alias           rmdir -> Remove-Item
Alias           rp -> Remove-ItemProperty
Alias           rdr -> Remove-PSDrive
Alias           ren -> Rename-Item
Alias           rni -> Rename-Item
Alias           rnp -> Rename-ItemProperty
Alias           rvpa -> Resolve-Path
Alias           scb -> Set-Clipboard         7.0.0.0    Microsoft.PowerShell.Management
Alias           si -> Set-Item
Alias           sp -> Set-ItemProperty
Alias           cd -> Set-Location
Alias           chdir -> Set-Location
Alias           sl -> Set-Location
Alias           stz -> Set-TimeZone          7.0.0.0    Microsoft.PowerShell.Management
Alias           saps -> Start-Process
Alias           start -> Start-Process
Alias           sasv -> Start-Service
Alias           kill -> Stop-Process
Alias           spps -> Stop-Process
Alias           spsv -> Stop-Service

In the end, the Microsoft.PowerShell.Utilityhas 43 aliases and the Microsoft.PowerShell.Management module has 63 aliases. Maybe there is a good reason for the source, not being included most of the time. But, we are here to make my aliases easier to find. The source property is not a settable property, therefore, we are going to use the Description property to indicate when an alias is one of mine. Here are my aliases from earlier, each with a new addition to their Description property. It is my name.

Set-Alias -Name c -Value Clear-Host -Description 'tommymaynard'
Set-Alias -Name psrh -Value Open-PSReadLineHistoryFile -Description 'tommymaynard'
Set-Alias -Name sel -Value Select-Object -Description 'tommymaynard'
Set-Alias -Name wa -Value Watch-Apartment -Description 'tommymaynard'
Set-Alias -Name psgal -Value Show-PSGalleryProjectJob -Description 'tommymaynard'
'l','link' | ForEach-Object {Set-Alias -Name $_ -Value Find-Link -Description 'tommymaynard'}
Set-Alias -Name cts -Value Convert-TextToSpeech -Description 'tommymaynard'

Now, after my profile script runs, I can invoke a modified Get-Alias command to return just my aliases from the current PowerShell session.

Get-Alias | Where-Object -Property Description -eq 'tommymaynard'
CommandType     Name                               Version    Source
-----------     ----                               -------    ------
Alias           c -> Clear-Host
Alias           cts -> Convert-TextToSpeech
Alias           l -> Find-Link
Alias           link -> Find-Link
Alias           psgal -> Show-PSGalleryProjectJob
Alias           psrh -> Open-PSReadLineHistoryFile
Alias           sel -> Select-Object
Alias           wa -> Watch-Apartment

This post would not be complete if there was no new function to add to my $PROFILE script. So, with that, here is that new function and alias. Notice I added the description to the new alias, too!

Set-Alias -Name gmal -Value Get-MyAlias -Description 'tommymaynard'
function Get-MyAlias {
    Get-Alias | Where-Object -Property Description -eq 'tommymaynard'
}
gmal
CommandType     Name                              Version    Source
-----------     ----                               -------    ------
Alias           c -> Clear-Host
Alias           cts -> Convert-TextToSpeech
Alias           gmal -> Get-MyAlias
Alias           l -> Find-Link
Alias           link -> Find-Link
Alias           psgal -> Show-PSGalleryProjectJob
Alias           psrh -> Open-PSReadLineHistoryFile
Alias           sel -> Select-Object
Alias           wa -> Watch-Apartment

Part II has been published!

Looking Busy with PowerShell


Notice: The following post was originally published on another website. As the post is no longer accessible, it is being republished here on tommymaynard.com. The post was originally published on February 27, 2019.


Every once in a while, I write a short little article — if, I can even call it that — and then it sits around for a month or so doing nothing. It just hangs out in my drafts and stares at me. It’s happened again. Instead of focusing on it any longer, I’m writing this paragraph as my excuse, so I can publish this now and move on, already.

I saw the below Tweet late last year and I had a thought.

I love how having several windows of #Powershell open makes you look busy and/or like you know what you are doing.
— John Dalek (@DeckerDalek) November 20, 2018

I’ll go ahead and agree; why not?

I can see how this may give off that impression. With that quick and internal agreement, I had an idea. If PowerShell can make you look busy, then let’s use PowerShell to make it appear, you’re busy. We’ll open some consoles and execute some “work,” all with a single invocation of a single function. I’ll just be over here waiting for PowerShell to catch up with me.

The below function creates this illusion, just in case this is something you’re after. Hopefully, it’s not, as we all likely have some real PowerShell to read, write, and review. Me included.

Function Show-MeBeingSuperBusy {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateRange(1,10)]
        [int]$ConsoleCount = 1
    )
    
    Begin {
        $Argument = '-NoProfile -Command & {1..50 | ForEach-Object {Get-PSDrive}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Process}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Service}}',
            '-NoProfile -Command & {1..50 | ForEach-Object {Get-Item -Path env:\}}'
    } # End Begin.
    
    Process {
        For ($i = 1; $i -le $ConsoleCount; $i++) {
            Start-Process -FilePath powershell.exe -ArgumentList ($Argument | Get-Random)
        } # End For.
    } # End Process.
    
    End {
    } # End End.
} # End Function: Show-MeBeingSuperBusy.
 
Show-MeBeingSuperBusy -ConsoleCount 5

And that’s it. Short, simple, and hardly very helpful. Now that this “article” is gone from my drafts, I should be able to focus on something a little more helpful — we’ll see. This might be all the help someone needed from me today, however.

Edit: As a part of bringing this old content back to tommymaynard.com, I tried out this function. It works, but be sure to give it a moment before you begin to think it is not working!

Use PowerShell to Edit a CSV, Revisited

1. Back in January 2019, I started writing on another website. In December of that same year, I modified how I was doing things. Instead of just writing a full post there, I would start a new post there and then finish the post here, on my site. I did that through June 2020. All the posts from January 2019 to November 2019 are gone and all the posts from December 2019 to June 2020 are partially gone (as only a portion was written away from tommymaynard.com).

2. I have recently mentioned that if I hit 416 posts by the end of June 2022, I will have an average of one post per week over an eight-year timeframe. Bringing those posts back to life and publishing them on my site would get me to my goal much quicker. Beyond that, I have long wanted to recapture that work anyway, and realistically, those posts could have just as easily — maybe even more easily — been published here, to begin with.

As it is time to fix this, it would not make sense to do things manually unless it was an absolute requirement. Automation is kind of why we are all here. I intend to recover as many of my posts as I can and publish them here with help from archive.org — the Internet Archive — and their API.

I have been down the CSV road before. While not as popular as my Hash Table to CSV post, with greater than 21,000 all-time views, my Use PowerShell to Edit a CSV post has greater than 15,000 all-time views. Hash Table to CSV has part II or revised post, and now Use PowerShell to Edit a CSV does, too.

At first, I started to manually collect data — the data in the below image. I did not want to do that any more than I had already done. That decision was all I needed to save some time that was better spent prepping and preparing for this post. In this first image, you can see the CSV from which I began.

Do notice a few things: One, we’re missing some dates on the left, two, we’re missing all the URLs from the WB_URL column, and three, all the Notes are empty too. Our goal is to add dates on the left where we can, add URLs into the WB_URL column, and Notes when that’s necessary. There’s nothing to add to the TM_URL column. Those empty lines indicate posts that were never written and published on my site.

The first thing to do is ensure I can import the above CSV file. We will use the $CsvBasePath variable to hold the path to the file we will import with the Import-Csv command. The values returned from this command will be stored in the $CsvFileContents variable. Then they will be output to a formatted table, so they are easier to read. This is just a quick check to ensure the CSV file can be imported. As you can see below, the file’s contents could be imported. This is the identical data we saw in the previous image.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$CsvFileContents = Import-Csv -Path "$CsvBasePath/Corrections.csv"
$CsvFileContents | Format-Table -AutoSize

If you want to read about it, and it is short, here is information on the API that we will be using: https://archive.org/help/wayback_api.php. It is too important not to show you. The below image is what a response looks like when it is returned from using the API. We will use this API with Invoke-RestMethod. It may not mean much now, but you may end up referring to it as you progress further into this post. As you will see shortly, I will use …status -eq 200, I will use and edit the timestamp, and I will collect the URL. Having seen this image may make understanding the PowerShell much more straightforward.

I now know the CSV file can be imported and I understand the structure of the API response.

In the below PowerShell, I removed outputting the contents of the $CsvFileContents variable to the host, and instead, I set up a ForEach-Object looping construct. The first thing done inside this loop is put the current PS_URL value into the $UriRemainder variable in line 5. In line 6, we concatenate the $UriBase variable and the $UriRemainder variable and use them as the URI supplied to the Invoke-RestMethod command. We will continue working through this code below.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$UriBase = 'http://archive.org/wayback/available?url='
$CsvFileContents = Import-Csv -Path "$CsvBasePath\Corrections.csv"
$CsvFileContents | ForEach-Object {
	$UriRemainder = $_.PS_URL
	$WBInfoFull = Invoke-RestMethod -Uri "$UriBase$UriRemainder"
	If ($WBInfoFull.archived_snapshots.closest.status -eq 200) {
		$WBInfoDate = $WBInfoFull.archived_snapshots.closest.timestamp
		$WBInfoDate = -join $WBInfoDate[0..7]
		$WBInfoUrl = $WBInfoFull.archived_snapshots.closest.url
		$WBInfoFull
		$WBInfoDate
		$WBInfoUrl
		<#
		$_.Date = $WBInfoDate
		$_.WB_URL = $WBInfoUrl
		#>
		# Remove-Variable -Name WBInfoFull,WBInfoDate,WBInfoUrl
	} Else {
		'[[[[[NOPE]]]]]'
	} # End If-Else.
	$_
} # | Export-Csv -Path "$CsvBasePath\Corrections-temp.csv" -NoTypeInformation

The Invoke-RestMethod command reaches out to archive.org and stores the result — remember the response object — in the $WBInfoFull variable. For each iteration through the loop, this variable is repeatedly filled with data from each lookup against the Wayback Machine — another name for archive.org if I did not say that already. If the status is 200, we know our Invoke-RestMethod command was a success and so we progress further into the If portion of our nested If-Else construct.

We will then set $WBInfoDate by returning the timestamp property such as 20201022005417 and then joining the first eight “digits” [lines 8 and 9]. We then set, or assign, the $WBInfoURL variable. In the remainder of this code, we just dump our values to the screen, clear the variables, and then move onto the next line in the CSV file. We have yet to actually write to a CSV yet.

The below image shows a portion of the output generated by the above commands. Again, I’m not writing to the CSV yet; I’m only making sure the values in my variables are accurate.

It is here where the working PowerShell will be modified in such a way, that we can begin writing to the CSV file. I should make something clear. I did not edit an existing CSV file as much as I created a new one. I suppose I could have written back to the same file…or maybe I could not. That is probably worth finding out someday, but I do suspect that an open CSV can be written to. In the first iteration of this post, I created a temporary file and then did a remove/rename, so it appeared I actually edited a file.  But this whole time, I have not really been editing anything. Such a fraud, I know.

Moving along though. The below changes include having removed the code that outputs the values in the variables to the screen. Instead, these values are being written to the Date and WB_URL columns. When the status isn’t 200, instead of writing [[[[[NOPE]]]]] to the screen as I did above, something more pleasant and professional is written to the Notes column. In order to write to a new CSV, I uncommented the Export-CSV command, as well. In the post’s final image, you can view the “updated” CSV file. By my count, there are over 40 lines in this CSV that I didn’t have to type or paste in after manually doing the search myself. I’ll take it!

Always search for an API to use. Always.

$CsvBasePath = 'C:\users\tommymaynard\Desktop\tommymaynard.com'
$UriBase = 'http://archive.org/wayback/available?url='
$CsvFileContents = Import-Csv -Path "$CsvBasePath/Corrections.csv"
$CsvFileContents | ForEach-Object {
	$UriRemainder = $_.PS_URL
	$WBInfoFull = Invoke-RestMethod -Uri "$UriBase$UriRemainder"
	If ($WBInfoFull.archived_snapshots.closest.status -eq 200) {
		$WBInfoDate = $WBInfoFull.archived_snapshots.closest.timestamp
		$WBInfoDate = -join $WBInfoDate[0..7]
		$WBInfoUrl = $WBInfoFull.archived_snapshots.closest.url
		$_.Date = $WBInfoDate
		$_.WB_URL = $WBInfoUrl
		Remove-Variable -Name WBInfoFull,WBInfoDate,WBInfoUrl
	} Else { 
		$_.Notes = 'Unable to locate an archived webpage for that URL.'
	} # End If-Else.
	$_
} | Export-Csv -Path "$CsvBasePath\Corrections-temp.csv" -NoTypeInformation

While the above image includes the updated data, there have been some unexpected changes from the CSV image earlier in this post. This was worrisome for me at first — why were the dates changing!? My PowerShell worked before, so why not now? It turns out that it still is working. The date changes are because newer snapshots of the pages have been taken by the Wayback Machine since I began this post back in October — yeah, it has been a long time coming. Therefore, no worries. What clued me in was the above code and the response. Take a look at this for a brief moment.

$WBInfoFull.archived_snapshots.closest.timestamp

The keyword is “closest,” as in the most recent snapshot. My most recent snapshots changed between the creation of my original CSV file, and the updated one.

There may be a way to use PowerShell and an API to gather the old content, but for now, I am collecting it manually. I want to reclaim this content ASAP in order to line up getting these posts — the ones that are still relevant — republished here on tommymaynard.com. It is a lot of work, but I am after 416 posts by the end of June 2022. It is fair to say that I will be back with some new, old content very soon.

Fourth Grade Multiplication Tables

I remember math class… in fourth grade. It was the year we learned multiplication. Day after day, for so many consecutive days, we’d have to write our “times tables.” Zero though 10, over and over again. Good thing, I suppose, as I still know them today. The combination of knowing PowerShell and my daughter learning her “times tables” now, got me thinking: How quickly could I “write” my “times tables” today? It turns out it takes around 5 milliseconds.

If you’ve been reading along recently you may have seen what we’ll do today, done before. We need multiple loops. One loop, our outer loop, will cycle through the numbers 0 through 10. For each of those, we’ll have an inner loop multiply 0 through 10 to the current outer loop number. You know, we’ll do zero, so zero times zero, zero times one, and so on, until we hit 10, at which point we’ll move on from zero to one (one times zero, one times one, one times two, etc.). Let’s begin with the outer loop.

foreach ($outernumber in 0..10) {
	$outernumber
}
0
1
2
3
4
5
6
7
8
9
10

Now, let’s do the inner loop. Again, this is going to repeat zero through 10 inside each inner loop, for each outer loop. If this is hard to follow, be sure to take a look at the soon-to-be upcoming example where we’ll highlight both outer and inner loops.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		$innernumber
	}
}
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9
10

This is the same example as we saw above; however, we’ve included the value of the outer loop in our output. Each outer loop value has two dashes (--) in front of it.

foreach ($outernumber in 0..10) {
	"-- $outernumber"
	foreach ($innernumber in 0..10) {
		$innernumber
	}
}
-- 0
0
1
2
3
4
5
6
7
8
9
10
-- 1
0
1
2
3
4
5
6
7
8
9
10
-- 2
0
1
2
3
4
5
6
7
8
9
10
-- 3
0
1
2
3
4
5
6
7
8
9
10
-- 4
0
1
2
3
4
5
6
7
8
9
10
-- 5
0
1
2
3
4
5
6
7
8
9
10
-- 6
0
1
2
3
4
5
6
7
8
9
10
-- 7
0
1
2
3
4
5
6
7
8
9
10
-- 8
0
1
2
3
4
5
6
7
8
9
10
-- 9
0
1
2
3
4
5
6
7
8
9
10
-- 10
0
1
2
3
4
5
6
7
8
9
10

And since this post is lengthy in long examples, let’s add another before we actually get to the results we came for. Our outer loop number is on the left side of the multiplication symbol, and our inner loop number is on the right side.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		"$outernumber x $innernumber ="
	}
}
0 x 0 =
0 x 1 =
0 x 2 =
0 x 3 =
0 x 4 =
0 x 5 =
0 x 6 = 
0 x 7 = 
0 x 8 = 
0 x 9 = 
0 x 10 =
1 x 0 = 
1 x 1 = 
1 x 2 = 
1 x 3 = 
1 x 4 = 
1 x 5 = 
1 x 6 = 
1 x 7 = 
1 x 8 = 
1 x 9 = 
1 x 10 =
2 x 0 = 
2 x 1 = 
2 x 2 = 
2 x 3 = 
2 x 4 = 
2 x 5 = 
2 x 6 = 
2 x 7 = 
2 x 8 = 
2 x 9 = 
2 x 10 =
3 x 0 = 
3 x 1 = 
3 x 2 = 
3 x 3 = 
3 x 4 = 
3 x 5 = 
3 x 6 = 
3 x 7 = 
3 x 8 =
3 x 9 =
3 x 10 =
4 x 0 =
4 x 1 =
4 x 2 =
4 x 3 =
4 x 4 =
4 x 5 =
4 x 6 =
4 x 7 =
4 x 8 =
4 x 9 =
4 x 10 =
5 x 0 =
5 x 1 =
5 x 2 =
5 x 3 =
5 x 4 =
5 x 5 =
5 x 6 =
5 x 7 =
5 x 8 =
5 x 9 =
5 x 10 =
6 x 0 =
6 x 1 =
6 x 2 =
6 x 3 =
6 x 4 =
6 x 5 =
6 x 6 =
6 x 7 =
6 x 8 =
6 x 9 =
6 x 10 =
7 x 0 =
7 x 1 =
7 x 2 =
7 x 3 =
7 x 4 =
7 x 5 =
7 x 6 =
7 x 7 =
7 x 8 =
7 x 9 =
7 x 10 =
8 x 0 =
8 x 1 =
8 x 2 =
8 x 3 =
8 x 4 =
8 x 5 =
8 x 6 =
8 x 7 =
8 x 8 =
8 x 9 =
8 x 10 =
9 x 0 =
9 x 1 =
9 x 2 =
9 x 3 =
9 x 4 =
9 x 5 =
9 x 6 =
9 x 7 =
9 x 8 =
9 x 9 =
9 x 10 =
10 x 0 =
10 x 1 =
10 x 2 =
10 x 3 =
10 x 4 =
10 x 5 =
10 x 6 =
10 x 7 =
10 x 8 =
10 x 9 =
10 x 10 =

In this example, we’re going to add the multiplication problem to our output. Notice that we are making use of the subexpression operator ($()) in order to embed the results of each expression — or multiplication problem — within our string.

foreach ($outernumber in 0..10) {
	foreach ($innernumber in 0..10) {
		"$outernumber x $innernumber = $($outernumber * $innernumber)"
	}
}
0 x 0 = 0
0 x 1 = 0
0 x 2 = 0
0 x 3 = 0
0 x 4 = 0
0 x 5 = 0
0 x 6 = 0
0 x 7 = 0  
0 x 8 = 0  
0 x 9 = 0  
0 x 10 = 0 
1 x 0 = 0  
1 x 1 = 1  
1 x 2 = 2  
1 x 3 = 3  
1 x 4 = 4  
1 x 5 = 5  
1 x 6 = 6  
1 x 7 = 7  
1 x 8 = 8  
1 x 9 = 9  
1 x 10 = 10
2 x 0 = 0  
2 x 1 = 2  
2 x 2 = 4  
2 x 3 = 6  
2 x 4 = 8  
2 x 5 = 10 
2 x 6 = 12 
2 x 7 = 14 
2 x 8 = 16 
2 x 9 = 18 
2 x 10 = 20
3 x 0 = 0 
3 x 1 = 3 
3 x 2 = 6 
3 x 3 = 9 
3 x 4 = 12
3 x 5 = 15
3 x 6 = 18
3 x 7 = 21
3 x 8 = 24
3 x 9 = 27
3 x 10 = 30
4 x 0 = 0
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12
4 x 4 = 16
4 x 5 = 20
4 x 6 = 24
4 x 7 = 28
4 x 8 = 32
4 x 9 = 36
4 x 10 = 40
5 x 0 = 0
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
5 x 10 = 50
6 x 0 = 0
6 x 1 = 6
6 x 2 = 12
6 x 3 = 18
6 x 4 = 24
6 x 5 = 30
6 x 6 = 36
6 x 7 = 42
6 x 8 = 48
6 x 9 = 54
6 x 10 = 60
7 x 0 = 0
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70
8 x 0 = 0
8 x 1 = 8
8 x 2 = 16
8 x 3 = 24
8 x 4 = 32
8 x 5 = 40
8 x 6 = 48
8 x 7 = 56
8 x 8 = 64
8 x 9 = 72
8 x 10 = 80
9 x 0 = 0
9 x 1 = 9
9 x 2 = 18
9 x 3 = 27
9 x 4 = 36
9 x 5 = 45
9 x 6 = 54
9 x 7 = 63
9 x 8 = 72
9 x 9 = 81
9 x 10 = 90
10 x 0 = 0
10 x 1 = 10
10 x 2 = 20
10 x 3 = 30
10 x 4 = 40
10 x 5 = 50
10 x 6 = 60
10 x 7 = 70
10 x 8 = 80
10 x 9 = 90
10 x 10 = 100

That was it. Well, there is one thing. Let’s get back to seeing how long this command takes to run.

Measure-Command -Expression {
	foreach ($outernumber in 0..10) {
		foreach ($innernumber in 0..10) {
			"$outernumber x $innernumber = $($outernumber * $innernumber)"
		}
	}
} | Select-Object -Property Milliseconds

Milliseconds
------------
           5

We could put this entire command in a loop too, and test it across ten iterations. This example includes ticks, as well as milliseconds because when this command is run back-to-back, the speed per iteration drastically increases. Have a look. So it has been said, there are 10,000 ticks per millisecond and 1,000 milliseconds per second.

1..10 | ForEach-Object {
	Measure-Command -Expression {
		foreach ($outernumber in 0..10) {
			foreach ($innernumber in 0..10) {
				"$outernumber x $innernumber = $($outernumber * $innernumber)"
			}
		}
	} | Select-Object -Property Milliseconds,Ticks
}

Milliseconds Ticks
------------ -----
           3 39181
           0  7081
           1 14917
           1 10313
           0  2318
           0  2289
           0  2235
           0  2149
           0  2099
           0  2083

A PowerShell Recursive Function

Note: There is a newer post on tommymaynard.com about a recursive function I wrote. When you are done reading this post, read that one.

I was recently in a position where I was asked to provide an example of a PowerShell recursive function. Unfortunately, I didn’t have time to do that as it was a part of a written and timed test. Sadly, I didn’t even really have time to review the other 50-some answers I had already provided. I mostly knew that might happen.

Anyway, the challenge has been eating at me, so why not write an example and share it. It’ll be good for you, as it will be for me too. It’s out there, but it’s not often utilized that I’m aware. A recursive function is a function that calls, or invokes, itself. For real. We’re not referring to a function, or cmdlet, that includes a Recurse parameter. Now that said, it’s possible that some of these commands, with that parameter, do this (call themselves), but I’ve yet to take a look to see for sure.

After you see today’s example, you’ll begin to understand why you might prefer to do things this way. If you can wrap your head around the concept, you’ll understand that it can reduce a portion of code writing on your part.

As stated, “A recursive function is a function that calls, or invokes, itself.” On that note, let’s first set up the folder structure necessary for this example. While you can do this with PowerShell, I simply didn’t bother. As you can see below, there’s a “Test” folder inside my “Documents” folder. That folder contains three files — A, B, and C.ps1. Additionally, it contains a nested, “More” folder, which contains three more files — D, E, and F.ps1.

So there’s that. Now, let’s take a look at and discuss our PowerShell recursive function.

Function Get-Ps1File {
    Param ($Path= '.\')

    Get-ChildItem -Path $Path | ForEach-Object {
        If ($_.Name -like "*.ps1") {
            $_.Name
        } ElseIf ($_.PSIsContainer) {
            Get-Ps1File -Path $_.FullName
        } # End If-ElseIf.
    } # End ForEach-Object.
} # End Function: Get-Ps1File.

This function loops its way through a path and returns the file(s) that are .ps1 files. If there’s a nested folder, it then goes into that folder and runs itself again (against the content of that folder). Knowing what we know about this folder structure and its files, the below results returned from running this function should make perfect sense.

Get-Ps1File
D.ps1
E.ps1
F.ps1
A.ps1
B.ps1
C.ps1

Let’s recap on this a bit with a little more detail. When the function is first invoked, it uses the current path, as none was supplied. It then runs the ForEach-Object cmdlet against the files in the base folder. Using the If portion of the If-ElseIf statement inside the loop, it determines it should return the A, B, and C .ps1 files. Then, on the fourth iteration through the loop, it hits our “More” folder and determines it’s a folder. There are other ways to determine if it’s a folder, but I stuck with the tried and tested, PSIsContainer NoteProperty.

At this point, it invokes itself against the path of the “More” folder. It runs or executes itself, again. This time, however, it uses the folder’s FullName property (its full path). Inside that second invocation of the function, it iterates over the files inside the “More” folder, echos them to the screen as output, and then ends the function’s second invocation. At that point, it returns back to the first invocation, where it then ends that invocation, as well, as its work is completed.

If you’d prefer to see this sorted, pipe the function name (Get-Ps1File) to the Sort-Object cmdlet.

Get-Ps1File | Sort-Object
A.ps1
B.ps1
C.ps1
D.ps1
E.ps1
F.ps1

And there you go, an example of a PowerShell recursive function. You may not need it often, but you may need it someday.

 

Making Dates: Good and Better

I have a new task. Review some old code written in the time of (Windows) PowerShell 2.0 ensuring I can support it, in case that’s ever needed.

Well, I started last week. While I haven’t been able to get back to it just yet, as some other work came up, I did code a potential change. First off, before you see this code, I didn’t write the original myself. Second, the person that did, would probably do if differently now, too. I’m not here to dis on anyone’s five-year-old code. It’s quite old, in this industry.

Here’s the code as I found it:

$Today = Get-Date
$DateArray = (Get-Date $Today.AddDays(-59) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-58) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-57) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-56) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-55) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-54) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-53) -Format "M-d-yyyy"),(Get-Date $Today.AddDays(-46) -Format "M-d-yyyy")

It’s a bit tough to follow. Let me use the included commas as line breaks. Do keep in mind that we can always move to the next line after a comma, and not interrupt our command. It already looks better; it’s much easier on the eyes, at minimum.

$Today = Get-Date
$DateArray = (Get-Date $Today.AddDays(-59) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-58) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-57) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-56) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-55) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-54) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-53) -Format "M-d-yyyy"),
(Get-Date $Today.AddDays(-46) -Format "M-d-yyyy")

Even though the readability is better, we can still clean this up some more. Before we do that however, let’s discuss what this code does. It essentially creates eight, string dates. Yesterday’s date was October 15, 2018. Based on that date, this code is designed to create string dates from dates in the past. It creates a date string from 58 days ago, 57 days ago, 56 days ago, 55, 54, 53, and even 46 days ago. We’ll run the code now, and return the $DateArray variable.

PS > $DateArray
8-17-2018
8-18-2018
8-19-2018
8-20-2018
8-21-2018
8-22-2018
8-23-2018
8-30-2018

Before I show you what I came up with to change this code, I do want to remind everyone that just because you can simplify something, doesn’t mean you always should. This is especially true if it’s going to make the next person to see your code wonder what the hell you did. I don’t think I’ve done that in this case.

In the below code, we set things up in a ForEach-Object loop. We iteratively send in the numeric values of -59 though -53, and -46. You’ll see that these same values were used in the above example. Inside our loop, we’ll run the Get-Date command subtracting the number of days, based on the number submitted to the loop on that iteration. Additionally, we format the date such as we did originally.

$DateArray = -59..-53 + -46..-46 | ForEach-Object {
    Get-Date -Date (Get-Date).AddDays($_) -Format 'M-d-yyyy'
}

As you can see below, we get the exact same output as we did above. We, simplified the code in such as way that it’s easier to follow visually and intuitively. There’s no speed improvement, as best I could tell, but I’ll still take what I’ve written over what was written years and years ago.

PS > $DateArray
8-17-2018
8-18-2018
8-19-2018
8-20-2018
8-21-2018
8-22-2018
8-23-2018
8-30-2018

Until next time, and for me at least, back to these two PowerShell projects!

Self-Destruction Script

As a part of some work I’m doing, I’ve decided that I need a specific script to do some final configuration for me. More to come on that in time, perhaps. The point here, however, is that a PowerShell script completes my final configuration, and when this final configuration is complete, I have no need for my script any longer. Can a PowerShell script delete itself? Sure it can.

How does a script delete itself? Easy. It’s a single line, really. However, I have a mildly more interesting example. Before you take a look at the included gif — this really does lend itself to a visual — here are the contents of my self-destruction script. It echos an indication that the script will self-destruct. At this point, it begins a countdown in seconds, from 5 to 1, before removing itself — the script file — from the file system. Take a look, and maybe, just maybe, you’ll find yourself in need of something like this sometime, too.

Write-Output -InputObject 'This script will self-destruct in 5 seconds.'

5..1 | ForEach-Object {
    If ($_ -gt 1) {
        "$_ seconds"
    } Else {
        "$_ second"
    } # End If.
    Start-Sleep -Seconds 1
} # End ForEach-Object.

Write-Output -InputObject 'Self destruction.'

# Here's the command to delete itself.
Remove-Item -Path $MyInvocation.MyCommand.Source

And here’s, the visual representation. It’s a bit small, but you should be able to determine what’s happening. The script executes on the left, and once the countdown is over, DeleteSelfScript.ps1 is removed on the right. It’s deleted itself.

Enjoy the week, and learn something new!

View Current PowerShell.org Q&A Forum Topics

Note: Update added at the bottom on this post on August, 9, 2016. Please read.

Sometimes you don’t always have the time to finish something you’ve started. For me, it was this function. I pounded this out in a quick few minutes, and while I don’t see myself investing in it any further, I didn’t want to forget the function, and thought I would hang on to it somewhere. Well, that’s why it’s here, especially as someone may find it useful, or helpful.

The function, which I called Get-PowerShell.orgForumTopic, runs out to PowerShell.org and grabs the current topics (page one) from the PowerShell Q&A forum (http://powershell.org/wp/forums/forum/windows-powershell-qa). It only returns the Thread name and the URL, because, well, that’s what seemed useful and relevant at the time I wrote it (which was many months ago).

Function Get-PowerShell.orgForumTopic {
    [CmdletBinding()]
    Param ()

    Begin {
    } # End Begin.

    Process {
        (Invoke-WebRequest -Uri 'http://powershell.org/wp/forums/forum/windows-powershell-qa/' |
            Select-Object -ExpandProperty Links |
            Where-Object {$_.outerHTML -like '*http://powershell.org/wp/forums/topic*'} |
            Select-Object @{N='Thread';E={$_.innerHTML}},@{N='Url';E={$_.href}} |
            Select-Object -First 30)[(0..30 |
                ForEach-Object {
                    If (-not($_ % 2)) {
                        $_
                    }
                 }
            )]
    } # End Process.

    End {
    } # End End.
} # End Function: Get-PowerShell.orgForumTopic

Here’s what the results looked like in the ConsoleHost, near in time to when this post was published.

current-powershell.org-qa-forum-topics-01

While I never added any more to this function, I had some ideas: add the thread status, add the “started by user,” add the user that made the last post, add the number of posts per topic, and allow it to run against other PowerShell.org forum topics. It might’ve also been helpful to include additional pages, if requested by the user of the function, such as adding a -Pages parameter (-Pages 4).

Anyway, here it is. Beside being helpful to see the top PowerShell.org Q&A forum posts at a specific point in time, it’s an interesting example of reading from a webpage, of which I had minimal experience. So yeah, I probably learned something by doing this exercise.

If you want to do the same, then start by running the first command, Invoke-WebRequest -Uri ‘http://powershell.org/wp/forums/forum/windows-powershell-qa/’. Then add the pipe and first Select-Object command and run that. Then add the Where-Object command, and so on. This will allow you to see how I finally got to only returning the “Threads” and “Urls.” Take care.

Update: Since a site redesign at PowerShell.org, this function, no longer functions. I’m not sure that I’ll bother to update it — I don’t get the feeling that it was ever used by anyone — but I’ll keep this post up for anything else it may offer, that may be helpful in learning PowerShell.