Tag Archives: foreach

Use Foreach When it’s Really Needed

We’re not all the same, but if we were, and you were like me, you’d have a huge number of tabs open in your browser of choice. Each of them, would have some relation to PowerShell and each would be sitting by idle, and waiting to be read. No idea where it came from, but in one of them last week, I saw this:

Get-Date -Format o | foreach {$_ -replace ":", "."}

I stared at it for a moment, and thought, why is someone piping Get-Date to foreach? Get-Date only returns a single value. Why would that need to be handed off to a looping construct? The point here is, it wouldn’t need to be, even though it works. I’ve decided I should bring this up, in case someone else is potentially going to make this same mistake. Even if you’re a Systems Administrator, you’re still going to want to write efficient, and well thought out code. Here’s how I would have expected to see this written:

(Get-Date -Format o) -replace ":", "."

Again, Get-Date is only going to provide a single returned value, and therefore, we can trust that we don’t need to loop through a set of results. It’s cleaner code, it’s tighter code, and it gives the rest of us some confidence that you’ve thought things through.

One of the things I enjoying doing is testing the speed of various ways to do the same thing. Don’t think for a minute that I didn’t do that here. This next example indicates the time to run the foreach version of this Get-Date command. The second example, indicates the time to run when foreach is not used.

Milliseconds Ticks
------------ -----
           1 19094
           1 10543
           0  7044
           0  4212
           0  3944
           0  3849
           0  4133
           0  4349
           0  4099
           0  3948
Milliseconds Ticks
------------ -----
           1 12386
           0  5277
           0  4438
           0  2479
           0  2345
           0  2325
           0  4003
           0  2352
           0  2335
           0  2311

The times are close; they’re indistinguishable to us humans, but the numbers don’t lie. There’s a better way based both on time to complete, and competency. If you wanted to see it, here’s what I used to measure these two different commands. Keep these things in mind, and keep on learning!

1..10 | ForEach-Object {
    Measure-Command -Expression {
        Get-Date -Format o | foreach {$_ -replace ":", "."}
    } | Select-Object -Property Milliseconds,Ticks
}

1..10 | ForEach-Object {
    Measure-Command -Expression {
        (Get-Date -Format o) -replace ":", "."
    } | Select-Object -Property Milliseconds,Ticks
}

Keep PowerShell Cmdlets Powerful

I’ve seen what I’m about to show you done repeatedly in Windows PowerShell. With that thought in my mind, I’ve decided it’s time to officially write about it. The first time I noticed someone doing it,… ugh, it was me. I did, however, write a correction to my team so they knew I was aware of my error, and as a hope they wouldn’t repeat it.

Those doing this are typically new to PowerShell, but if you aren’t careful and don’t fully understand the capabilities of the cmdlets you use, then it’s possible you might make this mistake, too. What am I talking about? I’m talking about Invoke-Command (alias: icm) and an improper way that it’s sometimes used.

Invoke-Command’s purpose is to run a command, or commands, on remote computers and return the results back to the local computer. That said, it can be used on the local computer, as well; however, this is usually only necessary to check if PowerShell Remoting is working on the local computer. While I don’t normally do this in my examples, I’m piping my result of this example to Format-Table -AutoSize, once they’re returned from the remote computer. This is so it’ll display better on this webpage.

PS> Invoke-Command -Computer DC01 -ScriptBlock {Get-PSDrive -PSProvider FileSystem} | Format-Table -AutoSize

Name Used (GB) Free (GB) Provider Root              CurrentLocation PSComputerName
---- --------- --------- -------- ----              --------------- --------------
A                                 A:\                               DC01
C        12.91     26.75          C:\  Users\tommymaynard\Documents DC01
D                                 D:\                               DC01

This example of Invoke-Command connected to the remote computer DC01, ran a filtered Get-PSDrive command, returned the results to my computer, and then were formatted by the Format-Table cmdlet.

Now, let’s say we have… 23 computer names stored in the variable $Comps and we want to run the command we used above, against each computer. What so many people seem to do is wrap Invoke-Command in a Foreach construct (think: loop). I’m guessing, because I’ve made the error myself, that it’s because someone learned Foreach first. I want to note that I discovered my error on my own, and only moments later. It didn’t wake me at 2 a.m. in a cold sweat after two months.

Here’s how not to do this, and again, $Comps is holding the names of 23 computers.

PS> Foreach ($Computer in $Comps) {
>>> Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
>>> }

Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     --------- --------      ----                                --------------- --------------
C                  20.47         59.18               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  50.36         29.30               C:\                                 ...rd\Documents WEB01
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  53.59         26.06               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

What we’ve done here is forced Invoke-Command to run against only one computer at a time. During each iteration of the Foreach loop, Invoke-Command runs against the computer name currently stored in $Computer. This variable is updated to the next computer name in the variable at the start of each loop. It’s like this: connect to DC02 and run the command. Now, connect to WEB01 and run the command. Are you done, Invoke-Command? Okay then, now Foreach says to run the same command against the computer SQL01. That’s not how this cmdlet was designed to be used.

Before we go any further, I wrapped this command inside the Measure-Command cmdlet to determine how long it took my system to run this against the computers in $Comps. It took nearly 30 seconds. While that’s not an eternity, we’re only talking about 23 computers, and we’re only talking about a single command.

PS> Measure-Command -Expression {Foreach ($Computer in $Comps) {
>>> Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
>>> }
>>> }

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 27
Milliseconds      : 513
Ticks             : 275132689
TotalDays         : 0.000318440612268519
TotalHours        : 0.00764257469444444
TotalMinutes      : 0.458554481666667
TotalSeconds      : 27.5132689
TotalMilliseconds : 27513.2689

Now, let’s set this command up the proper way. The -ComputerName parameter of Invoke-Command can take a collection of computers. It should be said that many cmdlets can take more than one computer as a value to their -ComputerName parameter. The difference, is that Invoke-Command can run against all of the 23 computers at the same time. In fact, the default throttle limit — the number of computers in which the command will run against simultaneously — is 32. It can be changed by including the -ThrottleLimit parameter, such as -ThrottleLimit 50 or -ThrottleLimit 15.

Now, here’s how this command should’ve been written.

PS> Invoke-Command -ComputerName $Comps -ScriptBlock {Get-PSDrive -PSProvider FileSystem}

Name           Used (GB)     Free (GB) Provider      Root                                CurrentLocation PSComputerName
----           ---------     --------- --------      ----                                --------------- --------------
A                                                    A:\                                                 DC02
A                                                    A:\                                                 WEB01
C                  41.72         37.93               C:\                                 ...rd\Documents WEB01
C                  41.13         38.52               C:\                                 ...rd\Documents DC02
D                                                    D:\                                                 DC02
D                                                    D:\                                                 WEB01
A                                                    A:\                                                 SQL01
C                  46.75         32.91               C:\                                 ...rd\Documents SQL01
D                                                    D:\                                                 SQL01
... # Not all results included.

I measured this command five times, and the average of those five runs was only 2.8 seconds. Remember, the Foreach loop took almost 30 seconds to get the same results. This is due to the fact that, again, Invoke-Command will work with the remote computers simultaneously (when it’s not inside a Foreach). There’s never any waiting to run the command on any additional computers unless there’s more computers than the value of the ThrottleLimit.

Please keep the topic in this post in mind, as it’s possible to momentarily forget this feature just long enough to wrap the Invoke-Command cmdlet in a Foreach, or looping, construct.

Report on Active Directory Objects in Abandoned Organizational Unit

Before we really start this post, I should mentioned that there’s no reason that the script discussed in this post can’t be run against an Organizational Unit (OU) that hasn’t been abandoned. It just worked out that I wrote the script in order to determine if an OU had be abandoned.

I threw a small script together in the last couple days and thought I’d share it. The reason for the script was because I may have had an Active Directory (AD) OU that was no longer being used. In order to determine if this was really the case, I wanted to check various properties on the user and computer objects in the OU, to include any nested OUs. These properties included the last logon time stamp, the last date the objects changed, and a few others.

The first couple of lines in the script set two different variables. The first one stores the Domain’s Distinguished Name, and the second one is assigned the location of the abandoned OU. The second variable is based partly on the first. This script requires the ActiveDirectory module and assumes it’s being run on PowerShell 3.0 or greater, as the AD module isn’t explicitly imported.

$DomainDN = ((Get-ADDomain).DistinguishedName)
$AbandonedOU = Get-ADObject -Filter * -SearchBase "OU=Finance,OU=Departments,$DomainDN"

In the next part of the script, we start to send the $AbandonedOU variable’s objects across the pipeline, to the Foreach-Object cmdlet. As each object passes across, we determine what type of AD object we’re dealing with. If it’s a user object, we set the $Command variable to the string, Get-ADUser. If it’s a computer object we set the $Command variable to the string, Get-ADComputer. If it’s neither, such as a nested OU, we’ll return to the $AbandonedOU variable and send the next object without assigning anything to the $Command variable (or running any of the upcoming code).

$AbandonedOU | ForEach-Object {
    If ($_.ObjectClass -eq 'user') {
        $Command = 'Get-ADUser'
    } ElseIf ($_.ObjectClass -eq 'computer') {
        $Command = 'Get-ADComputer'
    } Else {
        return
    }

Providing we have a user or computer AD object, we’ll run the code in the next example. This will execute the cmdlet, whether it be Get-ADUser or Get-ADComputer, returning the requested properties that we then calculate (think, customize).

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
}

Finally, we sort the collection of objects we’ve returned and customized, and in my case, pump the data out to a CSV file at the root of my C:\ drive. As you’ll see below, I’ve included both the code in the previous example and the additional code.

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
} | Sort-Object 'Last Logon TimeStamp' -Descending | Export-Csv -Path C:\AbandonedOU.csv -NoTypeInformation

I want to mention something about the line above that calculates the “Added To Domain By” property. In many environments this is going to only be <DOMAIN>\Domain Admins. The reason I added this, is because in the AD environment in which this ran, users, other than the Domain Admins, can join computers. I know this is a default; however, in many environments it is not allowed. This may or may not be a helpful property in your environment.

Cheers, and thanks for reading! I’ve included the complete script below.

$DomainDN = ((Get-ADDomain).DistinguishedName)
$AbandonedOU = Get-ADObject -Filter * -SearchBase "OU=Finance,OU=Departments,$DomainDN"

$AbandonedOU | ForEach-Object {
    If ($_.ObjectClass -eq 'user') {
        $Command = 'Get-ADUser'
    } ElseIf ($_.ObjectClass -eq 'computer') {
        $Command = 'Get-ADComputer'
    } Else {
        return
    }

    & $Command -Identity $_ -Properties * |
        Select-Object Name,
            @{N='Type';E={$_.ObjectClass}},
            @{N='Created';E={$_.whenCreated}},
            @{N='Last Logon TimeStamp';E={[datetime]::FromFileTime($_.LastLogonTimeStamp)}},
            @{N='Changed';E={$_.whenChanged}},
            @{N='Added To Domain By';E={$_.nTSecurityDescriptor.Owner}}
} | Sort-Object 'Last Logon TimeStamp' -Descending | Export-Csv -Path C:\AbandonedOU.csv -NoTypeInformation

Using the Range Operator for Calculating Total Push-Ups

In December of 2014, I decided that my life in 2015 was in need of some push-ups. Instead of just starting with 10 a day, or some other arbitrary number, I thought I would do as many push-ups a day as it was the day in the year. This meant that on day one (January 1, 2015), I would do one push-up and on day two, I would do two push-ups, and so on. Today is the 20th day of the new year, and so I’ll have to do 20 tonight. I wanted to know how many push-ups I will have done by January 31st. Being the Windows PowerShell hobbyist that I am, I enlisted PowerShell to do my calculations for me.

I started with a variable, $EndDay, and the range operator (..). The combination of the two provides me an integer array of the days in January, such as 1..$EndDay (or, 1..31). Using this, I can calculate how many total push-ups I will have done by the end of the day on January 31st. The example below sets up the integer array, as well as the ForEach-Object loop where we’ll do our calculations. Note: I’m using the ForEach-Object alias, foreach.

$EndDay = 31
1..$EndDay | foreach {

}

The first thing we do, below, is include a second variable, $PushUps, that will collect the total number of push-ups for the month. We’ll use the += assignment operator. This operator takes whatever is already in $PushUps, and adds to it. If the current value stored in $PushUps was 1, and we used the += assignment operator like so, $PushUps += 2, then the value in $PushUps would be 3 (1 + 2 is equal to 3). If we used the standard assignment operator (=), then $PushUps would be 2, as 1 would be overwritten.

On the next line, below, we write some information on the screen. We write the current day: that’s the current number from the integer array represented by $_ (as of PowerShell 3.0, $_ can be represented as $PSItem). Then we write out the total number of push-ups completed by that day: $PushUps.

$EndDay = 31
1..$EndDay | foreach {
    $PushUps += $_
    Write-Output -Verbose "Day: $_ / PushUp Total: $PushUps"
}

I noticed that when I reran the code in the ISE, that the value of $PushUps was incorrect on the second run. This is because the variable already exists, and by the end of the first run already contains 496—the number of push-ups I’ll have done by the end of January! Therefore, I added an If statement that removed the $PushUps variable when $_ was equal to $EndDay. This happens on the final run through the foreach.

$EndDay = 31
1..$EndDay| foreach {
    $PushUps += $_
    Write-Output -Verbose "Day: $_ / PushUp Total: $PushUps"
    If ($_ -eq $EndDay) {
        Remove-Variable PushUps
    }
}

If you change the value for $EndDay to 365, you’ll be able to determine that after December 31st (if I can somehow keep this up) I will have done 66,299 total push-ups for the year. It’s hard to imagine that I could do 365 push-ups at once, but then again, it’s hard to imagine I’ll get though the rest of the month. Here’s an image that shows the the full results when we run the function above.

Using the Range Operator for Push-Up Calculations

Thanks for reading, and wish me good luck—I’m going to need it.