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

Update: There’s a “part two” now. See the link at the bottom of this post.

I 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 it on-disk storage for a hash table 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 through each key-pair in our hash table.

$HashTable.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 wasn’t expecting to see the above Name property included. Sure, it holds the same values as Name, but in a host program, we’re actually accustomed 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 … whatever reason. 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.

Update: There’s a follow-up post now that includes an easier way.

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.

My Write-Verbose Best Practice

If you’ve read this post, and perhaps the ones before it, then you know that my advanced function templates have always used Write-Verbose statements for logging purposes. Anytime you want to log something using my template, it’s using Write-Verbose whether you know it or not. There’s multiple places you can log, and it’s all based on the value of the Log parameter (in 2.0 and greater). As of the 2.x versions, you can use … -Log ToScreen, … -Log ToScreenAndFile, and … -Log ToFile. Again, regardless of which way you choose to log, it’s Write-Verbose that’s doing the work. Okay fine, it’s really Out-File if you’re writing to a file, although my nested function is still called Write-Verbose. That said, there actually was a time when it was all Write-Verbose, even when writing to a file.

So, I’ve always had a specific way to write my logging statements. There’s no agreed upon best practice for Write-Verbose statements, but for me there is, and that’s why I’m writing again today. Maybe it’ll make sense to use this model for yourself, as well.

In the instance where something is about to happen (think, happens right after the current logging statement ends), I strive to use an “ing” word. From what I remember about my grade school years, I remember this as a present progressive. Here’s a few examples.

Write-Verbose -Message "$BlockLocation Setting required variables."
Write-Verbose -Message "$BlockLocation Checking if the current time zone is $DesiredTimeZone."
Write-Verbose -Message "$BlockLocation Collecting Forest FSMO Roles."
Write-Verbose -Message "$BlockLocation Displaying all FSMO Roles."
Write-Verbose -Message "$BlockLocation Downloading $Application installer from S3."
Write-Verbose -Message "$BlockLocation Installing $Application silently."
Write-Verbose -Message "$BlockLocation Invoking user is ""$env:USERDOMAIN\$env:USERNAME"" on the ""$env:COMPUTERNAME"" computer."

In my experience I tent to use an “ing” words about 80 – 90% of the time. There is, however, one other verb form I find myself using, and that’s the past tense verb form. Sometimes, it makes sense to indicate that something has completed even when a new logging, “ing” statement would indicated the previous logging topic had ended. Here’s a few examples from some of my functions.

Write-Verbose -Message "$BlockLocation Determined the time zone is not correctly set for $DesiredTimeZone."
Write-Verbose -Message "$BlockLocation Unhandled exception ($($Error[0].Exception.GetType().FullName))."
Write-Verbose -Message "Connected to Exchange Server: $ExchServ."

While looking for other worthy examples, I did find a few Write-Verbose statements that didn’t include either an “ing,” or “ed” verb. These are very rare for me, but they have been included at times to indicate information to whomever is reading the logging statements.

Notice, however, that they come just before a statement that uses “ing.” They probably shouldn’t be there, and wouldn’t be there if I was writing that tool today. Then again, they might be, as now that I think about it, and look over this older code, the logging differed because of its audience. In this case, having indicated the key and value already existed, did not exist, or did exist and needed correction, was important enough to convey. So much so, the “ing” word didn’t feel as though it was enough.

#region Create Registy Key(s) and add value (if necessary).
If (-Not($Exists)) {
	Write-Verbose -Message "$BlockLocation Registry key and value do not yet exist."
	Write-Verbose -Message "$BlockLocation Creating new Registry key and value."
	[System.Void](New-Item $RegKey -Force | New-ItemProperty -Name $RegName -Value $RegValue -Force)
	$CheckChange = $true
	$FixType = 'changed'

} ElseIf ((Get-ItemProperty -Path $RegKey -Name $RegName -ErrorAction SilentlyContinue).$RegName -ne $RegValue) {
	Write-Verbose -Message "$BlockLocation Registry key and value already exist, but the value is not correct."
	Write-Verbose -Message "$BlockLocation Correcting the Registry value."
	[System.Void](New-Item $RegKey -Force | New-ItemProperty -Name $RegName -Value $RegValue -Force)
	$CheckChange = $true
	$FixType = 'corrected'

} Else {
	Write-Verbose -Message "$BlockLocation Registry key and value already exist, and the value is correct."
}
#endregion

So, there’s that: My Write-Verbose best practice. While it wasn’t a stupid draft so much, there’s one less in the drafts!

Learning from Failure

I wrote a recent blog post that went from being a longtime draft, to finally being a published post. The idea was twofold: get a post out of my drafts already, and in doing so, teach by highlighting my failure. It’s weird, but it works.

It shouldn’t be too surprising that it works, however. Much of what we know can likely be attributed to either our own failure, or the failure of someone else, whether we know it or not. It’s not often we learn something new without some sort of mistake along the way. It’s just that the failure portion — how we potentially got to where we are, which ought to be success — isn’t always recognized, or acknowledge, or put forth as something from which others can learn, and actually see for themselves.

After my first, stupid draft turned post, post, I received the below Twitter comment. This was someone that noticed that through my failure, I was able to teach; I was ultimately able to succeed at potentially teaching those that read the post, by sharing what I had learned by failing.

For as long as I can, I’ll keep looking over my drafts to see what else we can collectively learn by my failure. It’s how we get better at everything, because it’s not often that we understand everything the first time we learn it — depending on what it is of course. Accept failure, and learn from it. I’ll do the same and see what else I can share from which we can both learn something new.

Require Use of a Cmdlet’s Prefix

I learned something new today. That’s always fun. Okay, not always, but when it’s about PowerShell, it often is. When you import a module — we’ll use the Active Directory module as an example — and use a prefix, you can still use the original cmdlet names. We’ll begin by importing the Active Directory PowerShell module to include a prefix.

PS > Import-Module -Name ActiveDirectory -Prefix Lol

With that module now imported, we’ll execute a Get-Command, command. As the Name parameter allows for wildcards, we’ll instruct it to return all the commands PowerShell knows about, at this stage in the session, that include the ADUser suffix from the ActiveDirectory module.

PS > Get-Command -Name '*ADUser' -Module ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          New-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-LolADUser          1.0.0.0    ActiveDirectory
Cmdlet          Set-LolADUser             1.0.0.0    ActiveDirectory

The above command results indicates that PowerShell knows about four commands that end with the ADUser suffix. Each of those commands that it knows about, include the Lol prefix we used in the first command we issued, Import-Module. So far, everything is going as expected. Or is it?

Check this out. When we opt to not use the Module parameter and use Where-Object and the Source property instead, we have some different results. This is just getting weird, but at minimum, there’s a bit of proof here that the original cmdlets still exist, as well as the prefixed versions.

PS > Get-Command -Name '*ADUser' | Where-Object -Property Source -eq ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          Get-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          New-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          New-LolADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-ADUser             1.0.0.0    ActiveDirectory
Cmdlet          Remove-LolADUser          1.0.0.0    ActiveDirectory
Cmdlet          Set-ADUser                1.0.0.0    ActiveDirectory
Cmdlet          Set-LolADUser             1.0.0.0    ActiveDirectory

If we reissue the Get-Command command and indicate we only want the Get-ADUser command specifically, it shows up, too. Still, it’s odd that it wasn’t included in the first results. I think most of us would have expected to see it there, too. We’ll get back to this in a moment.

PS > Get-Command -Name Get-ADUser -Module ActiveDirectory

CommandType     Name                      Version    Source
-----------     ----                      -------    ------
Cmdlet          Get-ADUser                1.0.0.0    ActiveDirectory

About this time in my poking around, is when I started examining the imported module. The first thing I noted, due to the default output, was that the first two commands I could see, under ExportedCommands, didn’t include the noun prefix. It’s more proof the original cmdlets are still around.

PS > Get-Module -Name ActiveDirectory

ModuleType Version Name            ExportedCommands
---------- ------- ----            ----------------
Manifest   1.0.0.0 ActiveDirectory {Add-ADCentralAccessPolicyMember, Add-ADComputerServiceAcc...

The next command I ran was to better view these exported commands. I haven’t included all the results, but I’m sure you’ll get the idea.

PS > (Get-Module -Name ActiveDirectory).ExportedCommands

Key                                               Value
---                                               -----
Add-ADCentralAccessPolicyMember                   Add-LolADCentralAccessPolicyMember
Add-ADComputerServiceAccount                      Add-LolADComputerServiceAccount
Add-ADDomainControllerPasswordReplicationPolicy   Add-LolADDomainControllerPasswordReplicationPolicy
Add-ADFineGrainedPasswordPolicySubject            Add-LolADFineGrainedPasswordPolicySubject
Add-ADGroupMember                                 Add-LolADGroupMember
Add-ADPrincipalGroupMembership                    Add-LolADPrincipalGroupMembership
Add-ADResourcePropertyListMember                  Add-LolADResourcePropertyListMember
Clear-ADAccountExpiration                         Clear-LolADAccountExpiration
Clear-ADClaimTransformLink                        Clear-LolADClaimTransformLink
Disable-ADAccount                                 Disable-LolADAccount
...

You’ll notice that we’re dealing with a hash table. For every key named with the original cmdlet name, we have a value that is the cmdlet name with the noun prefix. The hash table appears that it may be used to know the relationship between the original cmdlets and its matching prefixed version. For fun, I opened a new PowerShell session and imported the Active Directory module without the prefix. In that case, the above hash table had the same command for a Key and its matching Value.

This finding made me think of something. Perhaps that Get-Command, command that included the Module parameter, only checked for commands against the values in this hash table, but the Where-Object cmdlet, returned the keys and values from the hash table. Just a thought; I said I get back to this a moment ago.

So here’s why I really decided to write. What if I don’t want to allow a user to use the original command. I don’t want them to use Get-ADUser to invoke Get-ADUser; instead, I want them to use the prefixed version only. Why, I don’t exactly know, but here’s how I fixed that one.

We’ll pretend that we’ve yet to import the Active Directory module; we’re starting fresh with the below sequence of commands. We’ll begin by creating two variables: $Module and $Prefix. In the subsequent command, we’ll import the Active Directory module providing it’s yet to be imported. When it’s imported, we’ll include the Prefix parameter with the Lol value. This means, Get-ADUser becomes Get-LolADUser, as can be figured out from the previous examples.

PS > $Module = 'ActiveDirectory'; $Prefix = 'Lol'

PS > If (-Not(Get-Module -Name $Module)) {
>>> Import-Module -Name $Module -Prefix $Prefix
>>> }

PS > $ExportedCommands = (Get-Module -Name $Module).ExportedCommands

PS > Foreach ($Command in $ExportedCommands.GetEnumerator()) {
>>> New-Item -Path "Function:\$($Command.Key)" -Value "Write-Warning -Message 'Please use the $($Command.Key.Replace('-',"-$Prefix")) function.'" -Force
>>> }

After that, we’ll get all of the ExportedCommand into a variable with the same name. Then, the Foreach command iterates over the keys in our hash table — the original cmdlet names — and creates a function for each one of them. One minute, Get-ADUser executes as expected, and the next minute, it doesn’t. Now, every time someone enters the actual AD cmdlet name, a function with the same name will execute instead. It will indicate to the user to use the prefixed version. Here’s a few examples.

PS > Get-ADUser -Identity tommymaynard
WARNING: Please use the Get-LolADUser function.

PS> Unlock-ADAccount tommymaynard
WARNING: Please use the Unlock-LolADAccount function.

PS > Get-ADDomain
WARNING: Please use the Get-LolADDomain function.

PS > (Get-LolADUser tommymaynard).SamAccountName
tommymaynard

No idea if this might ever prove useful, but at minimum, I now know I can change the default behavior of a cmdlet, in order to point someone toward the prefixed version. Oh, and the reason why the function I created is run instead of the cmdlet, is because of command precedence. Read about it; it’s beneficial to know. I knew it existed and I’ve used it, I’ve just never used it in bulk to create functions to run in place of so many cmdlets. Additionally, however, I’m not sure if I’ve ever created a function by using the New-Item cmdlet directly against the Function PSDrive. Neat.

Enjoy the weekend.

Update: Ah, it’s Monday again. This seems to happen at least once a week. There’s something I wanted to note, and it’s not just the day. Even though we’re using a function to indicate to users to use the prefixed cmdlet, the original cmdlet can still be used. If someone uses the full path to the cmdlet, the function will be ignored. That’s to say, this still works.

PS > ActiveDirectory\Get-ADUser

Linux Prompt on Windows – Part VI

My most recent post in the Linux Prompt on Windows series, is Part V. Now, we’re on VI and it’s all because of PowerShell 6.0.

As 6.0 uses a different $PROFILE script, it was mandatory that I created a new one and quickly copied over my Linux prompt. I hate to be anywhere without it. You can create a $PROFILE script in PowerShell 6.0 the same way we did previously. It’s New-Item -Path $PROFILE -Force (press Enter) followed by notepad $PROFILE (press Enter) to open the file, in Notepad, which should be obvious.

The newest change is adding the version number, just to the right of the host program’s name in the WindowTitle. Here’s an example of how that appears. As it should the WindowTitle indicates it’s PowerShell 6.0.

Type in powershell.exe and press Enter, and the WindowTitle changes. Now we’re in PowerShell 5.1.

Go deeper in time and type powershell.exe -version 2, press Enter, and it changes again, but this time to 2.0. By adding this addition to the WindowTitle via my prompt function, I can move between versions of PowerShell, if needed, and always know the version in which I’m working.

I’ve included the fully updated prompt function below. Copy it into your $PROFILE script, restart PowerShell, and enjoy. I can’t be the only one to appreciate this prompt, especially as PowerShell 6.0 just went GA. As the beta versions before it were, it’s cross platform; therefore, my lookalike Linux prompt makes even more sense now.

Update: I recently cleaned out my new 6.0 $PROFILE script and did what I’ve always done instead. That is to dot source a different profile script (the one loaded by Windows PowerShell 5.1). So, in place of my prompt function, I now have this entry: . C:\Users\tommymaynard\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1. Yeah, you’re reading that right, “WindowsPowerShell.” All my stuff is in there! It should be fun to see what works and what doesn’t!

Update: I got really tired of looking up at the WindowTitle to see my version, so I added it to the prompt, as well. It’s just after the closing square bracket and before the # or $ symbol. By the way, the difference of those two is that # indicates an administrative user, and $ indicates a non-administrative user. Oh yes, and for today, my host is blue. It’s not the exact blue, but close. The font color is also gray (by default), another slight difference between the host configuration for 5.1 vs. 6.0. Anyway, I’ve updated the below code to indicate the version as a part of the prompt itself, just like in the below image.

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 "
}

 

Linux Prompt on Windows – Part V

http://tommymaynard.com/an-addition-to-the-linux-powershell-prompt-iv-2016/

http://tommymaynard.com/an-addition-to-the-linux-powershell-prompt-ii-2016/

http://tommymaynard.com/an-addition-to-the-linux-powershell-promp-2016/

http://tommymaynard.com/duplicate-the-linux-prompt-2016/

An Advanced Function Template (Version 2.1 -and -gt)

Version 2.1

Back in October 2017, I spoke at the Arizona PowerShell Saturday in Phoenix, Arizona. My topic, was introducing those that attended, to a Windows PowerShell Advanced Function Template. I found a flaw in it which I’ve since fixed in my 2.1 version linked below. For anyone after this template, this is the most current version. Newer versions, providing there are any will be included in this post and not in another, separate post around here. Therefore, 2.1 may not forever be the current version, but the current version will always be a part of this post.

The flaw that took us 2.1 was that all parameters and parameter values are logged. That’s a good thing, but it means that if someone supplied a password as a string, it would be logged (providing the Log parameter was used). Therefore, 2.1 looks for parameter’s with the word “password” in them, and then doesn’t include the parameter value. Instead, it would read like the below example. This isn’t fool proof, but I think it’s a helpful addition.

PS > Get-CPUCount -ComputerName server01 -Password 'test1' -MainPassword 'test2' -PasswordName 'test3' -xPasswordx 'test4' -Log ToScreen
...
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "ComputerName" parameter with the "server01" value.
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "Password" parameter without the parameter value.
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "MainPassword" parameter without the parameter value.
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "PasswordName" parameter without the parameter value.
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "xPasswordx" parameter without the parameter value.
[1/10/2018 10:03:27 PM]: [INFO   ] Including the "Log" parameter with the "ToScreen" value.
...
AdvancedFunctionTemplate2.1 (4002 downloads )

 

Older Posts:

An Advanced Function Template (Version 2.1 -and -gt)

An Advanced Function Template (Version 2.1 -and -gt)