The PoSH Student

April 25, 2011

Finding un-linked GPOs

Filed under: PowerShell — Tags: , , — Nathan @ 5:08 pm

At my new job, I needed to do some GPO cleanup. There are a LOT of them, and many are not even linked to anything. I need to remove them, but didn’t want to go through them one-by-one to do it. PowerShell to the rescue!

For the impatient, here is the quick script to show the un-linked GPOs. The explanation of how to get there is below.

Import-Module grouppolicy
$allGPOs = Get-GPO -All | sort DisplayName
ForEach ($gpo in $allGPOs) {
	$xml = [xml](Get-GPOReport $gpo.Id xml)
	If (!$xml.GPO.LinksTo) {

First off, load the Group Policy module with Import-Module grouppolicy, and find what commands are available with Get-Command -Module grouppolicy:

CommandType     Name                                                Definition
-----------     ----                                                ----------
Cmdlet          Backup-GPO                                          Backup-GPO -Guid  -Path  [-Comment...
Cmdlet          Copy-GPO                                            Copy-GPO -SourceGuid  -TargetName ...
Cmdlet          Get-GPInheritance                                   Get-GPInheritance [-Target]  [-Domain <S...
Cmdlet          Get-GPO                                             Get-GPO [-Guid]  [[-Domain] ] [[-S...
Cmdlet          Get-GPOReport                                       Get-GPOReport [-Guid]  [-ReportType] <Repo...
Cmdlet          Get-GPPermissions                                   Get-GPPermissions -Guid  [-TargetName <Str...
Cmdlet          Get-GPPrefRegistryValue                             Get-GPPrefRegistryValue -Guid  -Context <G...
Cmdlet          Get-GPRegistryValue                                 Get-GPRegistryValue -Guid  -Key  [...
Cmdlet          Get-GPResultantSetOfPolicy                          Get-GPResultantSetOfPolicy [-Computer ] ...
Cmdlet          Get-GPStarterGPO                                    Get-GPStarterGPO -Guid  [-Domain ]...
Cmdlet          Import-GPO                                          Import-GPO -BackupId  -Path  [-Tar...
Cmdlet          New-GPLink                                          New-GPLink -Guid  -Target  [-LinkE...
Cmdlet          New-GPO                                             New-GPO [-Name]  [-Comment ] [-D...
Cmdlet          New-GPStarterGPO                                    New-GPStarterGPO [-Name]  [-Comment <Str...
Cmdlet          Remove-GPLink                                       Remove-GPLink -Guid  -Target  [-Do...
Cmdlet          Remove-GPO                                          Remove-GPO -Guid  [-Domain ] [-Ser...
Cmdlet          Remove-GPPrefRegistryValue                          Remove-GPPrefRegistryValue [[-Server] ] ...
Cmdlet          Remove-GPRegistryValue                              Remove-GPRegistryValue [-Guid]  [-Key] <St...
Cmdlet          Rename-GPO                                          Rename-GPO -Guid  -TargetName  [-D...
Cmdlet          Restore-GPO                                         Restore-GPO -BackupId  -Path  [-Do...
Cmdlet          Set-GPInheritance                                   Set-GPInheritance [-Target]  -IsBlocked ...
Cmdlet          Set-GPLink                                          Set-GPLink -Guid  -Target  [-LinkE...
Cmdlet          Set-GPPermissions                                   Set-GPPermissions -Guid  -PermissionLevel ...
Cmdlet          Set-GPPrefRegistryValue                             Set-GPPrefRegistryValue -Guid  -Context <G...
Cmdlet          Set-GPRegistryValue                                 Set-GPRegistryValue -Guid  -Key  [...

So, I apparently can create a new Link, or set or remove one, but not simply get one. Nice. And Get-GPO doesn’t include links either:

DisplayName      : test
DomainName       :
Owner            : MYDOMAIN\Domain Admins
Id               : 1be29c7a-6cdb-48f8-aaef-18db7ab79b25
GpoStatus        : AllSettingsEnabled
Description      :
CreationTime     : 9/16/2004 11:10:04 AM
ModificationTime : 1/25/2011 5:39:44 PM
UserVersion      : AD Version: 6, SysVol Version: 6
ComputerVersion  : AD Version: 2, SysVol Version: 2
WmiFilter        :

So I need more detail. Get-GPOReport has lots of detail, but it’s in HTML or XML format. Since PowerShell can read XML, I went that way:

PS> $test = [xml](Get-GPOReport test xml)
PS> $test

xml                                                         GPO
---                                                         ---
version="1.0" encoding="utf-16"                             GPO

PS> $test.GPO

xsi                 :
xsd                 :
xmlns               :
Identifier          : Identifier
Name                : test
IncludeComments     : true
CreatedTime         : 2004-09-16T15:10:04
ModifiedTime        : 2011-01-25T21:39:45
ReadTime            : 2011-04-25T14:43:30.3150573Z
SecurityDescriptor  : SecurityDescriptor FilterDataAvailable : true
Computer            : Computer
User                : User
LinksTo             : LinksTo

PS> $test.GPO.LinksTo

SOMName                       SOMPath                 Enabled                       NoOverride
-------                       -------                 -------                       ----------
TestLab              true                          false

So, all I need to do is find the GPOs where the XML doesn’t have a GPO.LinksTo property, or said property is blank.

Import-Module grouppolicy
$allGPOs = Get-GPO -All | sort DisplayName
ForEach ($gpo in $allGPOs) {
	$xml = [xml](Get-GPOReport $gpo.Id xml)
	If (!$xml.GPO.LinksTo) {


Edit 4/25/2011 3:22 PM:
I realized I can also search for the GPOs which are linked, but have no settings at all:

$allGPOs = Get-GPO -All | sort DisplayName
ForEach ($gpo in $allGPOs) {
    $xml = [xml](Get-GPOReport $gpo.Id xml)
    If ($xml.GPO.LinksTo) {
        If (!$xml.GPO.Computer.ExtensionData -and !$xml.GPO.User.ExtensionData) {

October 12, 2010

How to hide a password in a script

Filed under: PowerShell — Tags: , , , , , — Nathan @ 9:14 pm

Disclaimer: this is not secure. If you want secure, you need to encode the password as a secure string in a supporting file. This is for when you have to have the password in the script, but you want to hide it in plain sight.

The trick: obfuscation.

First off, go to, put your password in the TEXT field, and get the HEX values. If your password is “Password”, the HEX will be 50 61 73 73 77 6f 72 64

Next, generate a very long string of random characters to use. I use my Generate-Password function to make a 48 character password of every printable character (with the Consolas font, anyway) in the standard ASCII set:

$charSet = [string]::Join(&amp;quot;&amp;quot;, (((33..126)+(161..255)) | %{[char]$_}))
Generate-Password -Length 48 -Chars $charSet

This leaves me with something like Ý×ôg’ÐÚ®ÉFS)ÉAÓ<ɨc¦TÊiÝبü*T&½Â{íÏÄüïN¾m!bhm0¦

Next, we need to combine these together. In your random string, find a place where there are no lower-case hexadecimal characters for the length of your password +1. In this case, it’s the string ®ÉFS)ÉA. It can be anywhere in your string; it just has to be clean. After the first character of the selected substring, enter your first Hex character pair (50 in this case.) Skip a character, enter the next one, and so on. When you are done, you should have a combined string like Ý×ôg’ÐÚ50®61É73F73S77)6fÉ72A64Ó<ɨc¦TÊiÝبü*T&½Â{íÏÄüïN¾m!bhm0¦

Note: I picked 48 characters because my 8 char password produces 16 hex chars, making a nice 64-digit block of seemingly-meaningless text. The whole point is to add as many layers of obfuscation as possible; even someone counting characters could be led nicely off-track for a while.

Now that you have this nice ridiculous string, assign it to a variable with a misdirecting name:

$Temp2 = &amp;quot;Ý×ôg'ÐÚ50®61É73F73S77)6fÉ72A64Ó&amp;lt;ɨc¦TÊiÝبü*T&amp;amp;½Â{íÏÄüïN¾m!bhm0¦&amp;quot;

It helps to also do the same for the username called $temp3, and maybe one or two that won’t be used.

Now that you have a nicely encoded, obfuscated password, you just need the script to be able to decode it. Run:

($Temp2 -creplace &amp;quot;[^0-9a-f]&amp;quot;,&amp;quot;#&amp;quot;).Split(&amp;quot;#&amp;quot;)

Note the use of -creplace instead of -replace; case-sensitive regex really matters here. You should get a whole bunch of blank lines, with perhaps a few single Hex characters, and somewhere in the middle, your Hex string doubles. What we did was find all the characters that did not match 0..9 or a..f and replace them with a character, then split the string into an array on that character. I picked the Hash because it has meaning in PoSH, but we are not using that meaning in this context, thus providing further obfuscation.

Now count your way to the first pair of your set (remember to start at 0) and the last pair. In my example case, it’s 8 and 15. Now, add a few lines to the end of your previous string:

($Temp2 -creplace &amp;quot;[^0-9a-f]&amp;quot;,&amp;quot;#&amp;quot;).Split(&amp;quot;#&amp;quot;)[8..15]

If you did it right, you just got your original Hex string, only as an array instead of a string separated by spaces. We’re just narrowing to only those members of the array.

We’re almost there. To get the password back, we need to convert each of these hex pairs into an ASCII character, and then change this array of ASCII characters into a single string.

[string]::join(&amp;quot;&amp;quot;, (($Temp2 -creplace &amp;quot;[^0-9a-f]&amp;quot;,&amp;quot;#&amp;quot;).Split(&amp;quot;#&amp;quot;)[8..15] | %{[convert]::ToInt32($_, 16)} | %{[char]$_}))

The [convert] portion of the pipeline converts the Hex (aka Base16) characters to Int32. The [char] portion converts the integers to their ASCII equivalents. And the whole pipeline is contained in parentheses within the [string]::Join command, which joins the array as a string. The output is the password we originally encoded.

Can this be decoded? Yes. Easily? No. Few have the knowledge, nor the patience to track backwards through this morass to figure out what the password is. When paired with an account with permissions only to the very narrowest possible resources needed to run the script, this will work as a “secure enough, good enough” sort of free solution.


Filed under: PowerShell — Tags: , , , , , , , — Nathan @ 8:12 pm

I generate random passwords a lot. Anytime I need a service account, or when creating new user accounts, I need a random password to use. But I don’t want it to be too random, lest I confuse 1, l, and I because of some font variant somewhere. The natural result: a function!

function Generate-Password {
	Param (
		[int]$Length = 8,
		[string]$Chars = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789",

	If ($Help) {
		Write-Host -f yellow @"
  Generate random password of a specific type
  Generate-Password [[-Length] <int>] [-Chars <string>]
  Defaults to 8 characters long. Specify [-Length] characters if needed.
  Defaults to all upper & lowercase letters and numbers, except confusable ones like
    0,O,1,l, etc. Specify your own [-Chars] as needed.
  Generate-Password 25 "§©ª«À¿¾æñ¶¤¨«®±µð÷"
  Make a 25 character password of untypeable characters.

	$random = New-Object random
	$result = ""
	for ($i=0; $i -lt $length; $i++) {
		$result += $chars[$random.Next(0,$chars.Length)]

It’s actually a very simple thing, but I use it almost daily, and sometimes for unexpected things. Need a large string to play with for testing? Generate a 1024 character password, or 128 eight-character ones. Want a huge password of untypeable characters? Here’s one for every character from [CHAR]0 to [CHAR]255 that prints using the Consolas font:

Generate-Password -Length 50 -Chars [string]::Join("", (((33..126)+(161..255)) | %{[char]$_}))

I keep this in a Functions.ps1 file that can be called by all my scripts, so it is available any time I need randomized characters.

August 23, 2010

Why I don’t try for one-liners

Filed under: PowerShell — Tags: , , , , , , — Nathan @ 9:41 pm

All I wanted to do was report on all the ActiveSync devices that have connected to our Exchange server in the last month:

Get-CASMailbox -ResultSize Unlimited -Filter {HasActiveSyncDevicePartnership -eq $true -and -not DisplayName -like "CAS_{*"} | Get-Mailbox | %{Get-ActiveSyncDeviceStatistics -Mailbox $_} | ?{$_.LastSuccessSync -ge (get-date).AddDays(-30)} | Select @{Name="FirstSyncTime";e={$_.FirstSyncTime.AddHours(-4)}}, @{Name="LastPolicyUpdateTime";e={$_.LastPolicyUpdateTime.AddHours(-4)}}, @{Name="LastSyncAttemptTime";e={$_.LastSyncAttemptTime.AddHours(-4)}}, @{Name="LastSuccessSync";e={$_.LastSuccessSync.AddHours(-4)}}, DeviceType, DeviceID, DeviceUserAgent, DeviceWipeSentTime, DeviceWipeRequestTime, DeviceWipeAckTime, LastPingHeartbeat, DeviceModel, DeviceIMEI, DeviceFriendlyName, DeviceOS, DeviceOSLanguage, DevicePhoneNumber, Identity, @{Name="User";e={$_.Identity.SmtpAddress}}, @{Name="OU";e={(Get-User $_.Identity.SmtpAddress).Identity.Parent.Name}} | Sort DeviceID,LastSyncAttemptTime | Export-Csv 'C:\ActiveSyncDevices.csv' -NoTypeInformation

Yes, this really will work to get a report. But this is a perfect example of why I don’t try for one-liners. Let’s break it down by doing this the least one-liner-ish that is reasonable:

$CasMailboxes = Get-CASMailbox -ResultSize Unlimited -Filter {HasActiveSyncDevicePartnership -eq $true -and -not DisplayName -like "CAS_{*"}

Since Get-Mailbox does not return a HasActiveSyncDevicePartnership attribute, I had to use Get-CASMailbox. This retrieves all of them that have the attribute. Total run time for my 18,000 CAS mailboxes: 2:09.

$Mailboxes = $CasMailboxes | Get-Mailbox

In the next step, I’ll be running Get-ActiveSyncDeviceStatistics, but that command requires a Mailbox ID Parameter, not a CasMailbox ID Parameter. So I have to get the mailboxes for each CasMailbox I obtained earlier. Total run time: 3:37.

$ASDevices = $Mailboxes | %{Get-ActiveSyncDeviceStatistics -Mailbox $_}

Finally getting around to getting the devices. This gets all of the ActiveSyncDeviceStatistics for each mailbox. It gives me some handy stats for each one, and handily each object represents a device so I now have the devices themselves. An interesting note: even though the documentation states that Get-ActiveSyncDeviceStatistics will accept pipeline input on the -Mailbox parameter, it doesn’t. Thus I have to do a % (aka ForEach-Object) loop to make it work. Total run time: 15:23.

$RecentDevices = $ASDevices | ?{$_.LastSuccessSync -ge (get-date).AddDays(-30)}

I only want the ones that have connected in the last 30 days, so I use
? (aka Where-Object) to filter it. Total run time: 0:04.

$Devices = $RecentDevices | select @{Name="FirstSyncTime";e={$_.FirstSyncTime.AddHours(-4)}}, @{Name="LastPolicyUpdateTime";e={$_.LastPolicyUpdateTime.AddHours(-4)}}, @{Name="LastSyncAttemptTime";e={$_.LastSyncAttemptTime.AddHours(-4)}}, @{Name="LastSuccessSync";e={$_.LastSuccessSync.AddHours(-4)}}, DeviceType, DeviceID, DeviceUserAgent, DeviceWipeSentTime, DeviceWipeRequestTime, DeviceWipeAckTime, LastPingHeartbeat, DeviceModel, DeviceIMEI, DeviceFriendlyName, DeviceOS, DeviceOSLanguage, DevicePhoneNumber, Identity, @{Name="User";e={$_.Identity.SmtpAddress}}, @{Name="OU";e={(Get-User $_.Identity.SmtpAddress).Identity.Parent.Name}}

Now we’re starting to get convoluted. Now that I had all the devices, I wanted to filter the properties I had for each. Seven of the properties are time-properties, but I needed them in local time, not GMT. So for each of them, I created a custom column with the same original name, but with “-4” hours added. I simply included the other columns/properties as-is, except I left out the RecoveryPassword password.

Then I added two more columns. The first is User, which is really just the first part of Identity before the backslash. I included it because I need to check for uniqueness; it can be helpful to know who is using both an iPad and an iPhone. I also included OU, so that I can get accurate counts of how many devices are used by faculty, staff, and students. (I keep them in separate OUs, even though many people/accounts actually fit multiple constituency groups.) Total run time: 0:06.

$SortedDevices = $Devices | Sort-Object DeviceID,LastSyncAttemptTime

I want the data to start sorted by the DeviceID, but I also included LastSyncAttemptTime in case the same device has been used by multiple accounts. (Turns out there was an iPad tested by 5 people in 1 department in 3 days.) Total run time: 27 milliseconds.

$SortedDevices | Export-Csv 'C:\ActiveSyncDevices.csv' -NoTypeInformation

Finally, I export it to a CSV. No big deal here; I expect to open it in Excel and start filtering from there. Total run time: 126 ms.

In the end, the total processing time is about 21:13.

This is a great of example of when it’s not a good idea to chain all your commands into one-liners. Should you want to change something about the commands later down the chain (for example, get only ones from the last 7 days, or don’t include DeviceWipe* properties) you don’t want to run the first several commands all over again, and twiddle your thumbs for 15 minutes. When I really ran this, I did part of it (filtering by recent, custom columns, sorting, export) as one pipeline, but it’s worth thinking ahead before onelining too many things.

August 4, 2010

Auto-cleaning IIS and other logs

Filed under: PowerShell — Tags: , , , , , , , — Nathan @ 9:46 pm

I wanted a way to automate the cleaning out of IIS log files, so I wrote a simple script to do it. But rather than keep it too simple, I modularized it so that it can be reused for any sort of log file in any location and delete files of any age. I also added a switch that allows me to apply NTFS compression to the files instead of delete them if I desire. (I normally don’t use that, but need it on occasion. I’ve also had 2 system crashes when scripting the compression before I added the pause, so I now use it very sparingly!)

Param (
	[string]$Path = "C:\WINDOWS\system32\LogFiles\W3SVC1",
	[string]$Extension = "log",

If ($Help) {
	Write-Host -f yellow @"
  Deletes or compresses old log files by date last written.
  Clean-Logs.ps1 [[-Path] ] [-Months|-Weeks|-Days ] [-Extension ] [-Compress]
  Deletes files older than specified time in specified path.
  Defaults to the default IIS log file path.
  Defaults to *.log files.
  Defaults to 3 months back.
  Can optionally compress the files instead, but this is a risky maneuver known to occasionally crash Windows.
  Clean-Logs.ps1 -Path "C:\Documents and Settings\All Users\Application Data\avg8\scanlogs" -Weeks 6

if ($Months) {
	$EndDate = (Get-Date).AddMonths(-$Months)
elseif ($Weeks) {
	$EndDate = (Get-Date).AddDays(-7 * $Weeks)
elseif ($Days) {
	$EndDate = (Get-Date).AddDays(-$Days)
else {
	$EndDate = (Get-Date).AddMonths(-3)

$files = Get-ChildItem -Path $Path -Filter "*.$Extension" | Where-Object { $_.LastWriteTime -le $EndDate }

if (! $Compress -and $files) {
	$files | ForEach-Object { $_.Delete() }

if ($Compress -and $files) {
	foreach ($file in $files) {
		$wmiFile = Get-WmiObject cim_datafile -Filter "Name='$($file.FullName.Replace('\','\\'))'"
		If (! $wmiFile.Compressed) {
			$wmiFile.Compress() | Out-Null
			Start-Sleep 1 #Seems to need to pause or the system crashes.😦

I add this to the scheduled tasks on a server, set the parameters to match my needs on that server, run it once a month, and forget it.

This script also includes something I’ve started adding to every script and function I write, the parameter [switch]$Help. Perhaps you don’t have this problem, but I tend to forget the switches and whatnots of a script/function when I have not used it in months. But this way, rather than needing to look into the script to remember what the switches are and what they do and what the defaults are, I run Verb-Noun -Help and get a quick reminder.

February 26, 2010

Formatting in PowerShell

Filed under: PowerShell — Nathan @ 10:33 pm

I’ve used most all of these formats when building custom strings, but I always seem to be digging all over other people’s posts to get to them. Here I brought together all the ones I expect I’ll ever use.

There are a few other ones out there as well.

Standard numeric strings
Format Name Use Output Notes
C | c Currency "{0:c}" -f 12345.6789
"{0:c}" -f -12345.6789
D | d Decimal "{0:d}" -f 12345
"{0:d}" -f -12345
Must be an integer
Gn | gn General "{0:g6}" -f 12345.6789
"{0:g16}" -f 12345.6789
"{0:g}" -f -12345.6789
Display up to n digits.
Nn | nn Number "{0:n6}" -f 12345.6789
"{0:n}" -f 12345.6789
"{0:n3}" -f -12345.6789
To exactly n decimal places.
Defaults to 2 places.
Use Fn for no separator mark.
Pn| pn Percent "{0:p}" -f 0.12345
"{0:p0}" -f -0.12345
-12.35 %
-12 %
Defaults to 2 decimal places.
Xn | xn Hexadecimal "{0:x}" -f 123
"{0:x1}" -f -2
"{0:x4}" -f 123
Uses minimum needed digits unless specified.
Negatives work backwards from ffffffff.

numeric strings
0 Zero
"{0:00.0}" -f 5.6789 05.7 Zero or the appropriate digit as needed.
. Decimal
"{0:00.0}" -f 5.6789 05.7
# Digit
"{0:###.##}" -f 5.6789
"{0:(###) ###-####}" -f 5175551212
(517) 555-1212
Appropriate digit if available, or blank.
% Percent
"{0:.##%}" -f 1.23456
"{0:##%}" -f 1.23456

Date & Time strings
$date = Get-Date
D | d Date
long & short
"{0:D}" -f $date
"{0:d}" -f $date
Saturday, February 06, 2010
T | t Time
long & short
"{0:T}" -f $date
"{0:t}" -f $date
9:08:01 AM
9:08 AM
F | f Full
long & short
"{0:F}" -f $date
"{0:f}" -f $date
Saturday, February 06, 2010 9:08:01 AM
Saturday, February 06, 2010 9:08 AM
G | g General
long & short
"{0:G}" -f $date
"{0:g}" -f $date
2/6/2010 9:08:01 AM
2/6/2010 9:08 AM
R | r RFC1123 "{0:r}" -f $date Sat, 06 Feb 2010 09:08:01 GMT Always in GMT format.
s Sortable "{0:s}" -f $date 2010-02-06T09:08:01 Lower case "s" only.
M | m Month/Day "{0:m}" -f $date February 06
Y | y Year/Month "{0:y}" -f $date February, 2010

Date & Time strings
$date = Get-Date
y | yy | yyyy Year "{0:y}" -f $date
"{0:yy}" -f $date
"{0:yyy}" -f $date
"y" does not work standalone.
M | MM Month "{0:M}" -f $date
"{0:MM}" -f $date
"M" does not work standalone.
MMM | MMMM Month name
d | dd Day "{0:d}" -f $date
"{0:dd}" -f $date
"d" does not work standalone.
ddd | dddd Day name "{0:ddd}" -f $date
"{0:dddd}" -f $date
h | hh | H | HH Hour "{0:h}" -f $date
"{0:h}" -f $date
"{0:H}" -f $date
"{0:HH}" -f $date
Only "HH" works standalone.
m | mm Minute "{0:m}" -f $date
"{0:mm}" -f $date
"m" does not work standalone.
s | ss Second "{0:s}" -f $date
"{0:ss}" -f $date
"s" does not work standalone.
t | tt AM/PM "{0:t}" -f $date
"{0:tt}" -f $date
"t" does not work standalone.
: | / Time & Date
"{0:ddd yy/M/d}" -f $date
"{0:dddd yyyy/MM/dd}" -f $date
"{0:h:mm:ss tt}" -f $date
"{0:HH:m:s}" -f $date
"{0:MMM d, yyyy}" -f $date
Sat 10/2/6
Saturday 2010/02/06
1:08:01 PM
Feb 6, 2010
Other string tricks
  Alignment &
forced lenght
">{0,10}<" -f "test"
">{0,-10}<" -f "test"
>      test<
>test      <

July 30, 2009

Get-NewVariables -or- View Only Non-System Variables

Filed under: PowerShell — Tags: , , — Nathan @ 2:18 pm

Sometimes, I run Get-Variable to search for a certain variable and am confronted with:

Name                           Value
----                           -----
Error                          {System.Management.Automation.IncompleteParseException: Missing closing '}' in statem...
DebugPreference                SilentlyContinue
PROFILE                        C:\Documents and Settings\nrandall\My Documents\WindowsPowerShell\Microsoft.PowerShel...
HOME                           F:\
Host                           System.Management.Automation.Internal.Host.InternalHost
MaximumHistoryCount            64
MaximumAliasCount              4096
input                          System.Array+SZArrayEnumerator
StackTrace                        at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String commandN...
ReportErrorShowSource          1
ExecutionContext               System.Management.Automation.EngineIntrinsics
true                           True
VerbosePreference              SilentlyContinue
ShellId                        Microsoft.PowerShell
false                          False
MaximumFunctionCount           4096
line                           get-v
ReportErrorShowStackTrace      0
FormatEnumerationLimit         4
?                              True
PSHOME                         C:\WINDOWS\system32\WindowsPowerShell\v1.0
lfFolderID                     AAMkAGYzN2JlMjg3LWJhZDAtNDhiMC05MTBjLTk4MTI3ODZjNDQ1ZQAuAAAAAAAyytDMTMDeT4xZYpkktVmKA...
lastWord                       get-v
MyInvocation                   System.Management.Automation.InvocationInfo
PWD                            F:\
^                              (
ReportErrorShowExceptionClass  0
ProgressPreference             Continue
ErrorActionPreference          Continue
args                           {}
MaximumErrorCount              256
NestedPromptLevel              0
WhatIfPreference               0
OutputEncoding                 System.Text.ASCIIEncoding
ReportErrorShowInnerException  0
ErrorView                      NormalView
WarningPreference              Continue
PID                            5576
ConfirmPreference              High
MaximumDriveCount              4096
MaximumVariableCount           4096
$                              fl

Not very helpful, usually. Sometimes I may want to remember that built-in variables like FormatEnumerationLimit or MaximumDriveCount exist, but typically I am more concerned with the variables I’ve created. And not only are there about 45 built-in variables to filter through, PoSH is not nice enough to offer anything resembling automatic sorting.

So I made my own way of dealing with it. In my Profile.ps1 file, I’ve added a few lines:

$coreVars = Get-Variable | %{$_.Name}
$coreVars += "coreVars","_"
function Get-NewVariables {
	Get-Variable | ?{$coreVars -notcontains $_.Name} | sort Name

When I open PowerShell, it automatically gets the system variables, and stores their names in array $coreVars. It also stores that variable’s own name into the array (plus $_, which normally won’t be created till later.) From then on, I can use the function Get-NewVariables to get all variables created since I opened PowerShell. And because I like them sorted, I sorted it by the name.

And if I ever want to remove all the variables I have created since I started a session, there’s a one-liner for that:

Get-NewVariables | select name | Remove-Variable

The one downside: no ability to use the switches you normally can with Get-Variable.

June 26, 2009

Mailbox Database Sizes

Filed under: PowerShell — Nathan @ 5:09 pm

Sometimes it’s helpful to know just how much data I have in each mailbox database; both in terms of number of mailboxes, and in terms of the total size of those mailboxes. The Get-MailboxDatabase command has no detail concerning the database size. Get-MailboxStatistics is great, so long as all the mailboxes are on Exchange 2007; ours are currently split between 2007 and 2003. Get-MailboxFolderStatistics works on 2007 and 2003 mailboxes, and the root folder (which, conveniently, is always array item [0]) will tell you the FolderAndSubfolderSize. Combine these together with some basic data from Get-Mailbox, and you can cobble together a massive one-liner that will tell you how many mailboxes are in each database, and the sum of all the mailbox sizes. I split the lines with the ` character to improve readability.

Get-Mailbox -ResultSize Unlimited |`
    Select-Object Database,@{Name="Size";Expression={
    	(Get-MailboxFolderStatistics $_)[0].FolderAndSubfolderSize.ToBytes()
    	}} |`
    Group-Object -Property Database |`
    Select-Object Name,Count,@{
    	Name="SizeGB";Expression={[math]::Round(($_.Group | Measure-Object Size -sum).Sum / 1gb,2)
    	}} |`
    Sort-Object SizeGB -Desc | Format-Table -AutoSize

First, a warning: If you have 15,000 mailboxes spread across 3 servers, this will take a LONG time. It took more than an hour for me.

So, let’s break this down:

Get-Mailbox -ResultSize Unlimited |`
This should be obvious. I’m getting every mailbox in my exchange organization. Since I have more than 1000, I have to use a larger (or unlimited) result size.

Select-Object Database,@{Name="Size";Expression={(Get-MailboxFolderStatistics $_)[0].FolderAndSubfolderSize.ToBytes()}}
The only thing I need from Get-Mailbox is the database, and I also want the size. So I filter out all other properties with Select-Object. The trickier part starts with the @{ symbols. Essentially, I’m opening a one-item Hashtable (aka Associative Array) to create a custom property to select. (There’s an article here on what commands need what formats; just remember that you always need an Expression, Format-* commands need a Label, and non Format-* commands need a Name. ) I use the easy propertyname Size. For the value, I run Get-MailboxFolderStatistics on the mailbox, and on the first folder item ([0]) I get convert the FolderAndSubfolderSize (which is a Microsoft.Exchange.Data.ByteQuantifiedSize data type) to bytes. If I were to stop at this point, I would have a massive table of DatabaseName and Size for each mailbox.

Group-Object -Property Database |`
This is where the real magic happens. Powershell sorts all these into groups based on the Database. The result is one group object per database. Each object has properties Count (number of objects in the group), Name (in this case, the name of each database), and Group (an array of the objects in the group.)

Select-Object Name,Count,@{n="SizeGB";e={[math]::Round(($_.Group | Measure-Object Size -sum).Sum / 1gb,2)}} |`
Since I want a total, not an array, I need to do one more bit of math on this. So I select the Name and Count again, and then pull a few tricks. I use ($_.Group | Measure-Object Size -sum).Sum / 1gb to “measure” the Size property of each object in the Group array, and get the Sum of all those properties. The Measure-Object command is a little strange in it’s output, so I had to then select the Sum property of it’s output. (Try running 1..10 | Measure-Object -sum to see why.) And since I didn’t want it in Bytes, I divided by GB. And since I didn’t want (up to) 15 decimal points, I used a .NET math function to round it to 2 decimal points. (Try [math]::Round(15.2561428,1) and similar for an understanding of how that works.)

Sort-Object SizeGB -Desc | Format-Table -AutoSize
These last 2 commands are very optional. I wanted to sort it by the size of the database, but you could sort by any property or none at all. And it’s already in table format, but I didn’t like the way it spread across the whole console, so I autosized it down. But that’s personal preference.

One other thought: this can be written much smaller. In fact, I originally wrote it using aliases, but then I expanded it out to post. Here’s the smaller version.

Get-Mailbox -Res Unlimited |`
    Select Database,@{N="Size";E={(Get-MailboxFolderStatistics $_)[0].FolderAndSubfolderSize.ToBytes()}} |`
    Group -P Database |`
    Select Name,Count,@{N="SizeGB";E={[math]::Round(($_.Group | Measure-Object Size -sum).Sum / 1gb,2)}} |`
    Sort SizeGB -Desc | FT -A

June 23, 2009

Bad ASCII characters

I was reading a database table, and trying to sort and display certain data. The table was the printers that are mapped by ScriptLogic Desktop Authority. All was going well, until I got to the point of displaying the rules. Some are simple, others are multiple:

  1. /S=BCREEK means Site is Battle Creek
  2. /W=2t1922 means Workstation name is 2t1922
  3. _/S=TOLEDO means Site is NOT Toledo
  4. /W=2t1921▼▼/W=3t1656▼▼/W=4ibm4070▼▼/W=7ibm5011▼▼/W=7ibm4865 means any of these workstations.

But… what’s with the upside-down triangles?

And often I will split these sorts of things up into an array, but I get nothing from:


Hmm. So what it’s outputting is obviously not the same character it’s showing me. But what is it? What I needed was to get the ASCII code for that character.

PS C:\> $dataset.Tables[0].rows[0].Rules
CO=\- SAU COMP\CAMPUS\Art Lab\*▼▼/G=ARBOR\gaink1
PS C:\> $temp = $dataset.Tables[0].rows[0].Rules.SubString(30,5)
PS C:\> $temp.ToCharArray()
PS C:\> $temp.ToCharArray() | %{ [int]$_ }

Going back through this: I displayed the rule itself, then set a variable to the 21st through 25th characters (*▼▼/G), then displayed them as an array (for future comparison) then, for each for them, displayed the ASCII code by converting the letter to an [int]. I can then check the codes here. Sure enough, “*” is character 42, “/” is 47, and “G” is 71. Thus, I can look and know that character 31 is “(unit separator)”. No wonder it didn’t display!

So what to DO with it?

PS C:\> $dataset.Tables[0].rows[0].Rules.Replace([char]31,"^").Replace("^^","^").Split("^")

Conveniently, the Replace() function on strings allows you to cheat a little. So I replaced every ▼ with a ^ (you can use any char you want), replaced every double ^ with a single, then split it on that character.

Edit: If you are curious how some of the other characters will display, you can easily show them all with one of a few methods:

0..127 | %{ [char]$_ }
0..255 | %{ [char]$_ }
0..127 | %{ "{0:0##} : $([char]$_)" -f $_ }
0..255 | %{ write-host -n ("{0:0##} : $([char]$_)   " -f $_) }

These lines will, in the following order:

  1. Display the first 128 ASCII characters (the ‘standard’ set), one per line.
  2. Display the first 256 ASCII characters (‘standard’ and ‘extended’.)
  3. Display each character, preceded by its character number and a colon, one per line.
  4. Display #3, but one one (wrapping) line. It is forced to use 3 digit numbers with leading zeros.

One warning on all of these: [char]007 is “Bell”. That means your computer’s internal speaker will beep. If you really want to mess with someone, include that character on every iteration of a large loop…

June 19, 2009

Get Subnets of Sites

Filed under: PowerShell — Tags: , , , — Nathan @ 2:42 pm

In trying to troubleshoot an issue, I needed to determine what subnets we were actually using for our 15 sites. Under Active Directory Sites and Services I can see all the subnets under the subnet folder, but I wanted a way to play with the way the data is displayed, and be able to copy and paste it. Turns out it’s easy under PoSH, so long as you know where to look.

$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$forest.Sites | ft Name,Subnets -Auto

Of course, that’s not quite what I wanted; the formatting was wrong, and it cut off some of the subnets at our largest site. I was also curious what other properties there were:

PS C:\>$forest.Sites[11]
Name                           : TOLEDO
Domains                        : {domain.tld}
Subnets                        : {}
Servers                        : {TOL-SRV1.toledo.domain.tld}
AdjacentSites                  : {NOC}
SiteLinks                      : {NOC-TOL}
InterSiteTopologyGenerator     : TOL-SRV1.toledo.domain.tld
Options                        : None
Location                       :
BridgeheadServers              : {TOL-SRV1.toledo.domain.tld}
PreferredSmtpBridgeheadServers : {}
PreferredRpcBridgeheadServers  : {}
IntraSiteReplicationSchedule   : System.DirectoryServices.ActiveDirectory.ActiveDirectorySchedule

For the time being, all I really want are Name and Subnets, but it’s useful to know the other info is available. As to the subnets, the {} characters around the data tells me that it’s a multi-valued field, so I can extract the individual subnets. I can either use:

$sites = $forest.Sites
$sites | %{$_.Subnets}

or, if I don’t want to continue to play with $sites:

PS C:\> $forest.Sites | %{$_.Subnets}

Name                                    Site                                    Location
----                                    ----                                    --------                           NOC                              NOC                           NOC                           NOC                           NOC                           NOC                           NOC                           NOC                         JACKSON                                 Jackson                         BCREEK                                  Battle Creek                         DETROIT                                 Detroit                         FLINT                                   Flint                         GAYLORD                                 Gaylord                         GRAPIDS                                 Grand Rapids                         KZOO                                    Kalamazoo                         LANSING                                 Lansing                         PETOSKEY                                Petoskey                         TOLEDO                                  Toledo                         TRAVERSE                                Traverse City                         TROY                                    Troy                         Sandbox

And from there, I can see the subnets and where they belong. I could even sort, filter, or otherwise manipulate them. In the end, I found the problem, too. My subnet was actually; I changed it after discovering the problem, but before writing this, and thus corrected the catch-all subnet to, well, catch all.

Older Posts »

Create a free website or blog at


Get every new post delivered to your Inbox.