Author Archives: tommymaynard

Write Functions, Not Scripts – Part II

In Part I of this series, I was supposed to begin offering reasons as to why someone would want to stop writing scripts, and start writing functions. I got a bit distracted. Instead, we discussed making an easy, and mostly meaningless, function. The reasoning for this, was because I get this feeling that moving from a script writer to a function writer can be a unsettling endeavor for some. I liken it to bicycle training wheels. They’re so easy, and reliable. It’s just not until you take them off, and get your balance, that you suddenly realize all the things they actually kept your from doing. Learn to balance this bike, and then you can start jumping off your makeshift ramp in the middle of the cul-de-sac, instead of just riding around it.

Speaking of cul-de-sacs, here’s another distraction. It’s our view from Friday morning. That’s not what it usually looks like in Tucson, AZ. Fog is really, really rare. In one of these follow ups, I’ll be sure to include our typical desert view, uninterrupted by winter type weather events, even on most days in the winter.

Functions can get advanced, sure, but once you know a few things about them, you’ll never look back. You’ll continue to learn more and more about them. There’s a whole new level of being proud of your work, as you transition to function writing. You’re going to sleep better at night knowing you’ve written five singled-purposed functions vs. that one long script you secretly hate to open and troubleshoot. Maybe, it’s not even a secret.

Look, at the end of the day, a function is nothing more than a scriptblock you can invoke by entering its name. You run commands all the time, maybe even a few, one right after the other. Now you can do the same thing, but by only typing a function’s name and pressing enter.

Let’s assume there’s was a point in time I often wanted to know the date, the system drive letter, the current PowerShell host major version, and return a random number between 1 and 20. If I grew tired of doing this manually, I could create a function to do this for me. It’s not likely this would ever be useful outside this post, but it certainly helps highlight what using a function does. It’s essentially four commands in one. Easy.

Function Start-RandomStuff {
    "Date : $(Get-Date)"
    "System Drive : $env:SystemDrive"
    "Host Major Version : $((Get-Host).Version.Major)"
    "Random Number (1-20) : $(Get-Random -Minimum 1 -Maximum 20)"
}
PS > Start-RandomStuff
Date : 02/13/2018 22:58:00
System Drive : C:
Host Major Version : 5
Random Number (1-20) : 3

Think of functions as a wrapper. You can wrap the execution of various (related) commands by running one command. This isn’t to say our example ran related commands so much; it really didn’t. The point is to keep in mind that functions should be single purposed. They’re tight, short, and to the point. If you start wondering if you’re adding too much procedural code to your function, you probably already did. If you keep adding to the function you’re currently writing, then you better be able to explain why.

Oh my, I’ve done it again! I haven’t really hit those reasons as to why functions, over scripts. Or perhaps I have a little. I do have some specifics topics, and for the second time, let’s hope the next part of this series, gets serious. There really are a few specific things I want to share — reasons why functions are all you want to be writing.

Back soon. And hopefully, with a non winter picture of the cul-de-sac.

Write Functions, Not Scripts – Part I

Seriously. If you’re not writing PowerShell functions yet, it’s. time. to. begin. It’s been time.

I’ve decided I should begin to compile some potentially influencing reasons why you’d want make this move. Additionally, I also thought that maybe we need to first, give those out there, that haven’t been doing it, a little push by example. Before we get to some reasons why functions over scripts, do this. Next time you start to write a PowerShell solution, just type what’s been written in the below example.

Function <Verb>-<SingularNoun(s)InCamelCase> {

}

Now, in place of <Verb>, enter an approved verb from the results of the Get-Verb cmdlet. As of today, there’s 98 approved verbs in Windows PowerShell 5.1, and an even 100 in PowerShell 6.0.*

PS > Get-Verb
Verb        Group
----        -----
Add         Common
Clear       Common
Close       Common
Copy        Common
Enter       Common
Exit        Common
Find        Common
Format      Common
Get         Common
Hide        Common
Join        Common
Lock        Common
...

Once you’re done replacing <Verb> with your approved verb, replace <SingularNoun(s)InCamelCase> with a singular noun, or set of singular nouns in camel case. Here’s some examples: Set-ADDisabledUser, Test-KMSServer, Get-TimeZone, Read-Log, etc. We don’t need plural nouns even if your function is going to run against multiple users, servers, timezones, or logs.

Let’s say you chose Get-TimeZone. Before we actually begin to write timezone related code, let’s make sure our function actually works. Copy and paste the below function to the ISE, or Visual Studio Code, or even the ConsoleHost, and press F8 or Enter (depending on which host you chose). What happens?

Function Get-TimeZone {
    '---->Testing our Get-TimeZone function.<----'
}

Nothing, right? It didn’t even work!

That’s not exactly true. Something did happen. Running the code that makes up a function, doesn’t invoke, or execute, the function. It adds it to memory, or, it adds it to the current PowerShell session. It gets it ready to be run, but how do we know? Let’s check our Function PSDrive. In this example, we’ll see the full process of the function not yet existing, it being created, and then it being available to use.

Here, we’ll verify the function doesn’t yet exist.

Get-Item -Path Function:\Get-TimeZone
Get-Item : Cannot find path 'Function:\Get-TimeZone' because it ...

Here, we’ll add the function to our session/memory.

Function Get-TimeZone {
    '---->Testing our Get-TimeZone function.<----'
}

And here, we’ll verify that it does, now exist.

Get-Item -Path Function:\Get-TimeZone

CommandType     Name                     Version    Source
-----------     ----                     -------    ------
Function        Get-TimeZone

Since it’s now available, let’s try it out.

PS > Get-TimeZone
---->Testing our Get-TimeZone function.<----

A function has a name that we can use to run its included code. The thing is, the code (our function) has to have been defined, or created, before we try and use it. Simple. So, before a function can be used… it must be loaded, or added to memory. Keep all of this in mind.

I want to show one other little secret that’s going to be helpful as you transition to using and writing functions. I know, I know, we were supposed to discuss why you want to use functions, not how to use them. I think it’s important to know a couple things before we really get started, and thus the minor derailment on today’s post. When we get back next time, we’ll immediately jump into the reasons to use functions.

Let’s say we have a file on our Desktop called MyFunctions.ps1. And, let’s say it contains three functions, like this:

Function Test-One {
    'This is the Test One function.'
}
Function Test-Two {
    'This is the Test Two function.'
}
Function Test-Three{
    'This is the Test Three function.'
}

How do we get these functions into our session!? They’re in a file… In a PowerShell host program, perhaps the ConsoleHost, dot source the file as in the below example. You can use either the full path, or navigate to the Desktop first, and just dot source the file by itself (no path). But what’s dot source, right? It’s a dot. Seriously.

PS > Get-Item -Path Function:\Test-One
Get-Item : Cannot find path 'Function:\Test-One' because it ...
PS > . C:\Users\tommymaynard\Desktop\MyFunctions.ps1
PS > Get-Item -Path Function:\Test-One

CommandType     Name                     Version    Source
-----------     ----                     -------    ------
Function        Test-One

PS > Test-One
This is the Test One function.
PS > Test-Two
This is the Test Two function.
PS > Test-Three
This is the Test Three function.

Had we not used the full path, we would have had to navigate to the file, and then as mentioned, dot source the file. This below example would also add the three functions to our current PowerShell session.

PS > Set-Location -Path C:\Users\tommymaynard\Desktop
PS > . .\MyFunctions.ps1

Okay, we’re done for today, and again, I’m promise we’ll go into why you want to use functions over scripts… now that you know functions aren’t even scary.

* If you’re curious which two additional approved verbs PowerShell 6.0 has over PowerShell 5.1, then take a look below to find out.

PS > ($PSVersionTable).PSVersion.ToString()
6.0.0
PS > (Get-Verb).Verb | Out-File .\Desktop\6.0Verbs.txt
PS > ($PSVersionTable).PSVersion.ToString()
5.1.14409.1012
PS > (Get-Verb).Verb | Out-File .\Desktop\5.1Verbs.txt
PS > 
PS > $Ref = Get-Content -Path .\Desktop\6.0Verbs.txt
PS > $Dif = Get-Content -Path .\Desktop\5.1Verbs.txt
PS > Compare-Object -ReferenceObject $Ref -DifferenceObject $Dif

InputObject SideIndicator
----------- -------------
Build       <=
Deploy      <=

Part II is now available!

ValidateSet Default Parameter Values

The example code I’m going to include below, I’ve used before. I really like it and so I’m going to give it place here on my website, in case it may ever be helpful for you, and maybe even me again.

The first time I used something like this code example was for a function that created random passwords. By default, that function’s CharacterType parameter would include the four values Lowercase, Number, Symbol, and Uppercase. By using the parameter, you can specify which of the values you actually use, if you didn’t want to use all four. By default, the parameter included them all.

We are defining an advanced function called Test-Function with a single parameter called Type. This parameter uses ValidateSet in order that it’ll only ever accept four different parameter values, for the Type parameter. Additionally, the Type parameter actually includes a default value that includes all four of the values: FullAccess, SendAs, SendOnBehalf, and Calendar. If you ever find yourself needing an All parameter value, just use this option instead; you don’t actually need an All parameter value, you just need to include all the possible values as the default.

After the parameter inclusion, the function begins with a Foreach language construct that will evaluate each Type that been included, whether it’s all four by default, all four because someone use the parameter and entered all four possibilities (not necessary, obviously), or something less than the four options.

Inside each iteration thought the Foreach there’s a Switch statement that will be evaluated. Based on the current type value, its value will be displayed in a string that includes a hard coded value to ensure it’s correct.

Function Test-Function {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateSet('FullAccess','SendAs','SendOnBehalf','Calendar')]
        [string[]]$Type = ('FullAccess','SendAs','SendOnBehalf','Calendar')
    )

    Foreach ($T in $Type) {
        Switch ($T) {
            'FullAccess' {
                "Doing $T stuff (should match FullAccess)."
            }
            'SendAs' {
                "Doing $T stuff (should match SendAs)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match SendOnBehalf)."
            }
            'Calendar' {
                "Doing $T stuff (should match Calendar)."
            }
        } # End Switch.
    } # End Foreach.
}

PS > Test-Function
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing Calendar stuff (should match Calendar).

PS > Test-Function -Type 'FullAccess','SendAs','SendOnBehalf','Calendar' # Same as above.
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing Calendar stuff (should match Calendar).

PS > Test-Function -Type 'FullAccess','SendAs','SendOnBehalf'
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).
Doing SendOnBehalf stuff (should match SendOnBehalf).

PS > Test-Function -Type 'FullAccess','SendAs'
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).

PS > Test-Function -Type 'Calendar','FullAccess','SendAs'
Doing Calendar stuff (should match Calendar).
Doing FullAccess stuff (should match FullAccess).
Doing SendAs stuff (should match SendAs).

PS > Test-Function -Type 'SendOnBehalf','FullAccess','Calendar'
Doing SendOnBehalf stuff (should match SendOnBehalf).
Doing FullAccess stuff (should match FullAccess).
Doing Calendar stuff (should match Calendar).

Nothing down here, but thanks for reading all the way! Actually, here’s a bonus if you didn’t already know it. Those hard coded statements inside the Switch statement, could’ve been written a little differently.

This:

...
            'FullAccess' {
                "Doing $T stuff (should match FullAccess)."
            }
            'SendAs' {
                "Doing $T stuff (should match SendAs)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match SendOnBehalf)."
            }
            'Calendar' {
                "Doing $T stuff (should match Calendar)."
            }
...

could’ve actually been this:

...
            'FullAccess' {
                "Doing $T stuff (should match $_)."
            }
            'SendAs' {
                "Doing $T stuff (should match $_)."
            }
            'SendOnBehalf' {
                "Doing $T stuff (should match $_)."
            }
            'Calendar' {
                "Doing $T stuff (should match $_)."
            }
...

Clear Screen After Every Command

Here we go again. I just rescued another draft post, fixed it up enough to share, and published it. Again, this was one of those posts that just never made it past my drafts for one reason or another. I think I figured out what I wasn’t doing correctly, so I’m ready to hand it off.

Never know when I’m going to get inspired to write. Well, I just did and I’m not even sure how it started. All of sudden, I began thinking about ways in which to mess with someone using PowerShell. Perhaps it’s that April Fool’s Day just passed. It’s actually coming, this post is so overdue. I thought to myself, can I get the ConsoleHost to clear each time something is entered? Yeah, of course I can: the prompt function. Been here, done that, just not with such cruel, and obnoxious intentions.

Now to figure it out, for you (and next April Fool’s Day, perhaps). The idea is not to completely modify the prompt function, so as to tip off the user. Instead, the idea is to get their prompt — customized or not — and stuff a Clear-Host command to the end of it. That’s right, every time they enter a command and the prompt is redrawn, it’ll clear the host. Ha! Let’s do this. Yeah, let’s.

How you’re going to get on to your friend’s computer to do this, is up to you. That’s not what I’m here for, just remember your buddy is going to need to spend at least some time in the PowerShell console. Your click-next admin buddies aren’t going to see (or appreciate) this prompt function.

First, we need to get the content that makes up their prompt. It doesn’t matter if it’s been customized or not. Here’s the full command to do that. This command will assign the prompt’s ScriptBlock property to the $CurrentPrompt variable. Additionally, it will pass the value to the ToString() method.

PS MyPrompt > $CurrentPrompt = (Get-Command -Name prompt).ScriptBlock.ToString()

Let’s take a look at the contents of the $CurrentPrompt variable. If you didn’t notice already, you’ll be able to tell that I started with a custom prompt.

PS MyPrompt > $CurrentPrompt
    'PS MyPrompt > '

Next, we’ll create a $NewPrompt variable. Notice that this code includes the $CurrentPrompt variable we created in the first step. This is how we can ensure the prompt has the same look, whether it’s been customized or not. The difference now, is that we’ve added a Clear-Host command inside there. So mean. And Awesome.

PS MyPrompt > $NewPrompt = "$($CurrentPrompt)Clear-Host"

This final command will overwrite the current prompt function with our modification.

Set-Content -Path Function:\prompt -Value $NewPrompt

In closing, I want to provide the full code I used for testing. This contains four different clean prompts, as well as the code we saw above to make the modifications. The first prompt is the one I use on a daily basis and discussed several times around here — it’s my Linux lookalike prompt. Anyway, this will give you some test code to copy to your system and play with. Have fun, then torture your coworkers, but I didn’t say that.

Function Prompt {
	(Get-PSProvider -PSProvider FileSystem).Home = $env:USERPROFILE

	# Determine if Admin and set Symbol variable.
	If ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {
		$Symbol = '#'
	} Else {
		$Symbol = '$'
	}
	 
	# Write Path to Location Variable as /.../...
	If ($PWD.Path -eq $env:USERPROFILE) {
		$Location = '/~'
	} ElseIf ($PWD.Path -like "*$env:USERPROFILE*") {
		$Location = "/$($PWD.Path -replace ($env:USERPROFILE -replace '\\','\\'),'~' -replace '\\','/')"
	} Else {
		$Location = "$(($PWD.Path -replace '\\','/' -split ':')[-1])"
	}

	# Determine Host for WindowTitle.
	Switch ($Host.Name) {
		'ConsoleHost' {$HostName = 'consolehost'; break}
		'Windows PowerShell ISE Host' {$HostName = 'ise'; break}
        'Visual Studio Code Host' {$HostName = 'vscode'; break}
		default {}
	}

    # Determine PowerShell version.
    $PSVer = "$($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"

	# Create and write Prompt; Write WindowTitle.
    $UserComputer = "$($env:USERNAME.ToLower())@$($env:COMPUTERNAME.ToLower())" 
    $Location = "$((Get-Location).Drive.Name.ToLower())$Location"

    # Check if in the debugger.
    If (Test-Path -Path Variable:/PSDebugContext) {
        $DebugStart = '[DBG]: '
        $DebugEnd = ']'
    }

    # Actual prompt and title.
    $Host.UI.RawUI.WindowTitle = "$HostName $PSver`: $DebugStart[$UserComputer $Location]$DebugEnd$Symbol"
    "$DebugStart[$UserComputer $Location]$DebugEnd$PSVer$Symbol "
}

Function prompt {
    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

Function prompt {
    'PS > '
}

Function prompt {
    'PS MyPrompt > '
}

Get-ChildItem # Verify no Clear-Host command included.

$CurrentPrompt = (Get-Command -Name prompt).ScriptBlock.ToString()

$CurrentPrompt
	
$NewPrompt = "$($CurrentPrompt)Clear-Host"

Set-Content -Path Function:\prompt -Value $NewPrompt

Get-ChildItem # Verify Clear-Host command included.

PowerShell.org PowerShell Contributors

Have you seen this!?

Help us Recognize Amazing PowerShell Contributors!


PowerShell.org has brought back their PowerShell recognition program of the past. This is a great opportunity to say thank you to people in the PowerShell community that may have helped you along your journey. Chances are, that we didn’t all get here entirely on our own. I sure didn’t.

If you’ve been here before and I’ve been able to help you, then let them know. Maybe I did it about you too, because yes, I’ve submitted one name thus far. 🙂 The reward is helping people — I get that, I have since mid 2014 and 200+ posts ago — but recognition across our community is rewarding as well. Let’s lift up and inspire those that contribute, as we help those that want to learn.

One Function for Them All

I’m in the middle of a project that requires me to read over existing PowerShell scripts and provide edits. This means I need to add error checking, remove unnecessary duplication, and break down lengthy scripts into tighter, singled purposed functions. After breaking down the first script into three functions and therefore concerned we’re going to have a lengthy list of independent functions to execute, I had an idea to make things easier. I’ve said it before: I’d rather troubleshoot five 20 line functions, than one, one hundred line script.

The below example creates five basic, basic functions. Each of these Show-*Number functions writes a single string to the host when invoked. The problem here, is that I’m required to remember the names of each of the functions and their order. This example uses fairly easy to remember names, and the order is pretty straightforward, too. Even so, in some situations they won’t always be so easy to recall, to include their order. I understand we have various tools for command discovery, but I want a simpler way.

Function Show-FirstNumber {
    'This is sequence 1.'
}

Function Show-SecondNumber {
    'This is sequence 2.'
}

Function Show-ThirdNumber {
    'This is sequence 3.'
}

Function Show-FourthNumber {
    'This is sequence 4.'
}

Function Show-FifthNumber {
    'This is sequence 5.'
}

PS > Show-FirstNumber
This is sequence 1.
PS > Show-SecondNumber
This is sequence 2.
PS > Show-ThirdNumber
This is sequence 3.
PS > Show-FourthNumber
This is sequence 4.
PS > Show-FifthNumber
This is sequence 5.

That simpler way, is to create a single function that has the job of invoking all of the other functions. I suspect you may have thought of this too, but if not, well then, there we go. Now, all my users need to know about is the Start-Sequence function defined below.

I’m out to “hide” those other functions. I want to say I took your three scripts, and now you can run them all with this single function. The user may not even know their single invocation just ran several separate functions, and that’s, fine with me.

Function Start-Sequence {
    'Starting the sequence.'
    Show-FirstNumber
    Show-SecondNumber
    Show-ThirdNumber
    Show-FourthNumber
    Show-FifthNumber
    'Ending the sequence.'
}

When the Start-Sequence function is invoked, it’ll remember the names of those other functions for us, and best of all, I won’t be required to the do the same. Here’s our results now. It’s one single command, running multiple functions, getting the entire job done, and making things much easier to troubleshoot later on.

PS > Start-Sequence
Starting the sequence.
This is sequence 1.
This is sequence 2.
This is sequence 3.
This is sequence 4.
This is sequence 5.
Ending the sequence.

Anyway, back to breaking apart some scripts.

Hash Table to CSV

I’m was sitting here, and I wondered, how do I get a hash table into a CSV? Have I even ever done that? Now, a few commands in, and I can’t even remember why I wondered that. There was a reason, I just wish I could remember what it was. Whatever it was, I should be more likely to remember how to get a hash table into CSV when I need it after today’s post. Was on disk storage for a hash tables for some software configuration… ugh, what was it?

Anyway, let’s start by assigning a hash table to a variable. Maybe it’ll come back to me.

$HashTable = @{
    Name = 'Tommy'
    StreetName = 'Vista Pl.'
    City = 'Tucson'
}

$HashTable
Name                           Value                                                                                                      
----                           -----                                                                                                      
Name                           Tommy                                                                                                      
StreetName                     Vista Pl.                                                                                         
City                           Tucson

Now that we’ve assigned our $HashTable variable the value of our hash table, we can try and get it into a CSV. At first, someone might try the below option. Let’s see how that works.

$HashTable |
    Export-Csv -NoTypeInformation -Path .\Desktop\HashToCsv.csv

As you can see, this doesn’t work.

In order to get this to work properly, we need to use the GetEnumerator method. I seem to use this quite often. This allows us to walk though each key-pair in our hash table.

$Hash.GetEnumerator() |
    Export-Csv -NoTypeInformation -Path .\Desktop\HashToCsv2.csv

Now it’s just perfect, minus the whole Name property (column). Huh? I only expected the Keys and Values, like we’d see produced onscreen. With this in mind, let’s instead pipe to the Select-Object cmdlet before Export-Csv and get things properly filtered.

Update: It dawned on me, after I made all these screen captures, that I actually meant I wasn’t expecting to see the Key property included. Sure, it’s the same values as Name, but in a host program we’re actually accustom to seeing Name and Value, not Key and Value.

$HashTable.GetEnumerator() |
    Select-Object -Property Key,Value |
        Export-Csv -NoTypeInformation -Path .\Desktop\HashToCsv3.csv

My next logical thought was, can we use calculated properties? Can I use a different descriptor than Key and Value? You bet we can — take a look.

$HashTable.GetEnumerator() |
    Select-Object -Property @{N='Property';E={$_.Key}},
    @{N='PropValue';E={$_.Value}} |
        Export-Csv -NoTypeInformation -Path .\Desktop\HashToCsv4.csv

So yeah, there we go. I can use a hash table, saved to disk, for some sort of configuration. I still don’t remember why I wondered this originally, but in case it’s helpful, I know where to find this when I remember why I wondered.

Manipulate Text: Convert [BEGIN ] to Begin

I continue to say the same thing around here, and I suspect I’ll continue to do so. I use my site to record things I’m going to want to find quickly one day. While this is going to be a short post, it’s going to hold a few lines of PowerShell code I don’t want to rewrite some other afternoon. This isn’t to say it difficult by any means. It’s meant to say, that my time is better spent learning than relearning all over again. Bonus, there’s a good chance this might help someone else out too!

I’m right in the middle of improving my 2.1 version of my Advanced Function Template. For each block it enters (Begin, Process, and End), it needs to turn this string, for example, “[BEGIN  ]” into this this string, “Begin.”

To do that, we need to remove the square brackets, remove the spaces after the word BEGIN, and finally make “EGIN” lowercase. Not terribly complex, but worth the five minutes it takes to write and publish this post. And with that, the code.

Remove-Variable x -ErrorAction SilentlyContinue

$x = '[BEGIN  ]'
"1st: $x - No edits."

$x = $x.Trim(' []')
"2nd: $($x.Length) - Length proves spaces/brackets removed."

"3rd: $($x.Substring(0,1)) - First letter only."

"4th: $($x.Substring(1,($x.Length-1))) - All letters but first."

"5th: $($x.Substring(1,($x.Length-1)).ToLower()) - All letters but first (lowercase)."

$x = $x.Substring(0,1)+$x.Substring(1,($x.Length-1)).ToLower()
"6th: $x - Done."

1st: [BEGIN  ] - No edits.
2nd: 5 - Length proves spaces/brackets removed.
3rd: B - First letter only.
4th: EGIN - All letters but first.
5th: egin - All letters but first (lowercase).
6th: Begin - Done.

Enjoy the weekend.

Remove a Hash Table from an Array of Hash Tables

Take a look at this post’s title. I got that as a question, from a coworker yesterday. I didn’t know the answer right away, but I took some time last night and a half hour or so today to put it to bed. As of now, I am able to remove a hash table from an array of hash tables. Ugh.

The below array is the one you get when you don’t cast the variable. That’s to say the $x variable doesn’t need [System.Array] in front of it. It effectively does nothing different to that variable. The problem with this type of array is that it’s partially immutable. We can’t remove values from the array.

[System.Array]$x = 'one','two','three'

Therefore, we either need to create a new array to hold the hash tables we still want (as we are removing one of them), or cast our array as [System.Collections.ArrayList]. A variable cast as such allows us to remove values from it.

[System.Collections.ArrayList]$y = 'four','five','six'

Use this next information as a key, or legend, for the further below code. The entire code can be copied into the ISE, or Visual Studio Code, and run.

1. Removes the user-defined variables used in the below code.
2. Creates three hash tables that each include a User key, a Number key, and a Color key.
3. Prompts user to enter 1 or 2 whether they want to use the same array, or a new one.*

* The same array indicates we’re casting our variable as [System.Collections.ArrayList] and using the same variable. The new array indicates we’re casting a new variable as [System.Array], which again is the default and doesn’t actually require a cast.

4. Creates an array of hash tables based on the value 1 or 2.
5. Displays the current array of hash tables (before any changes).
6. Loops through the values in the array and uses the same array (if 1 was chosen), or creates a new array (if 2 was chosen).
7. Displays the updated array of hash tables (removes the hash table that includes the User “bsmith”).

#1 Remove variables (not using function/function scope).
Clear-Host
Remove-Variable -Name Hash1,Hash2,Hash3,Option,HashtableArray,i,HashtableArrayNew -ErrorAction SilentlyContinue 

#2 Create hash tables.
$Hash1 = @{User = 'landrews';Number = 1;Color = 'Red'}
$Hash2 = @{User = 'bsmith';Number = 2; Color = 'Blue'}
$Hash3 = @{User = 'sjackson';Number = 3;Color = 'Yellow'}

#3 Set SameArray vs. NewArray.
Write-Output -InputObject 'This function is hard coded to remove the hash table that include "bsmith" from an array.'
Do {
    $Option = Read-Host -Prompt 'Press 1 to use the same array, or 2 to create a new array'
} Until ($Option -eq 1 -or $Option -eq 2)

#4 Create array of hash tables.
Switch ($Option) {
    '1' {[System.Collections.ArrayList]$HashtableArray = $Hash1,$Hash2,$Hash3; break}
    '2' {[System.Array]$HashtableArray = $Hash1,$Hash2,$Hash3}
}

#5 Display unmodified hash table.
###################################
$HashtableArray
"'''''''^ Array of hash tables bfore ^''''''"
"'''''''v Array of hash tables after v''''''"
###################################

#6 Loop through array of hash tables.
For ($i = 0; $i -le $HashtableArray.Count - 1; $i++) {
    Switch ($Option) {
        '1' {
            If ($HashtableArray[$i].Values -contains 'bsmith') {
                $HashtableArray.Remove($HashtableArray[$i])
            }
        }
        '2' {
            If (-Not($HashtableArray[$i].Values -contains 'bsmith')) {
                [System.Array]$HashtableArrayNew += $HashtableArray[$i]
            }
        }
    }
}

#7 Display updated array.
Switch ($Option) {
    '1' {$HashtableArray}
    '2' {$HashtableArrayNew}
}

The below results show the exact same thing when run either by entering 1 or 2. The difference is the variable that’s displayed. You can see the above variable that’s returned based on 1 or 2. If it’s 1, we display the $HashtableArray variable (the one we created initially). If it’s 2, we display the $HashtableArrayNew variable. That’s the one we create, since we can’t modify the existing $HashtableArray variable when it’s cast as [System.Array].

This function is hard coded to remove the hash table that includes "bsmith" from an array.
Press 1 to use the same array, or 2 to create a new array: 2

Name                           Value
----                           -----
Color                          Red
Number                         1
User                           landrews
Color                          Blue
Number                         2
User                           bsmith
Color                          Yellow
Number                         3
User                           sjackson
'''''''^ Array of hash tables bfore ^''''''
'''''''v Array of hash tables after v''''''
Color                          Red
Number                         1
User                           landrews
Color                          Yellow
Number                         3
User                           sjackson

And with that, I’m done here. Fun stuff. Mostly.

ScriptsToProcess Scheduled Task

Before we really get started, I think it’s helpful to first mention that I was recently deep in a project at work that doesn’t include Active Directory, but does include Windows clients. Therefore, I had do things in ways that could have been made easier had I had Active Directory to leverage.

I’ve long known that there’s a ScriptsToProcess option for PowerShell script modules that use a module manifest file. First, a script module is most often a collection of functions inside a script written, PowerShell module — a .psm1 file. Second, a .psd1 file is the module manifest file, and it contains information, among other things, about the module itself. One of the things that can be used in this file, is ScriptsToProcess, where a path, or paths, to a .ps1 file(s) can be included. This ultimately means a script can be run the moment your module is imported. ScriptsToProcess allows for an environment to be set up the way you want, prior to anyone actually using the functions in your module.

I quickly realized that in that newer, AWS project, that I should’ve had some sort of automation create a scheduled task on my EC2 instances. What I wish had been automated was the regularly scheduled downloading of my main PowerShell module for these projects from S3 to the instances. A task such as this would keep me, or a coworker, from ever needing to manually update the PowerShell module on these instances again. Instead, a scheduled task would just download the module a few times a day, whether or not it had been updated. If it had, then suddenly my instances would have the newest functions, with no continued manual work on my part.

And, that led me to wonder, can I create a scheduled task on an instance when a PowerShell module is imported, as a part of the ScriptsToProcess .ps1, that can be set to run at the module import? That answer, is no.

Just kidding. It’s a yes! You can create a scheduled task on a computer the moment a PowerShell module is imported, thanks to ScriptsToProcess. There’s a bunch of parts and pieces to this, but what I’m going to include is the script file ScriptsToProcess executes, when my module imports.

# Create scheduled task, if able and necessary (admin).
If ([System.Boolean](([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups -match 'S-1-5-32-544')) {

    $TaskName = 'EC2General PowerShell Module Update'
    If (-Not([System.Boolean](Get-ScheduledTask -TaskPath '\' -TaskName $TaskName -ErrorAction SilentlyContinue))) {
        $Command = "Import-Module -Name 'EC2General'; Update-EC2General"
        $Action = New-ScheduledTaskAction -Execute "$PSHOME\powershell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy ByPass -Command & {$Command}"
        $Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddDays(1).Date -RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration ([System.TimeSpan]::MaxValue)
        $Settings = New-ScheduledTaskSettingsSet -RunOnlyIfNetworkAvailable -WakeToRun
        $Principal = New-ScheduledTaskPrincipal -UserID 'NT AUTHORITY\SYSTEM' -LogonType S4U -RunLevel Highest
        [System.Void](Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Principal $Principal)
    }
}

Neat stuff, right? What this code does is this. It begins by determining whether or not the user that’s importing the module is a local administrator, or not. They’re going to need to be, to register the scheduled task. If they are, and the task doesn’t already exist, it creates all the necessary variables to get the task created. When those are available, it’ll register the scheduled task.

I do want to credit Chrissy LeMaire, as a post of hers was used while I wrote the code necessary to create a scheduled task. There’s so much that goes into those, that I wasn’t going to trust myself to remember, or require myself to read the help files. I was confident she could be trusted.

This makes me wonder, though, what else should I do when a module loads?

Update: For our project, we didn’t actually use ScriptsToProcess. We did, however, manually run the code to create the task separately. It was that whole, a user would need to be an admin problem, as most of my users weren’t admins. Keep that requirement in mind.