CSV Break Down

I wrote a post recently, but a while back—it is kind of both now. It was about arrays and hash tables. I highlighted the differences between them and who knew, it turned out to be a success. There was a good amount of sharing of the post on Twitter and my same-day visitor count jumped up a fair amount above my average, which is typically around 175 visits per weekday. I do not know who these people are that do not use PowerShell on the weekends but get your priorities straight. Fine, maybe it is me.

The above image indicates I brought in nearly 400 visitors the same day it was published—not bad. There were people in the PowerShell subreddit using the words “array” and “hash table” incorrectly and whatever just took over in me and did what it does to help clarify things for people in the PowerShell community. I have said it before: I have no problem revisiting the things I have already learned about PowerShell. There is a good deal to know, and so any opportunity to learn something you have already learned—we will call that review, right—is okay. I review these concepts all the time; I can only know if I know it by checking! And usually, I have forgotten something. The whole, “If you don’t use it, you lose it” saying, applies to PowerShell, too.

There was another post I did more recently, and while it was not as popular, I liked it. It used a CSV file of links—think, URLs—to create a menu system that could open links as they were chosen inside PowerShell. Good enough for me to push my post count up by one. I will have been writing about PowerShell for eight years in June 2022. Just maybe I can get to 416 posts by then making my average one post per week for eight straight years. Even if I do not make it, to even be this close is a feat. I must like PowerShell.

As I was working with a CSV for that most recent post, I read another PowerShell Reddit post where someone was talking about arrays in conjunction with CSVs. I do not have a link for that post, and I am not sure how much it would apply, but it did get me wondering about the type of variable you end up with when you import a CSV. Do you know by heart!? Care to guess? Let’s bring back the CSV we used in the last post. It will work fine to get us the answers we need.

Title,Link,Note
PowerShell GitHub,https://github.com/PowerShell/PowerShell,
PowerShell GitHub Issues,https://github.com/PowerShell/PowerShell/issues,
PowerShell Docs GitHub,https://github.com/MicrosoftDocs/PowerShell-Docs,
Powershell Docs GitHub Issues,https://github.com/MicrosoftDocs/PowerShell-Docs/issues,
PowerShell Docs,https://docs.microsoft.com/en-us/powershell,
PowerShell Gallery,https://www.powershellgallery.com,
PowerShell Reddit,https://www.reddit.com/r/PowerShell,
Twitter Legends @jeffhicks,https://twitter.com/JeffHicks,
Twitter Legends @jsnover,https://twitter.com/jsnover,
Twitter Legends @concentrateddon, https://twitter.com/concentrateddon,
TechCrunch.com,https://techcrunch.com/,Tech News
Cnet.com,https://www.cnet.com/,Tech News
Gizmodo.com,https://gizmodo.com/,Tech News
9to5mac.com,https://9to5mac.com/,Tech News
Engadget.com,https://www.engadget.com/,Tech News
Wired.com,https://www.wired.com/,Tech News
TechRadar.com,https://www.techradar.com/,News
Axios.com,https://www.axios.com/,News

If we assume the above CSV file is stored in the below path, then the Import-CSV command will supply us with the below results. Further below in the example, we will import it a second time and store that in the $CSVFile variable. From there, we will take a closer look at it. What do we have once it is imported and stored in our variable?

[PS7.2.1][C:\] Import-Csv -Path 'C:\users\tommymaynard\Desktop\Links.csv'

Title                            Link                                                    Note
-----                            ----                                                    ----
PowerShell GitHub                https://github.com/PowerShell/PowerShell
PowerShell GitHub Issues         https://github.com/PowerShell/PowerShell/issues
PowerShell Docs GitHub           https://github.com/MicrosoftDocs/PowerShell-Docs
Powershell Docs GitHub Issues    https://github.com/MicrosoftDocs/PowerShell-Docs/issues
PowerShell Docs                  https://docs.microsoft.com/en-us/powershell
PowerShell Gallery               https://www.powershellgallery.com
PowerShell Reddit                https://www.reddit.com/r/PowerShell
Twitter Legends @jeffhicks       https://twitter.com/JeffHicks
Twitter Legends @jsnover         https://twitter.com/jsnover
Twitter Legends @concentrateddon https://twitter.com/concentrateddon
TechCrunch.com                   https://techcrunch.com/                                 Tech News
Cnet.com                         https://www.cnet.com/                                   Tech News
Gizmodo.com                      https://gizmodo.com/                                    Tech News
9to5mac.com                      https://9to5mac.com/                                    Tech News
Engadget.com                     https://www.engadget.com/                               Tech News
Wired.com                        https://www.wired.com/                                  Tech News
TechRadar.com                    https://www.techradar.com/                              News
Axios.com                        https://www.axios.com/                                  News

[PS7.2.1][C:\] $CSVFile = Import-Csv -Path 'C:\users\tommymaynard\Desktop\Links.csv'
[PS7.2.1][C:\] $CSVFile.GetType()

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

It is an array! That makes perfect sense, right!? It is a list of values. Each row in the CSV file is a single value, in the array. Sure, each row has the potential of containing multiple values (Title, Link, Note), but each containing row is one single element in the array. The two commands below do the same thing. They return the first row, or element, in the CSV file, as it is stored in the $CSVFile variable.

[PS7.2.1][C:\] $CSVFile[0]

Title             Link                                     Note
-----             ----                                     ----
PowerShell GitHub https://github.com/PowerShell/PowerShell

[PS7.2.1][C:\] $CSVFile | Select-Object -First 1

Title             Link                                     Note
-----             ----                                     ----
PowerShell GitHub https://github.com/PowerShell/PowerShell

Just putting this out there, but we did not even have to use the .GetType() method earlier, to know this was an array. The minute we were able to use a numeric index ([0]), we should have known. I do not know about you, but this got me wondering about two things: One, where did the header row go? As we saw, it had nothing to do with index [0], and two, if each row is an element in an array, what is inside each element? How are the previous columns represented? Let’s answer these.

Before we do, though, a quick reminder: There is really no need to know the things we are discussing to use CSV files and the Import-CSV command. This happens whether we care to know it or not. It is just that I wanted to know what was happening and so I brought you with me. Back to the discussion.

As we have seen, the header row, as we may want to picture it, is gone. It found a place though. Several even. It is stored once per array element, or line or row, if you will, within the $CSVFile variable. What does that even mean!? That is answered by viewing the type of object returned when we view a single element in the array. The below examples use the .GetType() method to return the type of just index [0]. It is a PSCustomObject! We saw the output of this in the last two examples, however, we may have not put it together from just that. Let’s pipe index [0] to the Get-Member command and see what we recognize.

[PS7.2.1][C:\] $CSVFile[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSCustomObject                           System.Object

[PS7.2.1][C:\] $CSVFile[0] | Get-Member

   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Link        NoteProperty string Link=https://github.com/PowerShell/PowerShell
Note        NoteProperty string Note=
Title       NoteProperty string Title=PowerShell GitHub

Each column of the CSV has become a NoteProperty in the PSCustomObject. Each name is a column header and each corresponding value has become a Definition. Each definition is a string that includes an entry such as <header>=<value>. Knowing that it makes sense that each of the following examples indicates that their type is a string.

[PS7.2.1][C:\] $CSVFile[0].Title
PowerShell GitHub
[PS7.2.1][C:\] $CSVFile[0].Title.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

[PS7.2.1][C:\] $CSVFile[0].Link.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

[PS7.2.1][C:\] $CSVFile[0].Note.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

While we do not need to know these things to import a CSV file and use it as a part of an assignment or project, there is value in understanding what the command does with each row and the values in each row of a CSV file.

Bye-Yee ISE – Hey Yo VS Code

It is official — as official as it is going to get anyway — but I am in the final six-month stretch of my eight-year consecutive run of writing about PowerShell. Not that I will be done after eight years, but I have been at this awhile. What is also official is that I have written the dumbest post title yet. I am going to go with it though.

Today I had an interesting “conversation” on a post in the PowerShell Subreddit. The full link, if you are interested, is included at the bottom of this post. But really, the important parts are all included here.

The post was about someone using Write-Host that should have been using Write-Output. That was not where I took the conversation, however. Instead, it became a discussion about moving away from the ISE to Visual Studio Code. I think that this is an important move, and so I thought I would include it all here because I think I made some good points, and maybe because I am after that 416 post count by June 2022. More the first, though.


Me: “Beyond that, OP, it is time to move away from the ISE, and probably Widows PowerShell too. 2¢


Not me, or the OP: “Nah, ISE4LYFE

Me: “[SMH] Think of your résumé: ISE vs. VS Code. Speaking of your résumé, did you do create that in Notepad and print it on a dot matrix? 😉 I loved the ISE once too, but I think it’s time…


Not me, or the OP: “I mean, I’m not old enough to have used dot matrix (well, kinda, 30)

My main issue with VSCode vs ISE is that it’s a UI mess from as soon as you install/open. Vs ISE, it’s just notepad ontop of powershell.

ISE is just easier for me for the quick and simple stuff, where i just bust out some script for a minute or two.

Granted, running the code-server is really nice


Me: “Oh, I get it. VS Code takes some time. It took me some to finally give up the ISE in full. And, I’m the same guy that paid attention to the PowerShell launch in 2006. One person, around that time (that I believe was associated with Microsoft), said that if you’re using VBScript, then continue to use it. That it is still going to be there, as in, in the operating system. That’s all it took for me to not pay attention to PowerShell! I bet the ISE is still going to be there too.

Cut to six years later, in 2012, and I finally let go of VBScript, which I loved, and forced myself to use PowerShell, which I now love. You’d have to pay me a large amount of money to even look at VBScript now. While I can’t say the same thing about the ISE, I’d miss VS Code an awful lot if the ISE is all I had. While it took some time and frustration and wanting to go back, I pushed on!

Speaking of, “if the ISE is all I had,” I recently wrote an AWS CloudFormation template to build out a vanilla Windows Server, server. It was beyond frustrating to build a new instance and only have the ISE. As a part of the CloudFormation, I installed PowerShell 7, installed VS Code, and installed the PowerShell VS Code extension. Again, I once loved the ISE.

Here is the PowerShell Subreddit post.

CSV Browser Links

Edit: At some point between publishing this post and now — 11:43 p.m. on Christmas Eve, Eve — I thought people consuming this post would benefit from seeing the menu open a CSV-based bookmark. A short gif has been added to the bottom of this post.

I do not know what my problem is, but for the last few years, I’ve been an anti-browser-bookmarks person. I don’t really have a good reason as to why, but I look forward to getting over it; I do. But until then, I have been saving all my links inside of a CSV file with the assumption that at some point I will write some PowerShell that will allow me to easily search my CSV file and then open links programmatically. Well, guess what, I finally wrote that.

The first thing sharing this project is going to require is a properly formatted CSV file we can both work from. No need to put mine in here; it would be full of links that are worthless to anyone but me. Copy and paste the comma-separated data below and save that off to a CSV file called Links.csv Be sure to note the path where you chose to save it, as we will work with it as a part of this post.

Title,Link,Note
PowerShell GitHub,https://github.com/PowerShell/PowerShell,
PowerShell GitHub Issues,https://github.com/PowerShell/PowerShell/issues,
PowerShell Docs GitHub,https://github.com/MicrosoftDocs/PowerShell-Docs,
Powershell Docs GitHub Issues,https://github.com/MicrosoftDocs/PowerShell-Docs/issues,
PowerShell Docs,https://docs.microsoft.com/en-us/powershell,
PowerShell Gallery,https://www.powershellgallery.com,
PowerShell Reddit,https://www.reddit.com/r/PowerShell,
Twitter Legends @jeffhicks,https://twitter.com/JeffHicks,
Twitter Legends @jsnover,https://twitter.com/jsnover,
Twitter Legends @concentrateddon, https://twitter.com/concentrateddon,
TechCrunch.com,https://techcrunch.com/,Tech News
Cnet.com,https://www.cnet.com/,Tech News
Gizmodo.com,https://gizmodo.com/,Tech News
9to5mac.com,https://9to5mac.com/,Tech News
Engadget.com,https://www.engadget.com/,Tech News
Wired.com,https://www.wired.com/,Tech News
TechRadar.com,https://www.techradar.com/,News
Axios.com,https://www.axios.com/,News

With our CSV in place, we can work through the below PowerShell and then take it for a test drive. Have a look and meet me below for a quick discussion, before we try it out.

function Find-Link {
    [CmdletBinding()]
    Param (
        [Parameter()]
        $Path = 'C:\Users\tommymaynard\Documents\CSVs\Links.csv',
        [Parameter()]$Title,[Parameter()]$Link,[Parameter()]$Note
    )

    #region Import CSV/filter.
    $AllLinks = Import-Csv -Path $Path
    $ FilterLinks = $AllLinks | Where-Object -FilterScript {
        $_.Title -like "*$Title*" -and $_.Link -like "*$Link*" -and $_.Note -like "*$Note*"
    }
    #endregion
    #region Create link menu.
    for ($i = 0; $i -lt $FilterLinks.Count; $i++) {
        "[$($i + 1)] $(($FilterLinks[$i]).Title)"
    } # for
    #endregion
    #region Prompt user.
    do {
        $Option = Read-Host -Prompt 'Link Number'
    } # do
    until ($Option -in (1..$FilterLinks.Count))
    Start-Process -FilePath "$($FilterLinks[$($Option - 1)].Link)"
    #endregion
}

The function is named Find-Link, and it includes a -Path parameter. This is the location of the CSV file. While the function contains a default path, it can be changed when the function is invoked by passing in a different value. The static entry in the function can also be permanently modified, as well — it is up to you.

Find-Link -Path '/users/landrews/Documents/bookmarks.csv'

When the file is imported, the entire CSV is assigned to the $AllLinks variable. Then, it runs a command against that variable, creating a new variable, to filter down the results using the value(s) potentially passed to three other parameters: -Title, -Link, and -Note. There is more than just the -Path parameter.  It does not check if any of these parameters were actually included, but it could have using $PSBoundParameters. Once we have a filtered list of links to display, we cycle through them using a for loop, which creates a menu of options. Here’s an example of one of the outputs created by invoking this command with the -Title parameter.

Find-Link -Title Twitter
[1] Twitter Legends @jeffhicks
[2] Twitter Legends @jsnover        
[3] Twitter Legends @concentrateddon
Link Number:

The final portion of the function is a do-until loop. This invokes Read-Host prompt until one of the available menu numbers is entered. When a number that is not included is entered, it will prompt the user again for a different value. Here is an example of that.

Find-Link -Title Twitter
[1] Twitter Legends @jeffhicks
[2] Twitter Legends @jsnover        
[3] Twitter Legends @concentrateddon
Link Number: 8
Link Number: 15
Link Number: 4
Link Number: 

When a value is selected that is included from the list, Start-Process invokes the corresponding link. Before we close out, here are a few more examples.

Find-Link -Link powershell
[1] PowerShell GitHub
[2] PowerShell GitHub Issues     
[3] PowerShell Docs GitHub       
[4] Powershell Docs GitHub Issues
[5] PowerShell Docs
[6] PowerShell Gallery
[7] PowerShell Reddit
Link Number:
Find-Link -Note News
[1] TechCrunch.com
[2] Cnet.com
[3] Gizmodo.com
[4] 9to5mac.com
[5] Engadget.com
[6] Wired.com
[7] TechRadar.com
[8] Axios.com
Link Number:
Find-Link -Note 'Tech News' 
[1] TechCrunch.com
[2] Cnet.com      
[3] Gizmodo.com   
[4] 9to5mac.com 
[5] Engadget.com
[6] Wired.com   
Link Number:   

We can combine the parameters too, to further filter the results.

Find-Link -Title GitHub -Link powershell
[1] PowerShell GitHub
[2] PowerShell GitHub Issues
[3] PowerShell Docs GitHub
[4] Powershell Docs GitHub Issues
Link Number: 

There may be a few things to add over time, but for now, this gives me what I wanted. It is better than navigating to the document and copying out a link — never. again.

To Rename a PowerShell Variable

I had one of those thoughts…  you know, a dumb one. As dumb as it might be, it gave some brief inspiration to try something new. It seemed possible and easy enough and so I set out to prove, that as dumb (and useless), as it is, that it could be mine.

First off, on my machine, as it sits here today, I have 40 commands that include the “rename” verb. Additionally, I have five commands that include the “Variable” noun. Do you know what I do not have, though? I will tell you in a second if you have not figured it out yet. If you have figured it out, I will also tell you.

[PS7.2.1][C:\] Get-Command -Verb Rename | Measure-Object | Select-Object -Property Count

Count
-----
   40

[PS7.2.1][C:\] Get-Command -Noun Variable | Measure-Object | Select-Object -Property Count

Count
-----
    5

A Rename-Variable command. And you know why!? Because it is dumb and mostly useless. Regardless, I made it a thing and we will discuss it during this post. In the end, I think it turned out to be a success even though I will never use it again after today. That is what I say now, anyway.

Let’s do this in reverse. We will start with the commands I will invoke and then we will look at my function. My first below command Rename-Variable will send in the variable named TestVariableZero and the name to which we want to rename it, TestVariableZeroZero. This will not work, as we cannot rename a variable that does not yet exist. The function will use Write-Warning to inform us that it cannot be renamed. I could have used this section of the if-else to create the variable, but that is not today’s assignment. The next two lines will one, create the variable TestVariableOne with the value ValueOne and two, return information about the variable, so we know it has been created.

We are using the Force parameter in the first of these two commands in case the variable already exists. New-Variable cannot create a variable that already exists by default.

Rename-Variable -Name TestVariableZero -NewName TestVariableZeroZero

New-Variable -Name TestVariableOne -Value ValueOne -Force
Get-Variable -Name TestVariableOne

Rename-Variable -Name TestVariableOne -NewName TestVariableTwo

Get-Variable -Name TestVariableTwo
Get-Variable -Name TestVariableOne

Looking upward, we have three more commands to discuss. This Rename-Variable command will attempt to rename the TestVariableOne variable to TestVariableTwo. After that operation, we have two Get-Variable commands. The first will prove that we now have a TestVariableTwo variable and the second, that we no longer have a TestVariableOne variable. How fun right,… even though renaming a variable should not require its own command. With that, let’s take a look at the function.

function Rename-Variable {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        $Name,
        [Parameter(Mandatory)]
        $NewName
    )

    if (Get-Variable -Name $Name -Scope Global -ErrorAction SilentlyContinue) {
        Set-Variable -Name $NewName -Value (Get-Variable -Name $Name -ValueOnly -Scope Global) -Scope Global
        Remove-Variable -Name $Name -Scope Global
    }
    else {
        Write-Warning -Message "Unable to locate a variable with the name of $Name."
    }
}

You will be glad to know that not a lot of work went into writing this; it is very simple and very straightforward. Our function accepts two parameters: Name and NewName. Simple. We use Name to search for our existing variable and NewName to change its name. Notice the use of the -Scope Global parameter and parameter value. Without it, the variables that we would be working with inside the function would only be scoped to the function; they would not exist outside of the function. Providing the variable exists, we use Set-Variable inside the function to create a new/set an existing variable with the new name and take the value from the old variable and assign it as the value to the new variable. Once the new variable has been created and the old value assigned to it, we remove the previously named variable. Let’s try out the commands we saw at the beginning of this post.

Here is what happens when we try and rename a variable that does not exist.

[PS7.2.1][C:\] Rename-Variable -Name TestVariableZero -NewName TestVariableZeroZero
WARNING: Unable to locate a variable with the name of TestVariableZero.

So we can test out our function, let’s create our TestVariableOne variable and prove to ourselves that it has been created and that it now exists.

[PS7.2.1][C:\] New-Variable -Name TestVariableOne -Value ValueOne -Force
[PS7.2.1][C:\] Get-Variable -Name TestVariableOne

Name                           Value
----                           -----
TestVariableOne                ValueOne

Now, it is time to rename our variable from TestVariableOne to TestVariableTwo. The first command does just that. The next couple of commands proves that TestVariableTwo now exists with the proper value and TestVariableOne does not exist.

[PS7.2.1][C:\] Rename-Variable -Name TestVariableOne -NewName TestVariableTwo
[PS7.2.1][C:\] Get-Variable -Name TestVariableTwo

Name                           Value   
----                           -----   
TestVariableTwo                ValueOne

[PS7.2.1][C:\] Get-Variable -Name TestVariableOne
Get-Variable: Cannot find a variable with the name 'TestVariableOne'.

And that is it. If you are ever looking to rename a variable, using a built-in -Variable command, you are not going to find it. You can roll your own, however, if you really want to do that.

Variables, and More About Note Properties

I have been thinking a lot about variables and note properties recently. You know that, if you read A Better Way to Solve the Same Problem. It sets the stage for what we are going to see today. The question that started that post was, whether or not I could add note properties to a single variable, such as PowerShell does with the $PROFILE variable. I could. I ended up creating a variable that held one URL when you returned the variable but also held four other URLs, one in each additional note property.

I want to take this further, and the best part is that all the things I was worried about trying to do myself, PowerShell just does for me without any thinking about it. Follow along and you will see.

Let’s set up the variable I was using last time to help this recap. Then we will create another variable and work with it to explore some new things I learned. Building this variable is being done a little differently than it was previously. It incorporates some concepts from another recent post, There is a Difference: Arrays Versus Hash Tables. First, we will assign our initial value to the variable.

$PSSites = 'https://docs.microsoft.com/en-us/powershell/scripting/overview'

Next, we will create a hash table that includes four key-value pairs. Each pair includes a website name and an associated URL, because as we learned in that post, a hash table is an associative array.

$Sites = @{
    GitHub  = 'https://github.com/PowerShell/PowerShell'
    Docs    = 'https://docs.microsoft.com/en-us/powershell/'
    Gallery = 'https://www.powershellgallery.com/'
    Reddit  = 'https://www.reddit.com/r/PowerShell/'
}

Following that, we will use the foreach statement to loop through the key-value pairs in the hash table, running Add-Member against each of them, and thusly adding each as a note property on our $PSSites variable.

foreach ($Site in $Sites.GetEnumerator()) {
    $PSSites = $PSSites | Add-Member -NotePropertyName $Site.Key -NotePropertyValue $Site.Value -PassThru
}

We can use Get-Member to ensure the previous command added the additional URLs as note properties.

$PSSites | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name    MemberType   Definition
----    ----------   ----------
Docs    NoteProperty string Docs=https://docs.microsoft.com/en-us/powershell/
Gallery NoteProperty string Gallery=https://www.powershellgallery.com/
GitHub  NoteProperty string GitHub=https://github.com/PowerShell/PowerShell
Reddit  NoteProperty string Reddit=https://www.reddit.com/r/PowerShell/

At this point, we have our own variable like the $PROFILE variable that contains multiple string values. We can access the note properties of $PSSites using dot notation.

$PSSites.GitHub
$PSSites.docs
$PSSites.Gallery
$PSSites.Reddit

https://github.com/PowerShell/PowerShell
https://docs.microsoft.com/en-us/powershell/
https://www.powershellgallery.com/
https://www.reddit.com/r/PowerShell/

Now, let’s create another variable and add some members to it, as well. These will be a little different and you will see that momentarily. We will start by assigning a string value to the variable, as we did above.

$TestingVariable = 'A string value'
$TestingVariable
A string value

Before we add the rest, I suppose I should remind everyone about my end goal written at the bottom of the first post linked above: Save the previous value of a variable to a note property when assigning a new value to the variable itself.

This would be much easier if we could guarantee that every new value assigned to the variable was a string, and therefore, every old value property would also be a string. My worries, as mentioned earlier, were about arrays, hash tables, ordered dictionaries, and other data structures I may have to save to a note property. Can other data structures like those be put in a note property? Let’s work through these data structures and see if we can get them into note properties, to begin with. In this example, we will add two arrays to two separate note properties, as Array01 and Array02. One of these will and one will not use the array, sub-expression operator (@()).

$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'Array01' -NotePropertyValue 1,2,3 -PassThru
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'Array02' -NotePropertyValue @(4,5,6) -PassThru

Here, we will create a hash table and store it in the $HashTable variable. We will then use that variable as a part of another Add-Member command.

$HashTable = @{Dad = 'David'; Mom = 'Betty'; Daughter = 'Janice'; Son = 'Bryan'}
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'HashTable' -NotePropertyValue $HashTable -PassThru

Finally, we’ll create an ordered dictionary and add it as a note property, as well. If the hash table takes, then it is probably safe to assume this will take, too.

$OrderedDictionary = [ordered]@{
    Monday    = 'Chipotle'
    Tuesday   = 'Pizza'
    Wednesday = 'Mahi Mahi';
    Thursday  = 'Ravioli'
    Friday    = 'Orange Chicken'
}
$TestingVariable = $TestingVariable | Add-Member -NotePropertyName 'OrderedDictionary' -NotePropertyValue $OrderedDictionary -PassThru

At this point, we should probably see what Get-Member returns about the note properties we have assigned to our $TestingVariable. We have yet to test that anything has actually been added so far, although we have not received any errors stating otherwise. In the below example, we can see the four, note properties that we have added. The best part, the definition property indicates it knew the data structure of these when they were added as note properties. The arrays show object[], the hash table, hashtable, and the ordered dictionary, OrderedDictionary.

$TestingVariable | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name               MemberType   Definition
----               ----------   ----------
Array01            NoteProperty Object[] Array01=System.Object[]
Array02            NoteProperty Object[] Array02=System.Object[]
HashTable          NoteProperty hashtable Hash Table=System.Collections.Hashtable
OrderedDictionary  NoteProperty OrderedDictionary Ordered Dictionary=System.Collections.Specialized.OrderedDictionary

Let’s take a closer look at each of our new note properties. For each of them, we will run GetType() against the note property and then return the value it has stored. When we return a note property that contains an array, it returns that. Same for the others; the data structure was recognized and retained when it was added as a member. And it is respected on the way out, too. I cannot tell you how grateful I am that I do not have to be the one to figure out the underlying data structure of a value going in and then coming back out. This is great news for some of the other things I have planned.

$TestingVariable.Array01.GetType()

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

$TestingVariable.Array01
1
2
3

$TestingVariable.Array02.GetType()

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

$TestingVariable.Array02
4
5
6

$TestingVariable.HashTable.GetType()       

IsPublic IsSerial Name                                     BaseType     
-------- -------- ----                                     --------     
True     True     Hashtable                                System.Object

$TestingVariable.HashTable

Name                           Value
----                           -----
Daughter                       Janice
Son                            Bryan
Dad                            David 
Mom                            Betty

$TestingVariable.OrderedDictionary.GetType()

IsPublic IsSerial Name                                     BaseType      
-------- -------- ----                                     --------      
True     True     OrderedDictionary                        System.Object 

$TestingVariable.OrderedDictionary

Name                           Value
----                           -----
Monday                         Chipotle
Tuesday                        Pizza
Wednesday                      Mahi Mahi
Thursday                       Ravioli
Friday                         Orange Chicken

Scoping Out the Scope

Scope is awesome; it really is. In my mind, it is a protectionary layer to ensure things like variables are somewhat protected from say, modification. It can become a necessary concept for people using Powershell to understand. As I worked through some ideas recently, I ended up writing a function — this has been known to happen. As I sat there and fully comprehended what I had written, I began to realize that it may be worth sharing, so others can see it too. Sometimes we just need a simple example and a corresponding explanation. That has kind of been my goal over the years. Anyway, let’s focus on this function and I’ll explain what it is we have here and what it is doing.

The function is named, Get-TheVariable and it contains four commands and four comments, although two of those comments are just dashed lines. I don’t usually write comments anything like this, but I really want to break up the commands into two segments, to make sure I am as clear as I can be, as we walk through this, together. Those segments are the Global scope and the Local scope, but we will get to that soon enough. Down beneath the function are two separate commands separated by a semi-colon. The first command just clears the screen — because sometimes I need that to stay clear-headed — and the second invokes the Get-TheVariable function. Have a peek at the function now and then we will discuss it further below.

function Get-TheVariable {
    # Set and then get the GLOBAL version of the "Variable1" variable.
    # ----------------------------------------------------------------
    Set-Variable -Name Variable1 -Value 'G' -Scope Global -Description 'Scope:Global'
    Get-Variable -Name Variable1 -Scope Global | Select-Object -Property Name,Value,Description

    # Set and then get the LOCAL version of the "Variable1" variable.
    # ---------------------------------------------------------------
    Set-Variable -Name Variable1 -Value 'L' -Scope Local -Description 'Scope:Local/Function'
    Get-Variable -Name Variable1 -Scope Local | Select-Object -Property Name,Value,Description
}

Clear-Host; Get-TheVariable

So what is happening here? Before we go there, know something about how Set commands usually work. They work like New commands, unless whatever we are to trying to create already exists. In that case, they just make modifications.

The first two commands create, or modify a variable (if it already exists) and then return the variable. It is named “Variable1,” its value — like what it stores — is “G,” it is globally scoped, and it includes a description indicating that. It is created by the function, but not inside the function. It is created outside of the function, in the global scope.

The second two commands kind of do the same things, They create, or modify a variable (if it already exists) and then return the variable. It is named “Variable1,” its value — what it stores — is “L,” it is locally scoped, and it includes a description indicating that. It is also created by the function, but it is inside of the function. It is created inside of the function, in the local scope. While these variables have many things in common, such as their name, they are different.

In the end — and sorry for all the repetition — there are going to be two variables with the same name. This is perfectly okay since they are going to be created in different scopes. In real life, I would recommend not using the same name for variables even if they are scoped differently. For this example, and for fully understanding this concept, it is important we do it this way.

The below output is created by our function. In the first line, we have information about our globally scoped “Variable1” variable that was created by the function, outside of the function. Because it is globally scoped it is going to persist outside of the function. In the second line, we have information about our locally scoped “Variable1” variable that was created by the function, inside the function. Because it is locally scoped, it is not going to persist outside the function.

During the invocation of the function, the function’s scope is the local scope. When the function’s invocation is done and over, the local scope is the global scope (again). The local scope is always the current scope.

Name      Value Description
----      ----- -----------
Variable1 G     Scope:Global
Variable1 L     Scope:Function

When the function has ended, the locally scoped “Variable1” variable stops existing. The global version, however, is still alive and well. Let’s prove it.

Get-Variable -Name Variable1 -Scope Global | Select-Object -Property Name,Value,Description
Name      Value Description 
----      ----- ----------- 
Variable1 G     Scope:Global

In the above example, using -Scope Global was not necessary. Why? It’s because, again, we’re in the global scope and so there’s no need to tell the command to check a scope we are already in.

But, look at this.

Get-Variable -Name Variable1 -Scope Local | Select-Object -Property Name,Value,Description
Name      Value Description 
----      ----- ----------- 
Variable1 G     Scope:Global

What, the local variable still exists!? Uh, no, not the one created by the function for inside the function. This is kind of a recap — maybe the third or fourth one now, who knows. Again, now that we are outside of the function and back in the global scope, the Local parameter value returns the globally scoped variable, too. See the Value and Description properties? They are the same as the global variable because the local variable is the global variable when we are in the global scope.

I am beginning to think I should have left this topic to someone else. Read it a few times and if maybe I have confused you — ask questions, Read more on about_Scopes. I feel good about this post, I just likely need to read it like 10 more times and ensure it mostly makes sense.

I do want to mention this real quick because it comes up often, and good for you for reading this far; it may just pay off. If you are invoking a function and you reference a variable that has not been created or defined in the function, PowerShell will look for it outside of its local scope. That’s to say that it will go looking for the variable you didn’t assign, in the global scope, also known as the parent scope. And with the introduction of that new term, the function’s scope has another name, too. It is the child scope. Okay, I’m done. Hopefully, this didn’t confuse anyone. Phew.

There is a Difference: Arrays Versus Hash Tables

After reading a short post on the PowerShell subreddit recently, I was compelled to write a quick post here about arrays and hash tables. The concepts are similar, but the two are different, meaning that — interruption! There have now been two posts where the OP didn’t know one versus the other. It is basic stuff, sure, but it is important stuff.

Anyway, talking about one when you mean the other isn’t going to be okay any longer — it really never was. While someone mentioned the incorrect usage in the thread, writing, for me, still felt necessary and potentially beneficial.

Instead of writing my own definitions, let’s use Microsoft’s. We’ll begin with an array: “Data structures designed to store collections of items.“ If you think of a variable, back when you learned about those the first time, you probably learn how to store a single value in your variable. Okay good. This time we’re going to store multiple values in a single variable. That is what this data structure provides.

We are beginning with a simple variable that contains a simple, single value. Not much to it.

[PS7.2.0][C:\] $SingleValue = 'One thing'
[PS7.2.0][C:\] $SingleValue
One thing
[PS7.2.0][C:\] $SingleValue.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

Now, let’s work with arrays. We’re still going to use a single variable, we’re just going to load it up with a different data structure that’s going to let us store multiple values in it. We actually don’t have to do much to make this happen, so here we go.

In this example, we’re creating a variable named $Array01. We’re storing the numeric values of 1, 2, and 3 inside the variable as an array.

[PS7.2.0][C:\] $Array01 = 1,2,3
[PS7.2.0][C:\] $Array01
1
2
3
[PS7.2.0][C:\] $Array01.GetType()

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

Well, that was easy. This next example is the same as above, however, it includes @() around our numeric values. This operator — the array sub-expression operator — takes whatever is inside it and makes it into an array.

[PS7.2.0][C:\] $Array02 = @(4,5,6)
[PS7.2.0][C:\] $Array02
4
5
6
[PS7.2.0][C:\] $Array02.GetType()

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

The array sub-expression operator will even make an array of nothing.

[PS7.2.0][C:\] $Array03 = @()
[PS7.2.0][C:\] $Array03
[PS7.2.0][C:\] $Array03.GetType()

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

Here is one you probably do not see often. The comma in front of the 7 will indicate to PowerShell to create an array that contains a single value, which we will prove when we start returning the count of our various arrays.

[PS7.2.0][C:\] $Array04 = ,7
[PS7.2.0][C:\] $Array04
7
[PS7.2.0][C:\] $Array04.GetType()

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

[PS7.2.0][C:\] $Array04.Count
1
[PS7.2.0][C:\] $Array03.Count
0
[PS7.2.0][C:\] $Array02.Count
3
[PS7.2.0][C:\] $Array01.Count
3

Do you get it? It is a data structure designed to accommodate multiple values. You did not see it here yet, but arrays can hold strings (words or sentences), just as easily as numbers. You can even mix them in the same array and store them in a single variable.

This array, stored in the $Array05 variable contains five different colors.

[PS7.2.0][C:\] $Array05 = 'black','red','green','blue','brown'
[PS7.2.0][C:\] $Array05
black
red
green
blue
brown

Each item in an array can be referenced by an index — a number that represents its location. It is a positional place in line. The first element, or item in the array — black in our case — is in index zero. This is because arrays are always zero-based. This means red would be in index 1, green in index 2, blue in 3, and brown in 4. Zero through four is five, as in five total elements, or values, in our array. Let’s verify.

[PS7.2.0][C:\] $Array05[0]
black
[PS7.2.0][C:\] $Array05[1]
red
[PS7.2.0][C:\] $Array05[2]
green
[PS7.2.0][C:\] $Array05[3]
blue
[PS7.2.0][C:\] $Array05[4]
brown
[PS7.2.0][C:\]

Let’s try some other things! While not often used in my day-to-day, we can perform arithmetic on the indexes. We can even use the range operator (..) and go forward and backward through our elements. What else… Oh, let’s try some negative indexes numbers, too. Those work in reverse from the end of the array to the beginning.

[PS7.2.0][C:\] $Array05[3-3]
black
[PS7.2.0][C:\] $Array05[2+1]
blue
[PS7.2.0][C:\] $Array05[0..4]
black
red
green
blue
brown
[PS7.2.0][C:\] $Array05[4..0]
brown
blue
green
red
black
[PS7.2.0][C:\] $Array05[-1]
brown
[PS7.2.0][C:\] $Array05[-2]
blue
[PS7.2.0][C:\] $Array05[-3]
green
[PS7.2.0][C:\] $Array05[-4]
red
[PS7.2.0][C:\] $Array05[-5]
black

Hash tables have a good number of similarities to arrays, and perhaps that is the reason why there is some confusion. Microsoft even uses the word array to describe them: “A hash table, also known as a dictionary or associative array, is a compact data structure that stores one or more key/value pairs.” I don’t use the words associative array often enough, but I should; I like it.

To the person on Reddit that called hash tables, hashmaps, and just maps: No. I believe that term is a carryover from maybe Java. In my experience, I have not heard it in the PowerShell community.

Anyway, arrays and associative arrays are both data structures. They can both hold multiple values, and they can both be stored in a single variable. The difference is keys and values. Arrays hold single items in each index, while a hash table holds a key-value pair in each.

Before we fill this up, let’s create an empty hash table. Notice the difference between this and the array. If it is used for an array, the operator will always be an instant giveaway: @() is for arrays, and @{} is for hash tables. Notice my liberal use of the GetType() method in this post; it’ll help you make object determinations, so you can always know which you’re working with, without having to view, or see, the data stored in the data structure.

[PS7.2.0][C:\] $Hash01 = @{}
[PS7.2.0][C:\] $Hash01
[PS7.2.0][C:\] $Hash01.Count
0
[PS7.2.0][C:\] $Hash01.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Hashtable                                System.Object

Now, let’s populate a hash table… in VS Code. Somethings I would just rather not demonstrate in the console.

$Hash02 = @{
    Dad = 'David'
    Mom = 'Betty'
    Daughter = 'Janice'
    Son = 'Bryan'
}
$Hash02
Name                           Value
----                           -----
Son                            Bryan
Dad                            David
Mom                            Betty
Daughter                       Janice

This also could have been written in a couple of other ways.

$Hash02 = @{
    Dad = 'David'; Mom = 'Betty'; Daughter = 'Janice'; Son = 'Bryan'
}
$Hash02 = @{
    Dad      = 'David'
    Mom      = 'Betty'
    Daughter = 'Janice'
    Son      = 'Bryan'
}

While hash tables have indexes, they’re non-numeric. Actually, I am not even sure I like the term index when working with hash tables. I suggest we call it what they are: keys. Maybe you noticed, but the order in which we created our hash table was not the order in which it was returned. A numeric value here would not help us anyway. Instead, we’re going to use the keys as our indexes to return our values.

$Hash02['Dad']
David
$Hash02['Daughter']
Janice

There is a good possibility that you are probably going to want to loop through and display all the values in your hash table. It’s a little out of place, but we’ll do that with one of our arrays here, too. Using the GetEnumerator() method allows us to get to the key and value, values out of our hash table. In this first, hash table example we are using the key and associated value in a string. This requires the use of the subexpression operator ($()), so that we can display our values within the string.

foreach ($Person in $Hash02.GetEnumerator()) {
    "The $($Person.Key) is $($Person.Value)."
}

The Son is Bryan
The Dad is David
The Mom is Betty
The Daughter is Janice

We are using the subexpression operator in the string created by looping over our array, too.

$Array05 = 'black','red','green','blue','brown'
foreach ($Color in $Array05) {
	"Is your favorite color $($Color)?"
}
Is your favorite color black?
Is your favorite color red?
Is your favorite color green?
Is your favorite color blue?
Is your favorite color brown?

This has been a lengthy post — who knew this would happen!? There is one last thing I want to mention, and that is about creating an ordered hash table, or dictionary. We tend to use the dictionary term when we order a hash table.

$Hash03 = [ordered]@{
    Monday    = 'Chipotle'
    Tuesday   = 'Pizza'
    Wednesday = 'Mahi Mahi'
    Thursday  = 'Ravioli'
    Friday    = 'Orange Chicken'
}
$Hash03

Name                           Value
----                           -----
Monday                         Chipotle
Tuesday                        Pizza
Wednesday                      Mahi Mahi
Thursday                       Ravioli
Friday                         Orange Chicken

Because this is ordered, we can use numeric indexes again. Keep in mind, however, that doing this is only going to return the values and not the keys, as well. It may even momentarily confuse you into thinking this is an array, not a hash table, or an associative array. Maybe stick to using a foreach at this point. Do not forget the GetType() method to ensure you know your data structures.

$Hash03[0..4]
Chipotle      
Pizza
Mahi Mahi     
Ravioli       
Orange Chicken

$Hash03.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------     
True     True     OrderedDictionary                        System.Object

Okay, I am stopping here. There’s plenty to know about these two data structures, and at this point, it is more than I’m willing to share. Even so, this should provide a decent breakdown to go along with the Microsoft documentation and other information that’s available to consume on this topic. Know what you are talking about.

A Better Way to Solve the Same Problem

There once was a time I wrote a post. Realistically, there have been many times. Like many posts though, I was proud of it. I can’t show it to you, as it wasn’t written on my site and it has since been lost. I am actually kind of embarrassed by it these days, so I’m mostly glad it is gone.

Someone, on some forum, wanted to know the previous value of a variable. Not the current value, but the last value it had been assigned before its current value. My idea then, which seems ridiculous now, was to store its last value in its description property. If you don’t know, variables have a writable description property; really, they do. It is a pretty nice feature, were anyone to actually use it. I don’t imagine many people do. I know about it, and honestly, I don’t use it.

Instead of using a variable’s description property to store its last value, I got to thinking about the $PROFILE variable and how it works. Let’s have a look, but before we do, let’s make sure we all know what this variable holds. This variable contains a path to your profile script, whether your profile script exists or not.

[PS7.2.0][C:\] $PROFILE
C:\Users\tommymaynard\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

The profile script is invoked when PowerShell launches, allowing you to create variables, define functions, and modify your PowerShell environment to suit you. In a recent post, I discussed creating a work-based ASCII image and sending in some keys. This made sure the first command I want to invoke was ready when PowerShell launched; all I had to do was press Enter. Oh, and here is every time I mentioned the $PROFILE variable on tommymaynard.com going all the way back to 2014. It has come up quite a few times over the years.

We have seen the value the $PROFILE variable holds, but there’s more. Let’s run it another way.

[PS7.2.0][C:\] $PROFILE | Select-Object -Property *

AllUsersAllHosts : C:\Program Files\PowerShell\7\profile.ps1
AllUsersCurrentHost : C:\Program Files\PowerShell\7\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : C:\Users\tommymaynard\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost : C:\Users\tommymaynard\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
Length : 71


Using the Select-Object cmdlet produced three additional paths: AllUsersAllHosts, AllUsersCurrentHost, and CurrentUserAllHosts. I am not going to go into what these all mean, but host refers to the program hosting the PowerShell engine. Read more about profiles here. With these three additional paths, there are four total locations where a profile script can be located and loaded by default. So, what do we have here? We have a variable with the ability to hold more than a single value (without making it an array or a hash table). That’s just how this variable works and I want the same thing for my example variable.

In this example, we’ve returned the same values as we did above, however, in this instance, we’re using the Get-Member cmdlet. I think it is important to see the values returned a couple of different ways.

[PS7.2.0][C:\] $PROFILE | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name                   MemberType   Definition
----                   ----------   ----------
AllUsersAllHosts       NoteProperty string AllUsersAllHosts=C:\Program Files\PowerShell\7\profile.ps1
AllUsersCurrentHost    NoteProperty string AllUsersCurrentHost=C:\Program Files\PowerShell\7\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    NoteProperty string CurrentUserAllHosts=C:\Users\tommymaynard\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost NoteProperty string CurrentUserCurrentHost=C:\Users\tommymaynard\Documents\PowerShell\Microsoft.PowerShell_profile.ps1

Let’s start the remainder of this post by creating my example variable. Like the $PROFILE variable, we’re just going to work with string values.

[PS7.2.0][C:\] $PROFILE.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

[PS7.2.0][C:\] 
[PS7.2.0][C:\] $PSSites = 'https://docs.microsoft.com/en-us/powershell/scripting/overview'
[PS7.2.0][C:\] $PSSites
https://docs.microsoft.com/en-us/powershell/scripting/overview
[PS7.2.0][C:\] 
[PS7.2.0][C:\] $PSSites.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

[PS7.2.0][C:\]

At this point, we’ve assigned a docs.microsoft.com URL directly to the $PSSites variable. Now, let’s take a few new values and assign each of them as a NoteProperty on the same variable. This is done by using the Add-Member cmdlet. We assign the current value of the $PSSites variable back to the $PSSites variable, while also adding the new note property.

[PS7.2.0][C:\] $PSSites = $PSSites | Add-Member -NotePropertyName 'GitHub' -NotePropertyValue 'https://github.com/PowerShell/PowerShell' -PassThru
[PS7.2.0][C:\] $PSSites = $PSSites | Add-Member -NotePropertyName 'Docs' -NotePropertyValue 'https://docs.microsoft.com/en-us/powershell/' -PassThru
[PS7.2.0][C:\] $PSSites = $PSSites | Add-Member -NotePropertyName 'Gallery' -NotePropertyValue 'https://www.powershellgallery.com/' -PassThru
[PS7.2.0][C:\] $PSSites = $PSSites | Add-Member -NotePropertyMembers @{Reddit='https://www.reddit.com/r/PowerShell/'} -PassThru

You may have noticed, but in the last of the above four examples, we used the NotePropertyMembers parameter with a hash table for the associated value. This option allows us to supply both the name and value at the same time. It does exactly what the first three examples did — it is just another way to do it.

I should mention the PassThru switch parameter. This is a requirement because Add-Member cannot add types to strings, so this will generate an object. It’s worth keeping this in mind. Without it, you’ll drive yourself crazy trying to determine why it isn’t working — always, read the help!

Using dot notation we can see the additional values (note properties), that our variable has been assigned.

[PS7.2.0][C:\] $PSSites.GitHub
https://github.com/PowerShell/PowerShell
[PS7.2.0][C:\] 
[PS7.2.0][C:\] $PSSites.Docs
https://docs.microsoft.com/en-us/powershell/
[PS7.2.0][C:\] 
[PS7.2.0][C:\] $PSSites.Gallery
https://www.powershellgallery.com/
[PS7.2.0][C:\] 
[PS7.2.0][C:\] $PSSites.Reddit
https://www.reddit.com/r/PowerShell/

Like we did with the $PROFILE variable, we can use the Select-Object cmdlet to return all of our URLs. We can also use Get-Member.

[PS7.2.0][C:\] $PSSites | Select-Object -Property *

GitHub   : https://github.com/PowerShell/PowerShell
Docs     : https://docs.microsoft.com/en-us/powershell/
Gallery  : https://www.powershellgallery.com/
Reddit   : https://www.reddit.com/r/PowerShell/
Length   : 62


[PS7.2.0][C:\] $PSSites | Get-Member -MemberType NoteProperty

   TypeName: System.String

Name     MemberType   Definition
----     ----------   ----------
Docs     NoteProperty string Docs=https://docs.microsoft.com/en-us/powershell/
Gallery  NoteProperty string Gallery=https://www.powershellgallery.com/
GitHub   NoteProperty string GitHub=https://github.com/PowerShell/PowerShell
Reddit   NoteProperty string Reddit=https://www.reddit.com/r/PowerShell/

Now, with the PowerShell site URLs stored in the $PSSites variable, I can easily open whichever website I prefer using Start-Process and the corresponding property. No real idea if I’ll use this, but I’m grateful to know how to use a note property to store additional data along with a simple string variable.

[PS7.2.0][C:\] Start-Process -Path $PSSites.Gallery

Join me next time, perhaps, if I can devise a way to save the previous value of a variable to a note property when assigning a new value to the variable itself. It will take a little thought and work, but for now, I think it is possible.

Pass Range to Function Parameter

I had one of those thoughts, where you must know how something is handled in PowerShell, as soon as you possibly can. Maybe it happens to you, too. Unfortunately for me, it happen just before I decided to go to bed a few days ago, which kept me awake a bit longer than expected.

The question was this: Can I pass a range operator to a PowerShell function as a parameter? As you may know, a range operator alone isn’t worth much, so essentially the question was, Can I pass two numbers and range operator to a PowerShell function as a parameter?

Let’s start with an example of the range operator (..). It’s two dots between two numbers, and it essentially says, “provide me every number in this range.” The below example uses the numbers 1 through 10. Once executed, it returns the numbers 1, 2, 3, 4, etc., all the way up to 10.

[PS7.2.0][C:\] 1..10
1
2
3
4
5
6
7
8
9
10
[PS7.2.0][C:\]

A couple of things: One, the range operator returns an array, and two, as of PowerShell version 6, it works with letters, or characters, as well. Be sure to use single quotes around your string values, or it’ll think you are passing in command names, which will not work.

[PS7.2.0][C:\] (1..10).GetType()

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

[PS7.2.0][C:\] 'a'..'e'
a
b
c
d
e
[PS7.2.0][C:\] 

Let’s review this function first. This is my New-RangeOption function. It requires that two integers be supplied. One for the RangeFirstNumber parameter and one for the RangeLastNumber parameter. The idea here is that we don’t believe a range can be supplied to a function, as a parameter, and we must create it ourselves inside of our function. We do more than just that, however. As I was just testing, we display each number and then we display the type of each parameter value. They are cast as integers, so that really wasn’t necessary. Additionally, we create the range (add the operator between the two integers), display the results, and return the type of the range, as well. This should make sense when you see the below results.

function New-RangeOption {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [int]$RangeFirstNumber,

        [Parameter(Mandatory)]
        [int]$RangeLastNumber
    )
    $RangeFirstNumber;  $RangeLastNumber
    $RangeFirstNumber.GetType(); $RangeLastNumber.GetType()

    $InFunctionRange = $RangeFirstNumber..$RangeLastNumber
    $InFunctionRange
    $InFunctionRange.GetType()
}
New-RangeOption -RangeFirstNumber 1 -RangeLastNumber 10
1
10

IsPublic IsSerial Name                                     BaseType        
-------- -------- ----                                     --------        
True     True     Int32                                    System.ValueType
True     True     Int32                                    System.ValueType
1
2
3
4
5
6
7
8
9
10
True     True     Object[]                                 System.Array

This option works, but we can do better. This, below example, has only a single parameter, Range. We send in our range, which includes the two numeric values and the range operator. It outputs the range — all the numbers — the type, and the count, as in the count of the number of members in the array. It’s 10, one for each value from 1 to 10.

function New-BetterRangeOption {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [array]$Range
    )

    $Range
    $Range.GetType()
    $Range.Count
}
New-BetterRangeOption -Range (1..10)
1
2
3
4
5
6
7
8
9
10

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

There is an important thing to notice here, and that’s the group operator — the parentheses around 1..10. It is used to pass in the digits with the range operator. Without this operator, it would’ve been a mess — let’s see.

New-BetterRangeOption -Range 1..10
1..10

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

In this last output, it doesn’t display the actual digits within the range. While it still thinks it’s an array, it’s an array that contains 1..10 as a single, string member. That’s it though; curiosity satisfied. We can pass in a range, using the range operator (and the group operator), to our functions. Now, I only need a reason to do it.

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