Tag Archives: Sort-Object

Explore the Command and Ensure the Efficiency

The best part of this entire process—the writing about PowerShell for coming up on nine years—has been the realization of my own mistakes while using PowerShell, and then sharing those. I have an upcoming post on this, but in between preparing and publishing that, I, yes me, made another mistake.

Let’s start with what I wrote first, which is included below. It’s a simple Get-ADComputer command. By now, we’ve all likely written plenty of these. Looks great right? Does it though?

Get-ADComputer -Filter * -Properties OperatingSystem,Description |
    Where-Object -Property OperatingSystem -like '*Server*' |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

Before we move forward and determine what I did wrong, let’s use Measure-Command to see how long the execution takes for me on my machine.

Measure-Command -Expression {
    Get-ADComputer -Filter * -Properties OperatingSystem,Description |
        Where-Object -Property OperatingSystem -like '*Server*' |
        Select-Object -Property Name, OperatingSystem |
        Sort-Object -Property OperatingSystem, Name
}
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 4
Milliseconds      : 105
Ticks             : 41055467
TotalDays         : 4.75179016203704E-05
TotalHours        : 0.00114042963888889
TotalMinutes      : 0.0684257783333333
TotalSeconds      : 4.1055467
TotalMilliseconds : 4105.5467

Four seconds. That doesn’t seem too long, but I’m guessing it is based on the way in which the command was written. I never really trust a single test, so let’s set up the above Measure-Command command to run 10 times consecutively using ForEach-Object.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter * -Properties OperatingSystem,Description |
            Where-Object -Property OperatingSystem -like '*Server*' |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      5          370
      5          213
      5          311
      5          839
      4          500
      6          234
      5           50
      5          239
      4          656
      5          421

Multiple tests and this is closer to a full five seconds per invocation. I don’t know about you, but five seconds in PowerShell is an eternity. We use PowerShell for accuracy, sure, but we use it for efficiency, as well. The quicker the better; we shouldn’t limit ourselves. If you find a mistake then fix it.

Now, let’s use the Filter parameter the way it was intended to be used. Piping to Where-Object is always going to be slower than using the filtering options provided by the commands we use. Here’s our base command corrected.

Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

And here it is wrapped up to be tested 10 times such as we did previously.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      1          501
      0          922
      0          907
      0          895
      0          930
      0          906
      1          790
      1          534
      1          284
      0          937

Maybe you’re not worried about those lost seconds. If those aren’t that important, then let it be the possibility that someone’s going to see your failure to be efficient. If I saw this—and again I made this mistake first—I would be concerned that the person writing this code didn’t better explore the command(s) they’re using. If you’re going to use a command, then know it well enough to know how well you’re using it.

Minimum and Maximum Array Values

I determined something on Friday that I’m not sure I’ve needed before. I need to know which date in an array of dates, is the most recent—think the maximum value of the values in the array. Before we get to dates, let’s work with some simple-to-understand numeric values first.

In the below example, we’ll create a variable $Numbers and assign it five, out-of-order numerical values. Without much work, it’s easy to visually parse and determine the lowest value (the minimum value), and the highest value (the maximum value). One is the lowest and five is the highest.

$Numbers = 1,3,5,2,4
($Numbers | Measure-Object -Minimum).Minimum
1
($Numbers | Measure-Object -Maximum).Maximum
5

Let’s overwrite $Numbers with a different set of numbers—a larger set of random numbers. It’s not as easy to determine the smallest and largest of the numbers until we sort them.

$Numbers = Get-Random -Minimum 1 -Maximum 100 -Count 25
$Numbers
26
18
42
88
84
85
28
97
19
29
69
29
21
8
70
7
25
45
80
40
81
86
33
4
49
$Numbers | Sort-Object
4
7
8
18
19
21
25
26
28
29
29
33
40
42
45
49
69
70
80
81
84
85
86
88
97

Even though we can sort it, let’s return to Measure-Object and let it do the work for us. In the next two examples, you’ll notice that unlike the previous times I piped our array to Measure-Object, this time I didn’t only return the Minimum and Maximum properties. Instead, I returned all the properties in the object. They’re not all populated, but there’s a way to do that we’ll see soon enough.

$Numbers | Measure-Object -Minimum
Count             : 25
Average           :
Sum               :
Maximum           :
Minimum           : 4
StandardDeviation :
Property          :
$Numbers | Measure-Object -Maximum
Count             : 25
Average           :
Sum               :
Maximum           : 97
Minimum           :
StandardDeviation :
Property          :

Now, let’s try this with dates—both those that include times, and those that don’t. We’ll start by creating a $Date variable that contains five values. Once the values are assigned to our variable, we’ll display the contents of the array stored in our variable.

$Date = [datetime]'1/1/1975 02:05:48 PM',
[datetime]'2/2,1940',
[datetime]'1/1/1975 02:05:48 AM',
[datetime]'3/1/2022',
[datetime]'5/5/2080'
$Date
Wednesday, January 1, 1975 2:05:48 PM
Friday, February 2, 1940 12:00:00 AM
Wednesday, January 1, 1975 2:05:48 AM
Tuesday, March 1, 2022 12:00:00 AM
Sunday, May 5, 2080 12:00:00 AM

Let’s discuss the above dates before we move forward. We have one in 1940—the oldest—and one in 2080—the newest. These are the minimum and maximum, respectively. We have two on the same date in 1975. One happens at 2:05 in the morning and one at 2:05 in the afternoon. Which is the minimum and maximum of those values? You ought to be able to figure it out based on what you now know. Let’s work through some of the previous examples we did with our numerical array with this array. First, however, let’s ensure we’re working with an array—might as well.

$Date.GetType().BaseType.Name
Array

Ta-da. We’ll start by returning the minimum value, and then the maximum.

($Date | Measure-Object -Minimum).Minimum
Friday, February 2, 1940 12:00:00 AM
($Date | Measure-Object -Maximum).Maximum
Sunday, May 5, 2080 12:00:00 AM

Sure enough, 1940 is our minimum, and 2080 is our maximum. What happens when we sort the entire array? Will the two identical dates that include times sort properly? Let’s find out.

$Date | Sort-Object
Friday, February 2, 1940 12:00:00 AM
Wednesday, January 1, 1975 2:05:48 AM
Wednesday, January 1, 1975 2:05:48 PM
Tuesday, March 1, 2022 12:00:00 AM
Sunday, May 5, 2080 12:00:00 AM

They do. Now to put this to use! Perhaps I’ll be back with why I needed this. Before we wrap up though, let’s determine how to return more than just one property value at a time when using Measure-Object. Before that even, what happens when we don’t include any parameters?

$Numbers | Measure-Object
Count             : 25
Average           :
Sum               :
Maximum           :
Minimum           :
StandardDeviation :
Property          :

It only returns the Count property. We can use the AllStats parameter to return (just about) all of the properties at once.

$Numbers | Measure-Object -AllStats
Count             : 25
Average           : 46.52
Sum               : 1163
Maximum           : 97
Minimum           : 4
StandardDeviation : 29.7561198187308
Property          :

Until next time!

A PowerShell Recursive Function

Note: There is a newer post on tommymaynard.com about a recursive function I wrote. When you are done reading this post, read that one.

I was recently in a position where I was asked to provide an example of a PowerShell recursive function. Unfortunately, I didn’t have time to do that as it was a part of a written and timed test. Sadly, I didn’t even really have time to review the other 50-some answers I had already provided. I mostly knew that might happen.

Anyway, the challenge has been eating at me, so why not write an example and share it. It’ll be good for you, as it will be for me too. It’s out there, but it’s not often utilized that I’m aware. A recursive function is a function that calls, or invokes, itself. For real. We’re not referring to a function, or cmdlet, that includes a Recurse parameter. Now that said, it’s possible that some of these commands, with that parameter, do this (call themselves), but I’ve yet to take a look to see for sure.

After you see today’s example, you’ll begin to understand why you might prefer to do things this way. If you can wrap your head around the concept, you’ll understand that it can reduce a portion of code writing on your part.

As stated, “A recursive function is a function that calls, or invokes, itself.” On that note, let’s first set up the folder structure necessary for this example. While you can do this with PowerShell, I simply didn’t bother. As you can see below, there’s a “Test” folder inside my “Documents” folder. That folder contains three files — A, B, and C.ps1. Additionally, it contains a nested, “More” folder, which contains three more files — D, E, and F.ps1.

So there’s that. Now, let’s take a look at and discuss our PowerShell recursive function.

Function Get-Ps1File {
    Param ($Path= '.\')

    Get-ChildItem -Path $Path | ForEach-Object {
        If ($_.Name -like "*.ps1") {
            $_.Name
        } ElseIf ($_.PSIsContainer) {
            Get-Ps1File -Path $_.FullName
        } # End If-ElseIf.
    } # End ForEach-Object.
} # End Function: Get-Ps1File.

This function loops its way through a path and returns the file(s) that are .ps1 files. If there’s a nested folder, it then goes into that folder and runs itself again (against the content of that folder). Knowing what we know about this folder structure and its files, the below results returned from running this function should make perfect sense.

Get-Ps1File
D.ps1
E.ps1
F.ps1
A.ps1
B.ps1
C.ps1

Let’s recap on this a bit with a little more detail. When the function is first invoked, it uses the current path, as none was supplied. It then runs the ForEach-Object cmdlet against the files in the base folder. Using the If portion of the If-ElseIf statement inside the loop, it determines it should return the A, B, and C .ps1 files. Then, on the fourth iteration through the loop, it hits our “More” folder and determines it’s a folder. There are other ways to determine if it’s a folder, but I stuck with the tried and tested, PSIsContainer NoteProperty.

At this point, it invokes itself against the path of the “More” folder. It runs or executes itself, again. This time, however, it uses the folder’s FullName property (its full path). Inside that second invocation of the function, it iterates over the files inside the “More” folder, echos them to the screen as output, and then ends the function’s second invocation. At that point, it returns back to the first invocation, where it then ends that invocation, as well, as its work is completed.

If you’d prefer to see this sorted, pipe the function name (Get-Ps1File) to the Sort-Object cmdlet.

Get-Ps1File | Sort-Object
A.ps1
B.ps1
C.ps1
D.ps1
E.ps1
F.ps1

And there you go, an example of a PowerShell recursive function. You may not need it often, but you may need it someday.

 

Format PowerShell Results for Outside of PowerShell

There are times I use PowerShell to help format information I need to send along to others in an email. What I mean is that I return results from PowerShell commands, and format it using PowerShell, so that I can simply send it to the clipboard and paste it into an email.

The below example gathers all the computer-related details about my Lync servers each time I open a new Windows PowerShell session. The time to complete is minimal, so I’m perfectly okay with returning all the properties on each of the servers. The extra milliseconds are worth having the most current information on these servers inside my $Lync variable. By the way, you don’t need to know anything about Lync (or Skype) to make use of the post; it’s not the point.

PS > $Lync = Get-ADComputer -Filter * -SearchBase "OU=Lync,DC=MyDomain,DC=com" -Properties *

With the variable set and assigned, I can do things like the next two, combined examples. This comes in handy all the time, and without the need to think about rewriting the command whenever it’s needed again.

PS > $Lync.Name
L-FE01
L-PC02
L-PC01
L-FE02
L-Ed01
L-FE04
L-FE03
L-Ed02

PS > $Lync | Select-Object Name,Description

Name              Description
----              ----------- 
L-FE01            Lync 2013 Front End
L-PC02            Lync 2013 Persistent Chat
L-PC01            Lync 2013 Persistent Chat
L-FE02            Lync 2013 Front End
L-Ed01            Lync 2013 Edge
L-FE04            Lync 2013 Front End
L-FE03            Lync 2013 Front End
L-Ed02            Lync 2013 Edge

Let’s consider that I need to enter the names of the servers into an email and I want them to be comma separated. Easy, we’ll the use the -join operator to complete this task

PS > $Lync.Name -join ','
L-FE01,L-PC02,L-PC01,L-FE02,L-Ed01,L-FE04,L-FE03,L-Ed02
PS >
PS > # Humm... let's add spaces, too.
PS >
PS > $Lync.Name -join ', ' # <-- Notice the trailing space.
L-FE01, L-PC02, L-PC01, L-FE02, L-Ed01, L-FE04, L-FE03, L-Ed02

Because I’m sold on PowerShell, I’ll always take extra time to use it to its full potential. What I wanted to do was add the word “and” after the last comma and a space, and before the name of the final Lync server. This will make the most sense when my text is dropped into an email and used as, or part of, a sentence. We’ll start this example by determining the location of the last comma by using the .LastIndexOf() method. This returns the location within the string.

PS > $index = (($Lync.Name) -join ', ').LastIndexOf(',')
PS > $index
54

Now that we know the location of the last comma, we can remove it and then insert what we want. The next example uses two methods. First the .Remove() method removes the comma, and then the .Insert() method adds everything the way we want it.

PS > (($Lync.Name) -join ', ').Remove($index,1).Insert($index,', and')
L-FE01, L-PC02, L-PC01, L-FE02, L-Ed01, L-FE04, L-FE03, and L-Ed02
PS >
PS > (($Lync.Name) -join ', ').Remove($index,1).Insert($index,', and') | clip.exe

In the last above line, we reran the command and piped it to clip, so that it’s ready to be pasted into my email. After you do this awhile, you find little ways in PowerShell to handle the exact formatting you want. It’s these little tasks, that will give you an opportunity to continue to practice your PowerShell. And finally, here’s the email where I entered the information I had collected and formatted in PowerShell.

format-powershell-results-for-outside-of-powershell-01

Proving PowerShell’s Usefulness to Newbies, Part II

Back again with another example to share with the Windows PowerShell newbies. They’re out there: people that don’t know the practical power in PowerShell. The idea is to use real-world examples of things we simply wouldn’t want to do manually, in hopes that our lost friends will find the light. Maybe, that’s you.

Part I can be found here. In that post, we learned that we could use PowerShell to create 10,000 folders in about 10 seconds — that’s 1,000 folders per second, or 1 folder each millisecond.

Part II
What we’ll do today is read in the content of a file, sort the items we’ve read in, and then write our sorted results back out to the same file — it’s instant alphabetizing. Let’s start by reading in our file and see what we’re starting with.

PS> Get-Content -Path .\file.txt
web02.mydomain.com
web09.mydomain.com
dc03.mydomain.com
dc04.mydomain.com
sql08.mydomain.com
sql06.mydomain.com
dc08.mydomain.com
sql04.mydomain.com
dc10.mydomain.com
web01.mydomain.com
web03.mydomain.com
dc09.mydomain.com
web05.mydomain.com
web06.mydomain.com
dc02.mydomain.com
web07.mydomain.com
dc05.mydomain.com
web04.mydomain.com
web10.mydomain.com
sql01.mydomain.com
dc01.mydomain.com
sql03.mydomain.com
sql10.mydomain.com
web08.mydomain.com
sql05.mydomain.com
sql07.mydomain.com
dc07.mydomain.com
sql02.mydomain.com
sql09.mydomain.com
dc06.mydomain.com

Our file contains 30 fully-qualified servers names that are in no significant order. Sorting all 30 of these server names is probably going to be difficult, or at least, it’s going to be time consuming. Wrong. With one quick addition to the previous command — see the example below — our list is sorted and without any noticeable difference in the amount of time it took to complete.

PS> Get-Content -Path .\file.txt | Sort-Object
dc01.mydomain.com
dc02.mydomain.com
dc03.mydomain.com
dc04.mydomain.com
dc05.mydomain.com
dc06.mydomain.com
dc07.mydomain.com
dc08.mydomain.com
dc09.mydomain.com
dc10.mydomain.com
sql01.mydomain.com
sql02.mydomain.com
sql03.mydomain.com
sql04.mydomain.com
sql05.mydomain.com
sql06.mydomain.com
sql07.mydomain.com
sql08.mydomain.com
sql09.mydomain.com
sql10.mydomain.com
web01.mydomain.com
web02.mydomain.com
web03.mydomain.com
web04.mydomain.com
web05.mydomain.com
web06.mydomain.com
web07.mydomain.com
web08.mydomain.com
web09.mydomain.com
web10.mydomain.com

Yeah, that was tough. I ran this same command 10 times, measuring it with the Measure-Command cmdlet, so I could see the average amount of time it took to sort the list of computers. It completed this “challenge” in anywhere between 6 and 10 milliseconds. I’m sorry, but it would’ve taken me at least a few minutes do to this by hand.

Now that we know we can sort it, let’s write it back to disk. In this example, below, we’ll put our sorted results back into the file where it came from, overwriting the original contents. If you don’t want to do that, then be sure to alter the file name value for Set-Content’s -Path parameter.

PS> Get-Content -Path .\file.txt | Sort-Object | Set-Content -Path .\file.txt
PS>

Let’s verify that our file now contains the sorted contents by rerunning our first Get-Content command from the beginning of this post.

PS> Get-Content -Path .\file.txt
dc01.mydomain.com
dc02.mydomain.com
dc03.mydomain.com
dc04.mydomain.com
dc05.mydomain.com
dc06.mydomain.com
dc07.mydomain.com
dc08.mydomain.com
dc09.mydomain.com
dc10.mydomain.com
sql01.mydomain.com
sql02.mydomain.com
sql03.mydomain.com
sql04.mydomain.com
sql05.mydomain.com
sql06.mydomain.com
sql07.mydomain.com
sql08.mydomain.com
sql09.mydomain.com
sql10.mydomain.com
web01.mydomain.com
web02.mydomain.com
web03.mydomain.com
web04.mydomain.com
web05.mydomain.com
web06.mydomain.com
web07.mydomain.com
web08.mydomain.com
web09.mydomain.com
web10.mydomain.com

That’s it. We ran a one-liner command to read in some data, sort it, and push it right back to the same file. So we were able to compare, I went ahead and sorted the same, 30-line file manually. It took me 3 minutes and 30 seconds (proof below). While this wasn’t the most complex, or lengthy document one might sort, the time difference is still quite drastic. PowerShell has plenty to offer, and spending some time to learn it is going to pay off. This line of work requires all the time you have to continue to learn, and stay relevant. PowerShell can give you some time back.

PS C:\> $StartTime = Get-Date
PS C:\> $EndTime = Get-Date
PS C:\> New-TimeSpan -Start $StartTime -End $EndTime


Days              : 0
Hours             : 0
Minutes           : 3
Seconds           : 30
Milliseconds      : 860
Ticks             : 2108600828
TotalDays         : 0.00244051021759259
TotalHours        : 0.0585722452222222
TotalMinutes      : 3.51433471333333
TotalSeconds      : 210.8600828
TotalMilliseconds : 210860.0828