In a recent post I discussed the the process you might go through in developing a PowerShell function. By the end, I not only had a new tool for my PowerShell toolbox, but I had a function outline that I could re-use. If you read the previous article then you should recognize the idea of "managing at scale". Where possible I want my functions to work for not one thing or computer for many. One way to accomplish that is by incorporating remoting. The process memory tool I built leveraged Invoke-Command to run a simple PowerShell expression using Get-Process to retrieve the information I wanted. I can re-use that technique to accomplish all sorts of tasks. Let's do another.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Start with a Command
Whenever you are building a PowerShell script or function, I stress that you should begin with a command or set of commands that you can run from the console. This will be the core of the script or function. A lot of your scripting is adding convenience, error handling and other embellishments around this core. For my next function I wanted to replicate the information displayed when you run the winver.exe command. To see for yourself run winver at a prompt or in the Run box.
Much of the information displayed can be retrieved by querying the Win32_OperatingSystem class using Get-CimInstance. I consider Get-WmiObject deprecated and rarely use it now. However, I also know that the same information can be found in the registry. So for a change of pace I decided to query the registry to get the same information and ended up with this code.
$RegPath = 'HKLM:\SOFTWARE\Microsoft\Windows nt\CurrentVersion\' Get-ItemProperty -Path $RegPath | Select-Object -Property ProductName, EditionID, ReleaseID, @{Name = "Build"; Expression = {"$($_.CurrentBuild).$($_.UBR)"}}, @{Name = "InstalledUTC"; Expression = { ([datetime]"1/1/1601").AddTicks($_.InstallTime) }}, @{Name = "Computername"; Expression = {$env:computername}}
I am always thinking about writing an object to the pipeline so I'm defining some custom properties that not only replicate what I see in winver but that might also be useful.
Now that I have code that works, I can plug this code into the scriptblock for my function. Actually, let me take a step back first.
Using a Function Template
The Get-ProcessMemory function I wrote in the end became a terrific model I could re-use. So I created a template version of the file. You can get the template from GitHub.
I even added comment based help for all of the remoting parameters. In the Begin block there is a section to define the scriptblock that I intend to run remoting. That is where I'll stick my registry-related code. I don't need to pass any other parameters. If I did I would add them and update the help. The important steps include updating the scriptblock to incorporate those values either by employing the $using: reference or passing an ArgumentList parameter to Invoke-Command. My template is splatting PSBoundParameters to Invoke-Command so I (or you) need to add or remove parameters accordingly.
When finished, I had a new command called Get-WindowsVersion.
Now I have a scalable PowerShell tool!
Extending the Function
Even though I'm always stressing the importance of "objects in the PowerShell pipeline", I recognize that sometimes we really need a string or piece of text. When running winver.exe that is essentially what I'm seeing. Obviously the graphical tool doesn't scale. But PowerShell can. I already have all the pieces of information in the output from my function to create the string. My initial thought was to add a parameter to the function, like -AsString, that if used, would write a string to the pipeline instead of the object. That would be the lazy way and I realize the wrong way.
PowerShell functions should only write one type of object to the pipeline and I was potentially creating a function what was going to do 2 things. The answer, naturally, was to write a second function, Get-WindowsVersionString. This would actually be easy to write because all I need to do is call my Get-WindowsVersion function for each computer and create the string from the result. This becomes the Process scriptblock.
Process { $results = Get-WindowsVersion @PSBoundParameters #write a version string for each computer ` foreach ($result in $results) { "{0} Version {1} (OS Build {2})" -f $result.ProductName, $result.releaseID, $result.build } } #process
The parameters for this function are the same as Get-WindowsVersion so all I have to do is "pass" them using splatting. You can see the flow in the functions' verbose output.
The end result is I have 2 functions to easily get me the information I need and in different formats. The code for both functions is also on Github.
VS Code Bonus
If you scroll down the code for my Get-Something template you'll see a comment block at the end of the file. This is a json code snippet. You copy and paste this into the PowerShell snippets json file for VSCode. If you don't know how to get there, click on the gear icon and select User Snippets. Select the powershell.json file. Paste the snippet into this file. Watch your commas!
With this snippet in place I can quickly build a new function by starting to type 'get-something'.
Press Enter to insert the snippet. Add your scriptblock (which you already know works, right?), adjust parameters, update help and you're done!
A Process Paradigm
I hope you have found these last few articles helpful in building your own PowerShell scripts and functions. My end results aren't the goal and actually aren't that relevant. The main thing you should be focusing on is a process and pattern in PowerShell toolmaking. If you can "template-tize" it, so much the better. Your feedback is always welcome. I'm very interested in what roadblocks you face or what you find difficult to understand. You might also be interested the PowerShell Scripting and Toolmaking Book in learning more scripting paradigms and concepts.
Typo in line 243 , ? Watch your commas!
I guess I caught it in my local copy after I created the gist. I moved the functions to my PSScriptTools module and they look right.