Looking back at the scripts I created the last year, certain things always come back in most of them. In this blog post, I will show you a few and explain them.

Function/Param
In the past, I just started creating a script that just ran by pressing F5 in my PowerShell_ISE or starting .\script.ps1 from the command line. While this works, you can’t supply command line parameters and must do things like Read-Host and prompting for the output file name, server name, etc., if you want it to be dynamic instead of static. (By hardcoding all the options in the script itself)
Using the function feature of PowerShell, you can load a script into memory by putting it into your PowerShell profile or call it from within a script with parameters. For example:
function Get-LocalGroupMembers { [CmdletBinding(DefaultParameterSetName = 'All')] param ( [parameter(Mandatory = $true)][string]$Outfile, [parameter(Mandatory = $false, parameterSetName = "ComputerNameFilter")][string]$ComputerNameFilter, [parameter(Mandatory = $false, parameterSetName = "OUFilter")][string]$OUfilter, [parameter(Mandatory = $false, parameterSetName = "FileName")][string]$FileName, [parameter(Mandatory = $false)][string[]]$GroupNameFilter )
This is a part of my Get-LocalGroupMembers function, which let you specify parameters for Outfile, ComputerNameFilter, OUFilter, FileName, and GroupNameFilter. Some are Mandatory, which will prompt you when you use the function without parameters, and some are optional and not needed but allowed. It also has the ParameterSetName option, which allows you to hide parameters not in the same set. So, if you specify the ComputerNameFilter, the OUFilter/FileName/GroupNameFilter parameters will be hidden. This is easy when parameters can’t be used together.
You can also use the Param part without the Function statement. This way, you can start a script from the command line like this:
.\Get-LocalGroupMembers.ps1 - Outfile c:\data\members.csv -ComputerNameFilter Sharepoint
PSCustomObject
I use this to store items, and you can create a Custom Object and set properties and value pairs in it. (I wrote a blog post about that with more details.) I mainly use it in foreach loops to store everything in that loop in a variable. For example:
$total = foreach ($file in Get-ChildItem -Path C:\Temp) { [PSCustomObject]@{ Folder = $file.DirectoryName Name = $file.Name LastWrite = $file.LastWriteTime } }
This will add all items found in Get-ChildItem -Path C:\temp in the PSCustomObject $total, and this will look like this:
Folder Name LastWrite ------ ---- --------- C:\Temp apps.csv 17-5-2023 10:01:22 C:\Temp files.csv 24-3-2023 14:39:00 C:\Temp services.txt 25-4-2023 23:38:48 C:\Temp services2.txt 25-4-2023 23:39:09 C:\Temp ServicesFix-3.5.1.Log 3-5-2023 10:42:39 C:\Temp userslicenses.csv 8-5-2023 19:16:38 C:\Temp windows.csv 3-7-2023 17:30:52 C:\Temp x.csv 19-7-2023 19:24:37 C:\Temp x.log 20-3-2023 16:53:22 C:\Temp x.xlsx 19-7-2023 18:44:56 C:\Temp y.csv 3-7-2023 12:49:51
Splatting
This makes your scripts more readable. You put all your Parameters in a variable and use that as a parameter set in a cmdlet. For example:
$options = @{ Path = 'd:\temp' Recurse = $true Include = '*.ps1' Depth = '2' } Get-ChildItem @options
In this example, all the Get-ChildItem parameters were stored inside $options, and Get-ChildItem was started using @options, meaning it should use all things stored inside $options. This makes it much more readable than having a long command line with multiple parameters.
String formatting
Writing output to your screen can sometimes be challenging with special characters, escaping variables, etc. With string formatting, you can make things easier, for example:
Write-Host ("Connecting to server {0}" using IP-Address {1} -f $Server.Name, $Server.IP)
Using curly brackets with a number (0 and 1 in the example above), you can specify with the -f parameters which number is which variable. Using string formatting, you don’t have to do things like this:
Write-Host "Connecting to server $($server.name) using IP-Address $($server.ip)"
Try/Catch
It’s become my standard for running things and ensuring that if something fails, you will receive a message about that, and the script will stop without potentially causing issues with things that were not created. For example:
try { New-Item -Path $Outfile -ItemType File -Force:$true -Confirm:$false -ErrorAction Stop | Out-Null $total | Sort-Object Name, Property | Export-Csv -Path $Outfile -Encoding UTF8 -Delimiter ';' -NoTypeInformation Write-Host ("`nExported results to {0}" -f $Outfile) -ForegroundColor Green } catch { Write-Warning ("`nCould not export results to {0}, check path and permissions" -f $Outfile) return }
In this Try/Catch, I try to create a new file in the specified path ($outfile). The ErrorAction Stop will stop processing the Try section, switch to the Catch section, and return a warning. The return statement will stop the script afterward, you can also use continue instead of return to get an indication of the error, but the script will continue. (Only applicable when there’s no chance of something destructive 😉 )
While
Sometimes you must wait for a specific condition before continuing your script, while helps you with that. For example:
while (Get-Process -Name Notepad -ErrorAction SilentlyContinue) { Write-Host ("Notepad is still running, waiting for it to close...") Start-Sleep -Seconds 1 } Write-Host ("Notepad closed, continuing...")
In this example above, Get-Process is used to check if Notepad is still running and will report that it is running and wait 1 second before checking again. When it sees that Notepad is no longer running, it will report that it is closed and continue…
Notepad is still running, waiting for it to close... Notepad is still running, waiting for it to close... Notepad is still running, waiting for it to close... Notepad closed, continuing...
But you can also use while to keep running a script until you press Ctrl-C. For example:
while ($true) { Get-ChildItem -Path C:\Temp Start-Sleep -Seconds 30 Clear-Host }
This will show the files in C:\temp. Wait 30 seconds, clear the screen, and show the files in C:\temp until you press Ctrl-C. (I also use this to monitor services or processes. It keeps doing everything inside the curly brackets until you stop it. Better than refreshing/pressing F5 all the time 😉 )
Very handy. Some of the things I knew and used, but for example the string formatting I was having difficulties with pretty recently. This sure looks a lot easier than the solution I ended up with in that case. 🙂
Glad to help 😊
I’ve always struggled to see the real value inn using the {0} {1}… then putting it at the end of the line. If things are not in order and you have multiple parameters you are passing it can get confusing really quick. Especially with parameters flying around. In some cases i can see using it and that’s the beauty of PowerShell there are a bunch of ways to do the same thing. Below is my example of where I see it being more confusing than helpful. So I guess i am looking for where this will become a value when {0} has to be looked up vs having a variable name right there so you know what it is? It’s a very simple example just to try ti illustrate the logic (or lack thereof) in my head.
Create a custom object for demo purposes
$MyCustomObject = New-Object System.Object
$MyCustomObject | Add-Member -type NoteProperty -name Days -Value “365”
$MyCustomObject | Add-Member -type NoteProperty -name Months -Value “12”
$MyCustomObject | Add-Member -type NoteProperty -name Seconds -Value “86400”
Which one is easier to read?
It’s all in the write-host command making it easy to read
Write-Host “There are $($MyCustomObject.days) days in a month. And thewre are $($MyCustomObject.months) months in a year and $($MyCustomObject.seconds) seconds in a day”
You have to traslate the positional formatting to know what variuable is passing the parameter
Write-Host (“There are {0} days in a month. And there are {1} months in a year, and {2} seconds in a day” -f $MyCustomObject.days,$MyCustomObject.months, $MyCustomObject.seconds )
Worse if the are not in order
Write-Host (“There are {1} days in a month. And there are {2} months in a year, and {0} seconds in a day” -f $MyCustomObject.seconds,$MyCustomObject.days,$MyCustomObject.months )
Becomes a problem when you have some mistake like this
Write-Host (“There are {1} days in a month. And there are {2} months in a year, and {0} seconds in a day” -f $MyCustomObject.days,$MyCustomObject.months,$MyCustomObject.seconds)
vs the same mistake with the variable in the string
Write-Host (“There are $($MyCustomObject.months) days in a month. And there are $($MyCustomObject.seconds) months in a year, and $($MyCustomObject.days) seconds in a day” )
I understand what your saying, it’s like you said… There are multiple ways of doing things, personal preference 😉 But for formatting, using {0} / {1} is easier if you have issues escaping special characters… You don’t have to think about that using it like this… But if you don’t run into those issues, then anything goes 😉