Category Archives: Quick Learn

Practical examples of PowerShell concepts gathered from specific projects, forum replies, or general PowerShell use.

Search PowerShell Gallery Module for #Requires Statement

The first part of this post began on PowerShell.org. Start there, unless you already have: http://powershell.org/wp/2015/11/30/search-powershell-gallery-module-for-requires-statement.

Please use the connect.microsoft.com link here, or at the bottom of this post, to up vote my feedback.

Desired State Configuration (DSC) and Just Enough Administration (JEA) are two topics that have recently piqued my interest. This, after becoming involved in Windows PowerShell constrained endpoints, and writing proxy functions, to better control how cmdlets are used. While working with DSC and xJEA (x, as in experimental), I ran up against an error message in the Event Viewer on my Server 2012 R2 target node running Windows Management Framework (WMF) 4.0 (the installation package that contains PowerShell 4.0). Before I get too deeply involved in JEA in WMF 5.0 on Server 2016 — what appears to be constrained endpoints of the past, with the benefits of JEA, but without the need for DSC, and some additional new features — I wanted to ensure I was able to deploy JEA endpoints with DSC, while running WMF 4.0.

Knowing this, the first error message below, makes perfect sense. I downloaded a version of xJEA that had a PowerShell 5.0 requirement, without even knowing it. Instead of moving up to WMF 5.0 to “fix” this, I opted to first get an older version of xJEA and determine if I was able to make it run with a version that can be used on WMF 4.0. This seemed like a logical progression in my learning and understanding of both DSC and JEA. Here’s the error:

“Error Message = The script ‘MSFT_xJeaToolkit.psm1’ cannot be run because it contained a “#requires” statement for Windows PowerShell 5.0. The version of Windows PowerShell that is required by the script does not match the currently running version of Windows PowerShell 4.0.”

As to be expected, the error message that was reported in the console host, was much less helpful. In fact, I was at a total loss until I went hunting in the Event Viewer, where I turned up the previous error message. Here’s the error I saw in the console:

“Invoke-CimMethod : Failed to extract the module from zip file C:\Windows\TEMP\\635842583367244849\xJea_0.2.16.6.zip
downloaded by Download Manager WebDownloadManager.”

In order to determine which version of xJEA didn’t require PowerShell 5.0, I needed to download all the xJEA versions and inspect them. You see, there isn’t a way to find out if there’s a PowerShell version requirement… more on that shortly. To begin, I ensured that the PowerShell Gallery was a trusted repository, and therefore, wouldn’t prompt me when I ran the Save-Module command in an upcoming example. To do this, I ran the commands below to (1) verify if the PowerShell Gallery was trusted (which it wasn’t, and isn’t by default), (2) trust it as an installation source, and (3) verify it was trusted.

PS> Get-PSRepository

Name                      PackageManagementPro InstallationPolicy   SourceLocation
                          vider
----                      -------------------- ------------------   --------------
PSGallery                 NuGet                Untrusted            https://www.powershellgallery.com/api/v2/


PS> Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
PS> Get-PSRepository

Name                      PackageManagementPro InstallationPolicy   SourceLocation
                          vider
----                      -------------------- ------------------   --------------
PSGallery                 NuGet                Trusted              https://www.powershellgallery.com/api/v2/

When that was completed, what I needed was to download all the xJEA modules from the PowerShell Gallery, at once, and place them into a folder for each version. Luckily, the Save-Module cmdlet will handle the file structure. This means, each module (a DSC resource, in this case) will be saved inside a version folder, nested in the same xJea folder. I gave the cmdlet a base path (C:\), and it did the rest. Take a look below at the example and image.

PS> Find-Module -Name xJea -AllVersions |
>> ForEach-Object {Save-Module -Path C:\ -Name xJea -RequiredVersion "$($_.Version.ToString())"}

Search PowerShell Gallery Module for requires01

Once I had all the files downloaded, I needed to run through all the contained .psm1 files for the requires statement for PowerShell version 5.0. The command below returned 51 files where it located the string “requires -version 5.”

PS> (Get-ChildItem -Path C:\xJea -Recurse -Filter '*.psm1' |
>> Get-Content) -match 'requires -version 5' |
>> Select-Object PSPath | Convert-Path
C:\xJea\0.2.10\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.10\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.10\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.11\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.11\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.11\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.12\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.12\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.12\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.13\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.13\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.13\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.14\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.14\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.14\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.15\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.15\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.15\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.1\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.1\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.1\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.2\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.2\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.2\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.3\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.3\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.3\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.4\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.4\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.4\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.5\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.5\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.5\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.16.6\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.16.6\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.16.6\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.5\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.5\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.6\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.6\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.7\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.7\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.8\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.8\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.8\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
C:\xJea\0.2.9\DSCResources\Library\JeaAccount.psm1
C:\xJea\0.2.9\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
C:\xJea\0.2.9\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1

In the next two examples, I haven’t included all the results to save some space. The example below, takes the previous one, and removes the portion of the path to the left of the version number. Compare the last line in the example above with the last line in the example below, and you’ll see how the .Split() method cleaned up the beginning of each result.

PS> (Get-ChildItem -Path C:\xJea -Recurse -Filter '*.psm1' |
>> Get-Content) -match 'requires -version 5' |
>> Select-Object PSPath | Convert-Path |
>> ForEach-Object {$_.Split('\',3)[-1]}
0.2.10\DSCResources\Library\JeaAccount.psm1
0.2.10\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
0.2.10\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
0.2.11\DSCResources\Library\JeaAccount.psm1
0.2.11\DSCResources\MSFT_xJeaEndpoint\MSFT_xJeaEndpoint.psm1
0.2.11\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1
...
0.2.9\DSCResources\MSFT_xJeaToolkit\MSFT_xJeaToolkit.psm1

In this example, we added a second .Split() method to clean up the path information to the right of the version number. Now, we only have the versions, although we do have duplicates.

PS> (Get-ChildItem -Path C:\xJea -Recurse -Filter '*.psm1' |
>> Get-Content) -match 'requires -version 5' |
>> Select-Object PSPath | Convert-Path |
>> Foreach-Object {$_.Split('\',3)[-1].Split('\')[0]}
0.2.10
0.2.10
0.2.10
0.2.11
0.2.11
0.2.11
..
0.2.9

This example below, shows the full results again; however, each version is only listed one time. This is because we’ve piped the previous results to Select-Object -Unique. This means, for example, we only see 0.2.11 once, even though it appeared three times in the last example.

PS> (Get-ChildItem -Path C:\xJea -Recurse -Filter '*.psm1' |
>> Get-Content) -match 'requires -version 5' |
>> Select-Object PSPath | Convert-Path |
>> ForEach-Object {$_.Split('\',3)[-1].Split('\')[0]} |
>> Select-Object -Unique
0.2.10
0.2.11
0.2.12
0.2.13
0.2.14
0.2.15
0.2.16
0.2.16.1
0.2.16.2
0.2.16.3
0.2.16.4
0.2.16.5
0.2.16.6
0.2.5
0.2.6
0.2.7
0.2.8
0.2.9

We now have a complete list of all the versions that require PowerShell 5.0. That means we can determine which versions will run with PowerShell 4.0.

In this final example, we set a variable, $VersionsRequireWMF5, to the results produced by our last command. These are the versions that require PowerShell 5.0. In the middle of the example below, we set a second variable, $AllVersions, to all the versions of xJEA that we downloaded. We did this by running a Get-ChildItem (think, dir or ls) against the C:\xJea directory and returned just the directory names. That gives us two variables that we can supply to the Compare-Object cmdlet. That cmdlet will tell us the differences between the values of the variables, indicating which versions I can use with WMF 4.0.

PS> $VersionsRequireWMF5 = (Get-ChildItem -Path C:\xJea -Recurse -Filter '*.psm1' | 
>> Get-Content) -match 'requires -version 5' | 
>> Select-Object PSPath | Convert-Path | 
>> ForEach-Object {$_.Split('\',3)[-1].Split('\')[0]} | 
>> Select-Object -Unique
PS>  
PS> $AllVersions = (Get-ChildItem -Path C:\xJea | Select-Object).Name
PS>  
PS> Compare-Object -ReferenceObject $AllVersions -DifferenceObject $VersionsRequireWMF5

InputObject SideIndicator
----------- -------------
0.2         <=
0.2.1       <=
0.2.2       <=
0.2.4       <=

Based on these results, version 0.2, 0.2.1, 0.2.2, and 0.2.4, do not require WMF 5.0 and can be tested with WMF 4.0, before moving to the newer version of WMF. You can expect that I’ll work with 0.2.4 tomorrow morning.

A final note: Microsoft may not yet think they need it, but they should indicate the version of PowerShell that is required by modules on the PowerShell Gallery. That needs to be a part of their approval process, and the results need to added to the web interface, and made a part of the results returned by the Find-Module cmdlet (see the full Find-Module results below). These results should include a Requires property. This isn’t going to be the last time someone comes up against this problem, especially as we move into versions post WMF 5.0.

PS> Find-Module -Name xJea | Select-Object *

Name : xJea
Version : 0.2.16.6
Description : Module with DSC Resources for Just Enough Admin (JEA). Jea makes it simple to create
custom RBAC solutions using PowerShell.
Author : Microsoft Corporation
CompanyName :
Copyright : (c) 2014 Microsoft Corporation. All rights reserved.
PublishedDate : 5/14/2015 7:51:23 PM
LicenseUri :
ProjectUri :
IconUri :
Tags : {PSModule, PSIncludes_DscResource}
Includes : {Function, DscResource, Cmdlet, Command}
PowerShellGetFormatVersion :
ReleaseNotes :
Dependencies : {}
RepositorySourceLocation : https://www.powershellgallery.com/api/v2/
Repository : PSGallery
PackageManagementProvider : NuGet

If you think this should be included, then up vote the feedback I’ve left on connect.microsoft.com.

Invoke a Command from a Get-History Menu

Sometimes you line up the right cmdlets, appreciate what you’ve done for a moment, only to become mildly irritated that you never thought of that before. I just did that.

We all know, or should rather, that the Get-History cmdlet returns a list of commands that have been entered during the current Windows PowerShell session. If you open up a new console, enter a couple commands, and then run Get-History, or one of its aliases (ghy, h, history), it’ll show you what commands you’ve entered up until that point. Take this example, for instance:

PS> Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost


PS> Get-Service | Select-Object -First 2

Status   Name               DisplayName
------   ----               -----------
Running  AdobeARMservice    Adobe Acrobat Update Service
Stopped  AdobeFlashPlaye... Adobe Flash Player Update Service


PS> Get-History

  Id CommandLine
  -- -----------
   1 Get-Process | Select-Object -Last 2
   2 Get-Service | Select-Object -First 2


PS> Get-History

  Id CommandLine
  -- -----------
   1 Get-Process | Select-Object -Last 2
   2 Get-Service | Select-Object -First 2
   3 Get-History

I use the up arrow quite often to cycle through my previous commands, and press Enter when I’ve found the one I want to run again. That’s one way to rerun a previously run command. Another, is to use the Get-History cmdlet. First, you use it to determine the ID of the command you want to rerun, such as we’ve done above. Then, you can run Get-History again with the -Id parameter and pipe that result to Invoke-History (aliases: ihy, r), such as we’ve done below. You can also just use Invoke-History -Id 1 without the use of Get-History.

PS> Get-History -Id 1 | Invoke-History
Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost

What about making a “menu,” where we can select the command we want to run and press OK to run it? Easy. If you’ve been paying attention to PowerShell, then you’ve probably seen the Out-GridView cmdlet before. Let’s put it to good use, and I’ll show you the cmdlets I lined up.

PS> Get-History | Out-GridView -PassThru | Invoke-History

When I enter the command above, it will show the dialog box below; however, your dialog box will be the default size. I’ve resized mine so that the image better fits on this page. If it’s important to you, you can change what the title says by including Out-GridView’s -Title parameter: Get-History | Out-GridView -PassThru -Title ‘History Menu’ | Invoke-History.

Invoke-a-Command-from-Get-History-Menu01

After I select an option from the list, and press OK, the history item will be rerun.

PS> Get-History | Out-GridView -PassThru | Invoke-History
Get-Process | Select-Object -Last 2

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    263      11     1680       5132    46     0.02   3356 WUDFHost
    227      12     1824       6804    47     0.22   3840 WUDFHost

PS>

Well, there it is. Something so simple and so obvious that I can’t believe I never thought of it before.

Get Active Directory (Sites and Services) Subnets

I just started reading chapter 16 — Managing sites and subnets — in Richard Siddaway‘s book Learn Active Directory in a Month of Lunches. I already know a good deal about Active Directory (AD). I’ve been using and supporting it, in one fashion or another, since its introduction in 2000. Even so, that didn’t stop me from picking up this title. I’ll buy and read a book about a topic I already know, even if I’m only going to pick up a few new details. I also like to remind myself of things I already know, in an effort to keep things as fresh as possible in my mind. I’ve also found myself curious about other people’s writing styles. Since I’ve been writing and posting here, for coming up on a year and a half, I believe that my writing has improved, and it makes me eager to read other people’s writings, too.

I went a little off topic there, but anyway, I read a couple pages of the chapter, and then typed the command below into my Windows PowerShell console. This was in an effort to determine the subnets used by an AD site, before reading any further.

PS> (Get-ADReplicationSite -Identity Downtown02 -Properties Subnets).Subnets.ForEach{$_.Split(',',2)[0].Split('=')[-1]}
10.115.0.0/16
10.122.0.0/15
10.140.0.0/16

So what’s happening here? By default, the Active Directory Get-ADReplicationSite cmdlet returns information about your (computer’s) AD site; however, you can filter the results to get information about another AD site, or even, all the AD sites. In my example, I’ve indicated that I only want information about a single site, Downtown02. Although you don’t see it in the default properties that are returned by this cmdlet, there are extended properties returned when the -Properties parameter is included. This behavior is similar to some of the other AD cmdlets. In fact, you can see which AD cmdlets have a -Properties parameter by enter this: Get-Command -Module ActiveDirectory -ParameterName Properties. In our example, we are only returning the Subnets property, not all of the properties, as we would when using -Properties *.

The Subnet property is returned as a Distinguished Name, such as: CN=10.115.0.0/16,CN=Subnets,CN=Sites,CN=Configuration,DC=mydomain,DC=com. Due to this, I’ve added the .Split() method, twice. The first time it was used, we split at the comma and returned two pieces of our string, when it’s entered as .Split(‘,’,2). That’s difficult to explain, so here’s what that looks like:

PS> (Get-ADReplicationSite -Identity Downtown02 -Properties Subnets).Subnets.ForEach{$_.Split(',',2)}
CN=10.115.0.0/16
CN=Subnets,CN=Sites,CN=Configuration,DC=catnet,DC=mydomain,DC=com
CN=10.122.0.0/15
CN=Subnets,CN=Sites,CN=Configuration,DC=catnet,DC=mydomain,DC=com
CN=10.140.0.0/16
CN=Subnets,CN=Sites,CN=Configuration,DC=catnet,DC=mydomain,DC=com

In the example above, notice it only split at the first comma in the string. That’s what the 2 does (“leave me 2 sections”). Adding the [0] index, below, only keeps the first element of the results of each split.

PS> (Get-ADReplicationSite -Identity Downtown02 -Properties Subnets).Subnets.ForEach{$_.Split(',',2)[0]}
CN=10.115.0.0/16
CN=10.122.0.0/15
CN=10.140.0.0/16

The second time we use the .Split() method, we split at the equal sign and return the last element, like so: .Split(‘=’)[-1]. After these two .Split() methods have run, we have our subnets, just like we’d see them in the Name category, of the Active Directory Sites and Services MMC. I should mention the syntax in this command. Take a closer look, if you haven’t already: It’s using the ForEach method, not the ForEach-Object cmdlet, as you might have expected to see. This syntax became possible in PowerShell 4.0.

Okay, so I’ve mentioned this a time or two, now. I often learn something new, share it here, and then I find a better way. Sometime, while I’m still writing. Well, that happened again. We don’t need to get fancy with the results of Get-ADReplicationSite as we have above, and can instead, use the Get-ADReplicationSubnet cmdlet. How’d I find the cmdlet? Well, (while writing), I decided to search for the string “subnet” in all the cmdlet names from the ActiveDirectory module: Get-Command -Module ActiveDirectory -Name *subnet*. Here’s the command to use to return the same information we did with the first example in this post.

PS> (Get-ADReplicationSubnet -Filter * -Properties Site | Where-Object Site -like '*Downtown02*').Name
10.115.0.0/16
10.122.0.0/15
10.140.0.0/16

If you’re wondering why I didn’t use the -Filter parameter to filter on the site, well then, here you go. Here’s a quicker and more efficient method in which to write the command.

PS> (Get-ADReplicationSubnet -Filter {Site -eq 'Downtown02'}).Name
10.115.0.0/16
10.122.0.0/15
10.140.0.0/16

Thanks for joining me, and now back to my reading.

Cmdlet and Function Alias Best Practice

Aliases are a beautiful thing, really. Even though this may be true, we need to make sure we’re following best practice, when we opt to use them. I’m not sure what it is about me, but I get a little antsy whenever I see someone not conforming to a best practice in PowerShell. I really don’t care how you lace and tie your shoes, or make your peanut butter and jelly sandwiches, but when you use your PowerShell aliases is important to me, and it should be to you, too.

The main idea here is that cmdlet and function aliases shouldn’t exist in something that lasts longer than a one time use. That means we shouldn’t see them in help documentation (although, I believe I’ve seen some), in online articles, and in PowerShell-related forum posts. The only time you’ll see me use a cmdlet or function alias, is if you’re looking over my shoulder and watching me type commands into my console, or the ISE’s console pane. Realistically, aliases do two things: One, they help speed up getting results in a one time use scenario, as I’ve mentioned, and two, they confuse PowerShell newcomers that come across a blog where they’ve been used without consideration to best practice. Perhaps it’s my desire to help people learn PowerShell, that’s driving some of this alias indignation. I should mention, that I think it’s acceptable to include them in cases where you also explain that you’re using an alias, and indicate the full cmdlet or function name.

If you showed up here after seeing one of those blogs, or scripts, where someone littered their work with aliases, keep in mind that we have a Get-Alias cmdlet that can help you determine to which cmdlet an alias resolves — we’ll get back to that momentarily. Let’s say you found this command online:

PS> ls c:\windows | ? {$_ -like 'Win*'} | % {echo "$($_.Name)||$($_.LastWriteTime)"}

I understand this command has the potential to be written better. One such way, would be to not pipe to ? in order to filter on the file or directory name, but instead to use the ls -Filter parameter. Weird… in that last sentence, I used a couple aliases instead of their cmdlet names, and even that was confusing. Imagine having read that as a newcomer. This command, since it’s going to live on this webpage “forever,” should have been written like this (for the most part):

PS> Get-ChildItem -Path c:\windows | Where-Object {$_ -like 'Win*'} | ForEach-Object {Write-Output -InputObject "$($_.Name)||$($_.LastWriteTime)"}

It’s longer, there’s no doubt about it, but it’s much more complete and easier to comprehend, especially had you found it in a lengthy script or function.

Back to Get-Alias: If you find yourself confused by a command you found online, where someone “didn’t think of the next person,” then run though the command, or commands, and check the aliases against the Get-Alias cmdlet. Let’s do that below, for the first example command I wrote. Notice that Get-Alias will accept a comma-separated list of (alias) values.

PS> Get-Alias -Name ls,?,%,echo

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Alias           ls -> Get-ChildItem
Alias           % -> ForEach-Object
Alias           ? -> Where-Object
Alias           h -> Get-History
Alias           r -> Invoke-History
Alias           % -> ForEach-Object
Alias           echo -> Write-Output

Now, notice the results. We’ve returned aliases that we didn’t request, such as h for Get-History and r for Invoke-History. Why? A little off topic, but this is because the question mark (?), in regular expressions, or regex, stands in for a single character. The results are not only returning the alias for the question mark, but for any aliases that only have a single character. In order to only return what we want, we’ll need to escape the question mark character and put it in quotes, so that the parser is certain we have supplied the string value, of an actual question mark.

PS> Get-Alias -Name ls,'`?',%,echo

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Alias           ls -> Get-ChildItem
Alias           ? -> Where-Object
Alias           % -> ForEach-Object
Alias           echo -> Write-Output

There, we go.

As a community, we should do things so they better help the current, and future members. Use all the aliases you want, but do so where they won’t exist for someone else to stumble upon, unless, you’ve taken the time to explain that you’ve used an alias, and to which cmdlet it refers.

In closing, I want to mention the PSScriptAnalyzer module. I pulled it down and installed it in PowerShell 5.0, using the Find-Module and Install-Module cmdlets: Find-Module -Name PSScriptAnalyzer | Install-Module. I then copied my alias heavily command above, pasted it into notepad, and saved it as C:\file.ps1. Following that, I ran an Invoke-ScriptAnalyzer command. You can see the command and its results below

PS> Invoke-ScriptAnalyzer -Path C:\file.ps1 -IncludeRule PSAvoidUsingCmdletAliases

RuleName                            Severity     FileName   Line  Message
--------                            --------     --------   ----  -------
PSAvoidUsingCmdletAliases           Warning      file.ps1   1     'echo' is an alias of 'Write-Output'. Alias can
                                                                  introduce possible problems and make scripts hard to
                                                                  maintain. Please consider changing alias to its full
                                                                  content.
PSAvoidUsingCmdletAliases           Warning      file.ps1   1     '%' is an alias of 'ForEach-Object'. Alias can
                                                                  introduce possible problems and make scripts hard to
                                                                  maintain. Please consider changing alias to its full
                                                                  content.
PSAvoidUsingCmdletAliases           Warning      file.ps1   1     '?' is an alias of 'Where-Object'. Alias can
                                                                  introduce possible problems and make scripts hard to
                                                                  maintain. Please consider changing alias to its full
                                                                  content.
PSAvoidUsingCmdletAliases           Warning      file.ps1   1     'ls' is an alias of 'Get-ChildItem'. Alias can
                                                                  introduce possible problems and make scripts hard to
                                                                  maintain. Please consider changing alias to its full
                                                                  content.

The cmdlet was instructed to only return the problems it found with aliases (see the parameter used and the included value). It’s a very powerful cmdlet; not only can you find any alias-related best practice failures, but it’ll help you locate any other areas, where you can improve your code.

Thanks for your time, and… your new dedication to not use cmdlet and function aliases, that last longer than a one time use.

Update: I just saw a post from The Scripting! Guy (http://blogs.technet.com/b/heyscriptingguy/archive/2015/10/25/powertip-group-powershell-cmdlet-count-by-version.aspx) where he used a few aliases and told us what they mean. If you’re going to use them, explain them.

Run Background Commands after Every Command (Part II)

The first part of this post began on PowerShell.org. Start there, and it’ll get you to Part I: http://powershell.org/wp/2015/10/12/run-background-commands-after-every-command.

Recently I shared a small function I wrote to replace my prompt function. In addition to creating the prompt (PS: C\>) each time one command ended, it would run some background commands. In my attempt to take advantage of this function, I added code that would modify the WindowTitle, the text at the top of the console host or ISE, to indicate if there were any background jobs. Today, I’ve improved the function.

The additions I made include all of the following:
– Uses the singular word “job” when there is only one background job.
– Uses the plural word “jobs” when there is more than one background job.
– Adds an asterisk (*) when any job is actively running.
– Adds a plus sign (+) when any job has more data.

Here’s the full, updated function, and some sequential images beneath that. I’ve dumped the nested If statement for a switch statement, improving the function’s readability. Before you get into this, there’s something to consider. The indicators are only as accurate as the last time the prompt function was invoked. They won’t update without the user pressing the Enter key. That said, it’s quite convenient that you can determine if a background job is still running by pressing Enter. You don’t even have to type anything.

Function Prompt {
    If (-Not($OriginalTitle)) {
        $Global:OriginalTitle = $Host.UI.RawUI.WindowTitle
    }

    If (Get-Job) {
        $Job = Get-Job
        Switch ($Job) {
            {$Job.State -eq 'Running'} {$State = '*'}
            {$Job.HasMoreData -eq $true} {$MoreData = '+'}
            {$Job.Count -eq 1} {$Host.UI.RawUI.WindowTitle = "[$($State)Job$($MoreData)] $OriginalTitle"; break}
            {$Job.Count -gt 1} {$Host.UI.RawUI.WindowTitle = "[$($State)Jobs$($MoreData)] $OriginalTitle"}
        }
    } Else {
        $Host.UI.RawUI.WindowTitle = $OriginalTitle
    }

    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

Run-background-commands-after-every-command-PartII-01

Run-background-commands-after-every-command-PartII-02

Run-background-commands-after-every-command-PartII-03

Run-background-commands-after-every-command-PartII-04

Update: After looking at this a few more times, I decided to move the asterisk indicator (running jobs) to the back of the word Job, or Jobs, next the plus sign indicator (has more data). I thought it looked better. The code changes I made, are seen below, and a comparison image is down there, too. Cheers!

Function Prompt {
...
            {$Job.Count -eq 1} {$Host.UI.RawUI.WindowTitle = "[Job$($State)$($MoreData)] $OriginalTitle"; break}
            {$Job.Count -gt 1} {$Host.UI.RawUI.WindowTitle = "[Jobs$($State)$($MoreData)] $OriginalTitle"}
...
}

Run-background-commands-after-every-command-PartII-05

Run Background Commands after Every Command

For a complete introduction to this post, please read the first two paragraphs at PowerShell.org: http://powershell.org/wp/2015/10/12/run-background-commands-after-every-command.

There’s a part II, now. Be sure to read that when you’re done here: http://tommymaynard.com/quick-learn-run-background-commands-after-every-command-part-ii-2015.

You know the prompt, it often looks like this: PS C:\> or this: PS>, or even this [DC05]: PS C:\Users\tommymaynard\Documents>, when you’re interactively remoting to another computer. That built-in function determines the appearance of your prompt. I’ve seen several different modifications of the prompt. You can add the the date and time to the prompt, or like the example in the about_Prompt help file, you can add ‘Hello World’ every time the prompt is displayed. Doable, yes, but helpful, probably not.

This post isn’t about changing the prompt’s appearance, but instead about doing something else inside the prompt function. What the prompt looks like is defined by a function called, prompt — you guessed it. Take a look at your current prompt, as in the example below.

PS> (Get-ChildItem -Path Function:\prompt).ScriptBlock
"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
# .Link
# http://go.microsoft.com/fwlink/?LinkID=225750
# .ExternalHelp System.Management.Automation.dll-help.xml

This is the standard prompt function beginning in PowerShell 3.0 up though PowerShell 5.0, the current version of PowerShell, as of this writing.

This function is invoked each time a command is run and the prompt is displayed again. What if we added additional code inside this function? It would also be run every time a new prompt was displayed. My initial idea was to add the current directory to the WindowTitle. If you don’t know what the WindowTitle is, it’s the text at the top of the console host that says “Administrator: Windows PowerShell,” or “Windows PowerShell,” when in a non-elevated host.

I tried the current directory and quickly realized it took more time to look up at the WindowTitle, from the current prompt, than to leave the default prompt alone and get this information from there. The next idea was to add the date and time to the WindowTitle. This way I would know the time the last prompt was displayed. This is practically the time the last command ended. It seemed useful… for about a minute. I finally decided on putting an indicator in the WindowTitle so that I would know if there were any background jobs or not. I seem to open and close new consoles all day long, and knowing if I was about to dump a console with an active job, whether it was still running or not, or whether it still had data, seemed useful.

Let’s walk though what I did to get this to happen. Before we do that, let’s compare the two images below. The first one shows the standard WindowTitle when there’s no job, and the [Job] indicator when there is a job. We’re going to add a few lines of PowerShell to make this happen.

run-background-commands-after-every-command01run-background-commands-after-every-command02

The first thing I did was to define a function in my profile script ($PROFILE). This function would overwrite the default prompt function, as PowerShell reads in the profile script, after it has already created the default prompt.

Function prompt {

}

Next, I enter the default, prompt text. Notice I’m not changing how the prompt is displayed.

Function Prompt {
    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

Following this, I added the first piece of conditional logic. We test for a variable called $OriginalTitle. This variable will not exist the first time the prompt function is run, as the variable is created inside this If statement. It effectively ends up holding whatever was in WindowTitle when the console host was first opened. You’ll soon see how we reuse this value.

Before we go on, I should mention that I’ve made this a globally-scoped variable. This is because the variable needs to retain its value outside the function, and it needs to exist each time the function ends. Because of the way scope works, when we enter the function the second time and it can’t find $OriginalText, the function will go up a level and check for the existence of the variable in the parent scope, which in this case is the global scope.

Function Prompt {
    If (-Not($OriginalTitle)) {
        $Global:OriginalTitle = $Host.UI.RawUI.WindowTitle
    }

    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

In the next part of the function, I added a check to see if the Get-Job cmdlet returns anything. If it does, there are current background jobs, and if it doesn’t, then there are no current background jobs. We’ll start with what happens when there aren’t any jobs, first. In the Else portion of this If-Else statement, we set the current WindowTitle to whatever is stored in the $OriginalTitle variable. This ensures that the WindowTitle looks just like it did when we initially started the console host and there were no background jobs.

Function Prompt {
    If (-Not($OriginalTitle)) {
        $Global:OriginalTitle = $Host.UI.RawUI.WindowTitle
    }

    If (Get-Job) {
        # Coming
    } Else {
        $Host.UI.RawUI.WindowTitle = $OriginalTitle
    }

    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

So what happens when there are jobs? In this final portion of the function, we have an embedded If statement. If the current WindowTitle doesn’t already include the string [Job], then we add it. If it’s already there, then we leave it alone, and write the prompt.

Function Prompt {
    If (-Not($OriginalTitle)) {
        $Global:OriginalTitle = $Host.UI.RawUI.WindowTitle
    }

    If (Get-Job) {
        If ($Host.UI.RawUI.WindowTitle -NotLike '[Job]*') {
            $Host.UI.RawUI.WindowTitle = "[Job] $OriginalTitle"
        }
    } Else {
        $Host.UI.RawUI.WindowTitle = $OriginalTitle
    }

    "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
}

Thanks to the prompt function, we have another way to do something the moment the console is opened, and now, every time a new command is entered and a new prompt is displayed in the host. This little project has already got me wondering about some other things I may want to try with this function. I might expand what I’ve done so far, and provide indicators when there’s more than one job ([Job] vs. [Jobs]), if the jobs are still running, and perhaps if they have more data (whether it hasn’t been received yet, or was kept when it was received).

Keep in mind that this function runs after every command, successful or otherwise. Do your best not to overload the actions in the function. The default prompt comes in under a millisecond. My prompt, with the code above added, comes in at an average of 1 millisecond. If you end up doing too much in the function, you might end up waiting longer than you’re used to, making your additions to the function questionable.

Have fun, and thanks to everyone for reading this post.

Get a Locally Declared Function into a Remote Session

A part of why I write, is to have a place to store things I’m probably going to forget the moment I need them. Notice the search feature to the right. I’d be surprised to find out that someone has used it more than me.

That brings us to today’s topic: getting a locally declared function into a remote session. I wrote a recent post about getting variables into a remote sessions — you can read that here: http://tommymaynard.com/quick-learn-getting-local-variables-into-remote-sessions-2015 — and thought that getting a local function into a remote session, would make for a good followup.

First, let’s start with a function. This is a mildly complex function to use for an example, but by using it, I’ll be ensuring I can find it later, too. This function is a wrapper for the command: Dism /online /Get-Features. It takes the text produced by that command and returns the results as objects. I wrote this as part of an answer to a thread on the Microsoft Technet Forums: https://social.technet.microsoft.com/Forums/scriptcenter/en-US/972d87a9-9930-4f64-8592-5406f5fab8f4/dism-online-getfeatures-with-where-clause?forum=ITCG#01aeebe3-01b5-49cd-a8dd-1b1ce0352c8b.

Function Get-DismFeatures {
    [CmdletBinding()]
    Param ()

    Begin {
        $Results = Dism /online /Get-Features
        $Results = $Results[8..($Results.Count - 3)] | Where-Object {$_ -ne ''}

        $Feature = $Results | Select-String 'Feature'
        $State = $Results | Select-String 'State'
        $ResultsCounter = $Results.Count/2
    } # End Begin.

    Process {
        for ($i = 0; $i -lt $ResultsCounter; $i++) { 
            [pscustomobject]@{
                Feature = $Feature[$i].ToString().Split(':')[-1].Trim()
                State = $State[$i].ToString().Split(':')[-1].Trim()
            }
        }
    } # End Process.

    End {
    } # End End.
} # End Function.

Again, we could’ve use a much simpler function. Once a function is declared, we run the actions inside the function by invoking it — entering its name into the console. To run this locally, we’d just enter Get-DismFeatures, press Enter, and it would produce the results below. I’ve greatly shortened the results to save space.

PS C:\> Get-DismFeatures

Feature                                                     State
-------                                                     -----
Microsoft-Hyper-V-All                                       Enabled
Microsoft-Hyper-V-Tools-All                                 Enabled
Microsoft-Hyper-V                                           Enabled
Microsoft-Hyper-V-Management-Clients                        Enabled
Microsoft-Hyper-V-Management-PowerShell                     Enabled
Printing-Foundation-Features                                Enabled
Printing-Foundation-LPRPortMonitor                          Disabled
Printing-Foundation-LPDPrintService                         Disabled
...

To run this on a remote computer, or computers, we would need to ensure PS Remoting is available and working. You can test this with your own computer, or with a remote computer. Yes, you can use your local computer to determine if PS Remoting is working from, and into, your computer. Here’s three different examples of using my own computer, and one example of using a remote one.

PS> Invoke-Command -Computer localhost -ScriptBlock {$env:COMPUTERNAME}
TOMMYSCPU
PS> Invoke-Command -Computer . -ScriptBlock {$env:COMPUTERNAME}
TOMMYSCPU
PS> Invoke-Command -ScriptBlock {$env:COMPUTERNAME}
TOMMYSCPU
PS> Invoke-Command -Computer DC05 -ScriptBlock {$env:COMPUTERNAME}
DC05

Okay, so on to taking the function to a remote computer. Here’s how we do that (also with shorten results). Notice the placement of the dollar sign ($). This is different that we saw when taking locally declared variables to the remote session.

PS> Invoke-Command -ComputerName DC05 -ScriptBlock ${function:Get-DismFeatures}

Feature        : NetFx4ServerFeatures
State          : Enabled
PSComputerName : DC05
RunspaceId     : 72dc5b41-31b8-22b1-b5b1-29eae648123a

Feature        : NetFx4
State          : Enabled
PSComputerName : DC05
RunspaceId     : 72dc5b41-31b8-22b1-b5b1-29eae648123a
...

And, that’s it.

If you’re like me, you’re sitting back now and wondering, “Why not include the ability to run the function against remote computers, inside the function?” That’s an option, and often employed to make a function useful on both local and remote systems. Here’s the thing: You may not always write the tools you use, so having a way to use one that was written without a way to run it remotely, might be helpful one day.

Compare Two Arrays for Differences

I realized how obsessed I am with much I appreciate Windows PowerShell today, as I was working with Exchange 2013. I needed to find the differences in access rights between a PublishingEditor and Editor. The image below, pulled from a Microsoft web page, lists the different access rights.

Compare Two Arrays for the Differences01

One way to determine the difference would be to sit and stare at the screen. Nah. It didn’t take my eyes long, darting back and forth between lines, to desire a better way. Another way would be to take a screen capture and mark off the common entries. The image below shows what that looks like. Seriously, though, I can do better. Let’s do this in PowerShell and give ourselves a little practice. More on that in a moment.

Compare Two Arrays for the Differences02

I copied and pasted the first line I wanted to my PowerShell console.

PS> PublishingEditor   CreateItems, ReadItems, CreateSubfolders, FolderVisible, EditOwnedItems, EditAllItems, DeleteOwnedItems, DeleteAllItems

Then, I bounced around the entry to ensure that $PublishingEditor became a variable, by adding a dollar sign. I also added the =  assignment operator and put single quotes around the entire group of access rights, but this wasn’t quite enough.

PS> $PublishingEditor = 'CreateItems, ReadItems, CreateSubfolders, FolderVisible, EditOwnedItems, EditAllItems, DeleteOwnedItems, DeleteAllItems'

The next thing I did was add a couple methods — one right after the other, at the end of the command. The first method I added was .Replace(). In the version I added, .Replace(‘ ‘,”), it replaces all single spaces with nothing, effectively removing the spaces. The next method I added was .Split(). This, when added as .Split(‘,’), splits the string at each comma.

PS> $PublishingEditor = 'CreateItems, ReadItems, CreateSubfolders, FolderVisible, EditOwnedItems, EditAllItems, DeleteOwnedItems, DeleteAllItems'.Replace(' ','').Split(',')

Next, I copied the second line, for Editor access rights, pasted it in the console and cleaned it up, too.

PS> $Editor = 'CreateItems, ReadItems, FolderVisible, EditOwnedItems, EditAllItems, DeleteOwnedItems, DeleteAllItems'.Replace(' ','').Split(',')

At this point, I had two variables, $PublishingEditor and $Editor. I needed a way to compare the two variables to determine which one had additional access rights (presumably PublishingEditor) and what those were.

The first thing I did was echo the variables’ values to my screen. The second, was to run a comparison of the two. This was done using the Compare-Object cmdlet, where I provided one variable, $PublishingEditor, as the value for the -ReferenceObject parameter, and the other variable, $Editor, as the value for the -DifferenceObject parameter.

PS> $PublishingEditor
CreateItems
ReadItems
CreateSubfolders
FolderVisible
EditOwnedItems
EditAllItems
DeleteOwnedItems
DeleteAllItems
PS>
PS> $Editor
CreateItems
ReadItems
FolderVisible
EditOwnedItems
EditAllItems
DeleteOwnedItems
DeleteAllItems
PS>
PS> Compare-Object -ReferenceObject $PublishingEditor -DifferenceObject $Editor

InputObject                                                 SideIndicator
-----------                                                 -------------
CreateSubfolders                                            <=

The results, above, indicate that $PublishingEditor (the InputObject) has an additional access right, called CreateSubfolders. As you can see, the default results of Compare-Object only show the differences. Had I used the -IncludeEqual parameter the results would have looked like the example below.

PS> Compare-Object -ReferenceObject $PublishingEditor -DifferenceObject $Editor -IncludeEqual

InputObject                                                 SideIndicator
-----------                                                 -------------
CreateItems                                                 ==
ReadItems                                                   ==
FolderVisible                                               ==
EditOwnedItems                                              ==
EditAllItems                                                ==
DeleteOwnedItems                                            ==
DeleteAllItems                                              ==
CreateSubfolders                                            <=

Thanks for reading. Now off to find some other way to use PowerShell to speed up my day.

Before I do that, though, I mentioned practice earlier in this post. To really get PowerShell, we need to take every opportunity to continue to learn, but also to practice what we already know. This was a perfect opportunity. Not only would I get the results I needed — the difference in access rights — but I also got a half of minute of working with the Compare-Object cmdlet and the .Replace() and .Split() methods, where I may not have otherwise.

The more I use PowerShell, the less I have to try and remember when I really need it.

Getting Local Variables into Remote Sessions

There are at least three ways to get your local variables into your remote sessions. I’ve written these in preferential order beginning with the best option. Don’t forget to read about_Remote_Variables (Get-Help -Name about_Remote_Variables) to learn more, and see more examples.

$Word1 = 'PowerShell'
$Word2 = 'on'
$Computer = 'Server1'

# Using Scope Modifier
# (1st Option -- works in PowerShell 3.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Write-Verbose -Message "$Using:Word1 $Using:Word2 $env:COMPUTERNAME (Using Scope Modifer)" -Verbose
}
VERBOSE: PowerShell on Server1 (Using Scope Modifier)


# Param
# (2nd Option -- works in PowerShell 2.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Param($Word1InParam,$Word2InParam)
    Write-Verbose -Message "$Word1InParam $Word2InParam $env:COMPUTERNAME (Param)" -Verbose
} -ArgumentList $Word1, $Word2
VERBOSE: PowerShell on Server1 (Param)


# Args
# (3rd Option -- works in PowerShell 2.0 and up)
Invoke-Command -ComputerName $Computer -ScriptBlock {
    Write-Verbose -Message "$($args[0]) $($args[1]) $env:COMPUTERNAME (Args)" -Verbose
} -ArgumentList $Word1, $Word2
VERBOSE: PowerShell on Server1 (Args)

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