The last few month I’m getting contacted by more and more IT folks (mostly Dutch ones for now…) with PowerShell questions. Sometimes a script/command doesn’t work and sometimes it’s just slow.
I especially like it when I’m asked to review a script written by someone that’s a natural VB scripter… some of the amazing constructs are written by such people. Constructs I would never even thought of, or even though they were possible in that way.
Over the last months there are three simple things I’m seeing more and more which have motivated me to write a little post about. So, here are three practical tips that will enable you to do things with less code and/or faster and may even show you how you can use parameters in collaboration to avoid using the pipeline when it’s not needed.
Counting.
Whenever counting objects, most of us use the Measure-Object cmdlet.
For example, when you want to count the number of running processes the following can be used:
1 |
Get-ChildItem -Recurse | Measure-Object | Select-Object -ExpandProperty count |
This looks like a lot of code for such a simple task… so isn’t there a way with less code? Yes there is, by using the script method ‘count’. Bit in order to use this script method, a command needs to be ‘converted’ to a statement which you can do by putting the command between ().
Like so:
1 |
(Get-ChildItem -Recurse).count |
There is a negative side to using .count compared to using the Measure-Object… it’s very resource consuming and takes longer to run!
This is because when using the Count script method, all the information is stored in memory until the task is completed. But when you use the pipe and the Measure-Object cmdlet, all information is in the pipe and is filtered along the way without storing everything in memory; it releases information when no longer included in the pipe.
Note: With the Measure-Object cmdlet you are not only able to count the number of objects but you can also do some other calculations such as, for example, calculating the average and sum.
Conclusion: Use the right approach for the appropriate task and goal.
Filter parameters
Although the pipeline is very powerful, sometimes you don’t need it. Let’s take an example where you want to get all files that include “Microsoft” in their name…
When you use the pipe, the following will get you the desired information:
1 |
Get-ChildItem -Path "D:\Docs" -Recurse | Where-Object {$_.Name -like "*Microsoft*"} |
Depending on the cmdlet, you have the ability to use filter parameters. Some of those are: “-Filter”, ”-Include”, ”-Exclude” and “-Query”. The strength of those parameters is that they leave all the filtering to the
In the case of Get-ChildItem you have the –Filter parameter where you can filter on any property of the objects. So, to get all files that have “Microsoft” included in their name:
1 |
Get-ChildItem -Path "D:\Docs" -Recurse -Filter "*Microsoft*" |
Till this point there is a limitation to using the –Filter parameter compared to using the pipe and the Where-Object cmdlet: the –Filter parameter allows only for one comparison. Where-Object allows for multiple comparisons.
Take for example a situation where you want to get all files that include “Microsoft” in their name but are also larger than 1MB. This would be possible by using the pipe as follows:
1 |
Get-ChildItem -Path "D:\Docs" -Recurse | Where-Object {($_.Name -like "*Microsoft*") -and ($_.Length -gt 1MB)} |
And what if you want to get all files with “Microsoft” in their name and have the .pptx extension?
When using the pipe:
1 |
Get-ChildItem -Path "D:\Docs" -Recurse | Where-Object {$_.Name -like "*Microsoft*pptx"} |
As I stated earlier… there is also a filter parameter named “-Include”.
So, what if you would combine the –Filter and –Include parameter to accomplish the same, but a lot faster?
1 |
Get-ChildItem -Path "D:\Docs" –Recurse -Filter "*Microsoft*" -Include *.pptx |
And just to go a little bit further, you can use just one of both parameters instead!
1 |
Get-ChildItem -Path "D:\Docs" -Recurse -Filter "*Microsoft*pptx" |
1 |
Get-ChildItem -Path "D:\Docs" -Recurse -Include *microsoft*.pptx |
So, when to use which?
In the scenario as described above, it’s very simple. Using the –Filter parameter is faster… but you can’t include objects gathered by using multiple filters. By using the –Include parameter you can:
1 |
Get-ChildItem -Path "D:\Docs" -Recurse -Filter "*Microsoft*pptx","*Microsoft*doc" |
And this way is still faster compared to using the pipe, like follows:
1 |
Get-ChildItem -Path "D:\Docs " -Recurse | Where-Object {($_.Name -like "*Microsoft*.pptx") -or ($_.Name -like "*Microsoft*.doc")} |
Conclusion: When writing scripts with speed as a prerequisite, use the filter parameters and/or combinations of them instead of using the pipe… when it’s possible.
Use the pipe when it’s appropriate
Most IT folks that have done some VB scripting will write great scripts using the ForEach and Foreach-Object cmdlets, when the same task can be accomplished by using the pipeline.
This is something I’ve come across quite a lot when talking to system administrators at my customers. Let’s take an example of moving a department from one city to another.
An example of some code I encounter (quite a lot actually) at customers is the following:
1 2 3 4 5 |
$Users = Get-ADUser -Filter {City -eq "Amsterdam"} Foreach ($User in $Users) { Set-ADUser $User -City "London" } |
Although this will accomplish the task, it is a slow method and resource consuming while there is a far more elegant way of doing this.
When you use the pipe, objects are given to the next pipe in a pipeline. So if the first pipe would get the user objects, the next pipe can use the output of the previous pipe as input.
So, the same goal but by using the pipeline:
1 |
Get-ADUser -Filter {City -eq "Rotterdam"} | Set-ADUser $User -City "London" |
Conclusion: Don’t think in VB, think in PowerShell… think in objects and pipes.