Tuesday, March 27, 2018

Get weird time zones in PowerShell

There are weird time zones where the offset is not a round number of hours. If you are wondering where they are, we can find out with a single PowerShell line.

Windows has to work anywhere. So Microsoft keeps it up to date with all of the various time zones in the world. A “time zone” is not just the offset from UTC; it also includes the local rules for daylight saving time. As of this writing, there are 136 different time zone definitions recognized by Windows.

Windows stores these settings in the registry, but we can most easily get them using the .GetSystemTimeZones() static method of the [System.TimeZoneInfo] object.

[System.TimeZoneInfo]::GetSystemTimeZones()

This results in an array of TimeZoneInfo objects.

The TimeZoneInfo property .BaseUtcOffset holds the information we are looking for today. We only want those time zones with an odd offset. .BaseUtcOffset is a [TimeSpan] object. TimeSpan objects have a property called .TotalMinutes. We want those time zones whose .BaseUtcOffset is not a multiple of 60.

We can test an offset by using the % modulus operator to get the remainder of a division. (For math geeks, the % operator returns a remainder, not a modulus. For computer geeks, you can go back to thinking these are the same thing.)

When converting an integer to a [Boolean] $True or $False, zero converts to $False, and everything else converts to $True.

We combine these facts to create a filter that gives us only the time zones with unusual offsets.

[System.TimeZoneInfo]::GetSystemTimeZones() |
    Where-Object { $_.BaseUtcOffset.TotalMinutes % 60 }

And then we select specific properties to make the list more readable.

[System.TimeZoneInfo]::GetSystemTimeZones() |
    Where-Object { $_.BaseUtcOffset.TotalMinutes % 60 } |
    Select-Object -Property IdBaseUtcOffset

See also
It’s always 5 o-clock somewhere: Using .Net and PowerShell’s Extended Type System to find out where

Saturday, March 3, 2018

Leveraging dynamic type conversion in PowerShell - Part 1 - Walkthrough of a crazy example

Dynamic type conversion can be leveraged to create more efficient, intuitive PowerShell code, scripts that are easier to write, easier to read, and easier to maintain. You are already using it without realizing it. Better understanding dynamic type conversion and some of the tricks related to it means you can use it intentionally to improve your code.

Dynamic type conversion means we can use the wrong thing in an expression, and instead of throwing an error like any other language would do, PowerShell will automatically convert it into the right thing on the fly. PowerShell has over 16,000 lines of source code it uses to figure out how to perform a given conversion.

Let’s start by analyzing a crazy example that makes liberal use of dynamic type conversion tricks to make code that is almost impossible to write and read.

The journey begins in crazy town

Thomas Rayner, the Working Sysadmin, wrote this brilliant piece of obfuscated code.

$__="$(@{})";$__[-((!@()+!@())*(!@()+!@())*(!@()+!@())+(!@()+!!@()))]+$__[-(!@()+!!@())]+$__[-(!@()+!@())]+$__[-(!@()+!@())]+$__[(!@()+!@())*(!@()+!@())*(!@()+!@())]

Without running it, it is almost impossible to figure out what that does. If you do run it, you can see the end result, but probably can’t see how it does it.

Let’s figure it out.

First, it appears to be two commands, separated by a semicolon. Let’s split them onto separate lines.

$__="$(@{})"
$__[-((!@()+!@())*(!@()+!@())*(!@()+!@())+(!@()+!!@()))]+$__[-(!@()+!!@())]+$__[-(!@()+!@())]+$__[-(!@()+!@())]+$__[(!@()+!@())*(!@()+!@())*(!@()+!@())]

The dollar sign and double underscore is just an odd variable name.

Let’s change $__ to $X to make it easier to read.

$X="$(@{})"
$X[-((!@()+!@())*(!@()+!@())*(!@()+!@())+(!@()+!!@()))]+$X[-(!@()+!!@())]+$X[-(!@()+!@())]+$X[-(!@()+!@())]+$X[(!@()+!@())*(!@()+!@())*(!@()+!@())]

It’s a bit more apparent now that the first line assigns a value to $X, and the second line adds several $X things together.

Let’s add white space to make that even more apparent.

$X = "$(@{})"
$X[-((!@()+!@())*(!@()+!@())*(!@()+!@())+(!@()+!!@()))] + $X[-(!@()+!!@())] + $X[-(!@()+!@())] + $X[-(!@()+!@())] + $X[(!@()+!@())*(!@()+!@())*(!@()+!@())]

We can take that a step further. Rather than have it run off the side of the screen, we can break it into multiple lines. In PowerShell we can add a new line after anything that syntactically must be followed by another thing. So we can add a new line after each plus sign.

$X = "$(@{})"
$X[-((!@()+!@())*(!@()+!@())*(!@()+!@())+(!@()+!!@()))] +
    $X[-(!@()+!!@())] +
    $X[-(!@()+!@())] +
    $X[-(!@()+!@())] +
    $X[(!@()+!@())*(!@()+!@())*(!@()+!@())]

Now it’s easier to see each $X thing. It looks like each $X thing has some adding and multiplying going on. Let’s add more white space to see that better.

$X = "$(@{})"
$X[-( ( !@() + !@() ) * ( !@() + !@() ) * ( !@() + !@() ) + ( !@() + !!@() ) )] +
    $X[-!@() + !!@() )] +
    $X[-!@() + !@() )] +
    $X[-!@() + !@() )] +
    $X[!@() + !@() ) * ( !@() + !@() ) * ( !@() + !@() )]

An exclamation point is the equivalent of -not. Programmers from other languages are used the exclamation point in this context, but in PowerShell we usually use -not, because it is easier to intuitively read and understand.

So replace all of the ! with -not.

$X = "$(@{})"
$X[-( ( -not @() + -not @() ) * ( -not @() + -not @() ) * ( -not @() + -not @() ) + ( -not @() + -not -not @() ) )] +
    $X[--not @() + -not -not @() )] +
    $X[--not @() + -not @() )] +
    $X[--not @() + -not @() )] +
    $X[-not @() + -not @() ) * ( -not @() + -not @() ) * ( -not @() + -not @() )]

So far we haven’t changed anything. We just made it easier to read. Now we can begin to parse it.
Here’s the first dynamic type conversion. The -not operator can only take a [Boolean] $True or $False as input. If we give it a different type of input, PowerShell converts the input to a [Boolean] before applying the -not.

For arrays, a non-empty array converts to $True, and an empty array converts to $False. So let’s replace all of the empty arrays with $False.

$X = "$(@{})"
$X[-( ( -not $False + -not $False ) * ( -not $False + -not $False ) * ( -not $False + -not $False ) + ( -not $False + -not -not $False ) )] +
    $X[--not $False + -not -not $False )] +
    $X[--not $False + -not $False )] +
    $X[--not $False + -not $False )] +
    $X[-not $False + -not $False ) * ( -not $False + -not $False ) * ( -not $False + -not $False )]

-Not $False, of course, converts to $True. Let’s make that replacement.

$X = "$(@{})"
$X[-( ( $True + $True ) * ( $True + $True ) * ( $True + $True ) + ( $True + -not $True ) )] +
    $X[-$True + -not $True )] +
    $X[-$True + $True )] +
    $X[-$True + $True )] +
    $X[$True + $True ) * ( $True + $True ) * ( $True + $True )]

There were two -not -not $False, which are now -not $True. We can replace those with $False.

$X = "$(@{})"
$X[-( ( $True + $True ) * ( $True + $True ) * ( $True + $True ) + ( $True + $False ) )] +
    $X[-$True + $False )] +
    $X[-$True + $True )] +
    $X[-$True + $True )] +
    $X[$True + $True ) * ( $True + $True ) * ( $True + $True )]

Here is the second dynamic type conversion. When we add two things that don’t otherwise make sense to add, PowerShell will convert them to numbers, if it can.

For Boolean values, $True converts to 1, and $False converts to 0. Let’s do that conversion.

$X = "$(@{})"
$X[-( ( 1 + 1 ) * ( 1 + 1 ) * ( 1 + 1 ) + ( 1 + 0 ) )] +
    $X[-1 + 0 )] +
    $X[-1 + 1 )] +
    $X[-1 + 1 )] +
    $X[1 + 1 ) * ( 1 + 1 ) * ( 1 + 1 )]

( 1 + 1 ) is 2, and ( 1 + 0 ) is 1. (It’s true. I looked it up.)

$X = "$(@{})"
$X[-2 * 2 * 2 + 1 )] +
    $X[-1] +
    $X[-2] +
    $X[-2] +
    $X[2 * 2 * 2]

A little more math, more advanced this time, gives us this.

$X = "$(@{})"
$X[-9] +
    $X[-1] +
    $X[-2] +
    $X[-2] +
    $X[8]

That’s now short enough that we can put the second expression all back on one line.

$X = "$(@{})"
$X[-9] + $X[-1] + $X[-2] + $X[-2] + $X[8]

Let’s turn our attention to what $X is.

This is the third dynamic type conversion.

At first glance, it’s just a string with random symbols. But those are double quotes.

This is an expandable string. The dollar sign and parenthesis denote a subexpression. The subexpression is just @{}. An empty hashtable. So the result of the subexpression is not a string, but PowerShell knows it’s supposed to be, so it automatically converts it.

Conversions to string are accomplished using the .ToString() method that exists on each .Net object. Some object types, like [datetime], have very useful implementations of .ToString(). Other object types, like [hashtable], have (normally) completely useless implementations of .ToString(). They convert to a string of the fully qualified name of the type of thing they are, which in this case is “System.Collections.Hashtable”.

$X = "System.Collections.Hashtable"
$X[-9] + $X[-1] + $X[-2] + $X[-2] + $X[8]

So $X is just a string. And the second line indexes into the string. Positive numbers refer to position from the start of the string, counting from 0. Negative numbers refer to position from the end of the string, counting from -1.

The second line is five characters being added together.

This is the fourth dynamic type conversion.

When you index into a string, PowerShell gives you a [char] object. But [char] objects can’t be added. When you add them anyway, PowerShell converts them to [string] objects, which can be added or concatenated.

So let’s index into $X to get the [char] objects and then convert them to strings.

'H' + 'e' + 'l' + 'l' + 'o'

Final result

And then concatenate them together to get the final result.

'Hello'

A long way to go for that, but the journey itself is often the reward.

A different path

Instead of going all the way to the end, we can go back a few steps, and take a different path to find a more concise way of expressing the original code we started with.

$X = "System.Collections.Hashtable"
$X[-9] + $X[-1] + $X[-2] + $X[-2] + $X[8]

Another way to join strings is by using the -join operator. If we put it before an array of strings, it concatenates them together. If we put it before an array of [char] objects, PowerShell converts them to [string] objects and concatenates them together.

$X = "System.Collections.Hashtable"
-join $X[-9], $X[-1], $X[-2], $X[-2], $X[8]

We can replace the array of indexed $X references with a single $X reference with an array of indexes. (Yes, I know that sentence wasn’t helpful. Just look at the next step and you’ll see what I meant.)

$X = "System.Collections.Hashtable"
-join $X[-9, -1, -2, -2, 8]

And lastly we put the value of $X in its place, to arrive at a version of the original code that is somewhat easier to understand than the original code.

-join "System.Collections.Hashtable"[-9, -1, -2, -2, 8]

The journey continues

So there you have it. An example that used four types of dynamic type conversion to do something completely useless.

But really cool.

In the next article or two in the series, I’ll give lots of more useful places dynamic type conversion can be used. These will include uses that are so familiar and mundane that you never realized there was magic happening behind the scenes, and well as a variety of tips and tricks for using it to improve your code.

Wednesday, February 14, 2018

Group objects evenly by size in PowerShell

A PowerShell function to group objects evenly by size, using a simple packing algorithm. But with advanced pipeline techniques for handing nested arrays and leveraging the PowerShell parser to protect against code injection. Just for fun.

The initial challenge

Brett Miller and Bob Frankly recently posed the hypothetical question on the PowerShell Slack channel, how can you easily split a collection of objects into 4 groups, based on the size of each object, such that the total size of each group of objects is approximately the same?

I threw together a quick handful of lines of PowerShell code to meet the requirements. And then I started to work on a generic function to solve the problem generally.

And that’s when it got interesting.

The added complexity

I wanted the function to be able to take input either through a parameter or from a pipeline. Techniques for handling that are well known. But as the input could be nested arrays, the standard techniques would not work for both input methods. I had to find out how to determine when the function is part of a pipeline.

I wanted the user not only to be able to specify a property to use to determine an object’s size, but also to be able to specify a nested property. For example, if I have an Active Directory $Group, the size of the group is in $Group.Members.Count. But if I let the user give me a string I am going to execute, I needed a way to confirm the string contains nothing but nested property names, and not a code injection.

I wanted the user to also be able to group objects by simple object count. I realized that this would be default behavior of the functions when it is used with non-collection objects simply by making the default -Property value “Count”, with no extra coding needed.

The code

Let’s define the function.

function Group-Evenly
    {

A good comment-based help block is good. I do this for functions that I’m sharing, or that I’m likely to reuse in other scripts, or when I want to thoroughly document it for myself or the poor soul that has to maintain my code after I’ve moved on.

     <#
    .SYNOPSIS
        Evenly divides input objects into a given number of groups
        optionally weighted by the value of a given property.

    .DESCRIPTION
        Creates specified number of groups (arrays)
        Input object are sorted by value of the specified Property, descending
            (If no property is specified, .Count is used)
        Each object is placed in the group with the smallest totale value of the specified Property

        This algorithm may not always produce an optimal result, but does
        produce a reasonable result quickly compared to the brute force
        required to guarantee an optimal result.

    .OUTPUT
        [array[]]

    .PARAMETER InputObject
        Objects to be grouped
        Accepts pipeline input
        Unlike most commands, accepts Null pipeline input

    .PARAMETER Property
        String - Property to use to determine object size for weighted grouping
        Accepts nested property names, e.g. - Members.Count
        Default to "Count"

    .PARAMETER Number
        Int32 - Number of groups to create
        Defaults to 2

    .EXAMPLE
        $Users = Get-ADUser -Filter *
        $Teams = Group-Evenly -InputObject $Users

        Results in two arrays, each with half of the users.

    .EXAMPLE
        $DataChunks = Get-ChildItem C:\Temp -File |
            Group-Evenly -Property Length -Number 4

        Results in four arrays of files, grouped such that the total file sizes
        of the groups are approximately equal.

    .EXAMPLE
        $Meetings = Get-ADGroup -Filter { Name -like "Dept*" } -Properties Members |
            Group-Evenly -Property Members.Count -Number 6

        Results in six arrays of AD department groups, grouped such that the total
        membership of the grouping are approximately equal

    .EXAMPLE
        $Whatever = Get-ChildItem C:\Temp -File |
            GroupEvenly -Property Directory.Parent.FullName.Length

        Results in two arrays of files, grouped evenly but weighted by the length
        of the full path of the parent of the file's directory. That is, of course,
        completely useless, but I didn't feel like taking the time to come up with
        a better example of using a deeply nested property value.

    .NOTES
        v 1.0 Tim Curwick Created
    #>

[cmdletbinding()] tells PowerShell to automatically do various advanced function things and better parameter handling than otherwise. In PowerShell 4.0 and up, [cmdletbinding()] is not needed if [parameter()] is used, but it doesn’t hurt to add it, and it’s good practice so I don’t forget it in those scripts where I do need it.

    [cmdletbinding()]
    Param (

Parameter $InputObject will hold the objects to group. It will be an array of any type of object that needs to be grouped. We want to be able to take objects from the pipeline.

We are not making it mandatory, because I like the behavior of returning empty groups instead of nothing if $InputObject is empty.

        [parameter( ValueFromPipeline = $True )]
        [array]$InputObject,

Parameter $Property is the string describing the path to the property or nested property to use to determine the size of the objects.

By defaulting to ‘Count’, arrays are automatically grouped according to the number of elements they have, and objects that are not collections are simply split into groups with equal numbers of objects.

        [string]$Property = 'Count',

Parameter $Number is the number of groups to divide the objects into.

        [int]$Number = 2 )

Because we want to act on pipeline objects, but not one at a time, we have to gather them up before starting to work on them. We’ll use a Begin block to create an array to hold the objects, a Process block to add objects to the array as they come in the pipeline or from the parameter, and then do all of the actual work in the End block.

    Begin
        {
        # Initialize array
        $RawItems = @()
        }

Typically, in a Process block such as this, we would simply add any incoming objects to the array.

But atypically here, the individual objects may themselves be arrays. PowerShell’s special handling of arrays requires us to do some special handling to get our desired behavior.

If we get an array from the -InputObject parameter, the element of the array are the objects we want to sort, and what we want to add to $RawItems.

But if we get an array from the pipeline, that means it was itself an element nested within a parent array. In that case, we don’t want to add each element of the array to $RawItems. We want to add the entire array as a single element in $RawItems.

To distinguish between the two, we need to be able to tell the when the function is running in a pipeline. Thanks to √ėyvind Kallstad for his blog Quick tip: Determine if input comes from the pipeline or not with the answer.

$PSCmdlet.MyInvocation.ExpectingInput is $True if we’re in a pipepline.

    Process
        {
        # If input is from pipeline
        # Treat an array as a single input item
        If ( $PSCmdlet.MyInvocation.ExpectingInput )
            {

If we are in a pipeline, we want the array to be added as a single element to $RawItems. To do that, we use a unary comma to indicate that it is an element. ,@($x) results in what this looks like: @( @( $x ) ). But @( @( $x ) ) results in @( $x ) by design, so we have to resort to the unary comma.

            $RawItems += ,$InputObject
            }

If we are not in a pipepline, we want to add all of the element of the $InputObject to $RawItems, which is the normal PowerShell behavior when “adding” two arrays.

        # Else (input is from paramter)
        # Treat an array as a collection of input items
        Else
            {
            $RawItems += $InputObject
            }
        }

Once we have collected all of the objects from the pipeline, the End block runs, and we can actually do some work.

    End
        {

First we create a string which, when executed, will get the size of the object based on the $Property string.

        ## Test for code injection

        # Build property string
        $SizeString = "`$_.$Property"

Then we need to check it to confirm that it really will do nothing other than reference a property or nested property. A scripter might take input from an untrusted source and use it to populate the -Property value, and end up with Group-Evenly -Property 'x;Remove-Item C:\*.* -Recurse' which would be a problem if we didn’t do this check.

To run this or any code, the PowerShell parser first needs to cut it up into identified tokens. We can leverage that capability, and ask PowerShell to do so now, and identify the tokens for us to parse.

        # Use PowerShell parser to tokensize the property string
        $TokenErrors = [System.Collections.ObjectModel.Collection[System.Management.Automation.PSParseError]]@()
        $Tokens = [System.Management.Automation.PSParser]::Tokenize( $SizeString, [ref]$TokenErrors )

If there were no errors during tokenizing, we set a validity flag to the $True and continue. If there were errors, the scriptblock is not going to run properly, and we set the flag to $False.

        # If there are errors, it won't work anyway; set to invalid
        $PropertyValid = $TokenErrors.Count -eq 0

Then we examine the tokens. The tokens would look like this if $Property = 'Members.Count'.

Content     Type Start Length StartLine StartColumn EndLine EndColumn
-------     ---- ----- ------ --------- ----------- ------- ---------
_       Variable     0      2         1           1       1         3
.       Operator     2      1         1           3       1         4
Members   Member     3      7         1           4       1        11
.       Operator    10      1         1          11       1        12
Count     Member    11      5         1          12       1        17
...      NewLine    16      2         1          17       2         1

Or like this if $Property = 'x;Remove-Item C:\*.* -Recurse '

Content                   Type Start Length StartLine StartColumn EndLine EndColumn
-------                   ---- ----- ------ --------- ----------- ------- ---------
_                     Variable     0      2         1           1       1         3
.                     Operator     2      1         1           3       1         4
x                       Member     3      1         1           4       1         5
;           StatementSeparator     4      1         1           5       1         6
Remove-Item            Command     5     11         1           6       1        17
C:\*.*         CommandArgument    17      6         1          18       1        24
-Recurse      CommandParameter    24      8         1          25       1        33
...                    NewLine    33      2         1          34       2         1

The $ in $_ is simply an indicator that a variable name follows, and is not included in any of the tokens.

In our script, the first token is always Type “Variable” and Content “_”, and the second token is always Type “Operator” and Content “.”, because we hardcoded $_. at beginning of the scriptblock. So we can ignore those.

In valid code (by our definition), all of the remaining tokens are of either Type “Operator”, “Member”, or “NewLine”. So if any of the tokens are of any other Type, we set the $PropertyValid flag to $False.

The only Operator token we need has the Content “.”. If any other Operators are present, we set the flag to $False.

            # If there are any tokens after the $_ other than .PropertyName.PropertyName.etc
            # (Bad -Property value (or code injection))
            # Set to invalid
            $Tokens[2..($Tokens.Count-1)].
                Where{
                    $_.Type -notin 'Operator', 'Member', 'NewLine' -or
                    ( $_.Type -eq 'Operator' -and $_.Content -ne '.' ) }.
                ForEach{ $PropertyValid = $False }

Otherwise, we are safe to proceed. (You might be concerned that a Member token can be a method rather than a property, but if it was, there would be associated GroupStart and GroupEnd tokens holding the parentheses following the method name, which are not allowed. If there were an extra NewLine token in there, it would have to be followed by something we aren’t allowing in order to be dangerous. Even if a NewLine were followed by a dot Operator that is intended as a call operator rather than as a member operator, the call operator would only be dangerous if it were followed by a String, Variable, or some other type of token that we are not allowing.)

        # If property string is valid
        # continue
        If ( $PropertyValid )
            {

We create an array of the correct number of arrays to hold groups that we will group the input objects into. The simplest way to do this is to create an array with a single empty array as an element, using the unary comma discussed earlier. Then we “multiply” the array by the number of groups we want, which in PowerShell means make X additional elements in the array that are copies of the existing element(s).

            # Initialize array with the desired number of groups
            $Groups = ,@() * $Number

We will want to quickly check the sizes of each group as we go along. Rather than re-measure the groups each time, we’ll store the running totals in an integer array. The index of a size in this array will match the index of the group in the group array that it measures.

Again, the simplest (or prettiest) way to do this is to create an array with a single zero, and then multiply it to get the correct number of zeros.

            # Initialize array to hold group sizes
            $Sizes  = @(0* $Number

We will be frequently referencing the last index of the arrays. Rather than repeating the calculation, we do it once and store it in a variable. It’s faster, and it makes the code prettier.

            # Get highest index number
            $TopIndex = $Number - 1

Then we convert the $SizeString to a [ScriptBlock] for later execution. We didn’t do it until we were sure it was valid so that the conversion doesn’t throw an error. (We’ll throw our own error later if it isn’t valid.) We’re doing it now instead of in the following code so that it is only done once. It’s faster and makes the code prettier.

            # Convert size string to a scriptblock
            $SizeBlock = [ScriptBlock]::Create( $SizeString )

To simplify handling of the objects and their sizes, we’re wrapping them in custom objects along with their sizes, and then sort them by size, biggest ones first.

            # Create an array with the items and their calculated sizes
            # Sort by size descending
            $Items = $RawItems |
                Select-Object -Property @(
                    @{ Label = 'Value'; Expression = { $_ } }
                    @{ Label = 'Size' ; Expression = $SizeBlock } ) |
                Sort-Object -Property Size -Descending

Then we simply go through the items, biggest ones first, and put each in whatever group has the most room.

            # For each item (starting with the largest)
            # Place item in smallest group
            ForEach ( $Item in $Items )
                {

For each possible group index, we sort them by the sizes of the group, and take the first one (the index of the smallest group).

                # Find the index of the smallest group
                $Smallest = 0..$TopIndex | Sort-Object -Property { $Sizes[$_] } | Select-Object -First 1

Add the item to the smallest group.

                # Add the item to the smallest group
                $Groups[$Smallest] += $Item.Value

Add the size of the item to the running total size of the smallest group.

                # Add the size of the item to the group size
                $Sizes[ $Smallest] += $Item.Size
                }

Repeat until all of the items are placed.

Return the results.

            # Return the results
            return $Groups
            }

If the $Property string is invalid, we throw an error. We use Write-Error instead of keyword Throw to make it a non-terminating error whose behavior is dictated by $ErrorActionPreference or by using the common parameter -ErrorAction on our function. (We don’t have to define -ErrorAction; [cmdetbinding()] took care of that for us.)

        # Else (invalid Property value)
        # Throw error (respecting ErrorAction)
        Else
            {
            Write-Error -Message "Invalid Property value."
            }
        }
    }


Usage

Now we can use our new function.

Using default parameters, and no pipeline, we can take all of our users and split them into two groups.

$Users = Get-ADUser -Filter *
$Teams = Group-Evenly -InputObject $Users

Or let’s say the file share where my user’s home drives are stored need to be split up onto four new drives. I don’t care which users go where, but I want the total spaced used on each share the be roughly equal.

$UserFolders = Get-ChildItem $SourceShare -Directory |
    ForEach-Object {
        [pscustomobject]@{
            FullName = $_.FullName
            FolderSize = Get-ChildItem $_.FullName -File -Recurse |
                Measure-Object -Property Length -Sum |
                Select-Object -ExpandProperty Sum } }

$NewShareGroups = $UserFolders |
    Group-Evenly -Property FolderSize -Number 4

HR needs to schedule 6 meetings to talk with all employees. They want all employees in a given department to go to the same meeting, and they want the meetings to be of roughly the same size. There are Active Directory groups that define who is in what department.

$Meetings = Get-ADGroup -Filter { Name -like "Dept*" } -Properties Members |
    Group-Evenly -Property Members.Count -Number 6

Just to test how well we handle nested properties, let’s group some files based on the length of the full name of the parent of the file’s directory. It’s kinda stupid, but I didn’t have the time to come up with a better example.

$Whatever = Get-ChildItem C:\Temp -File -Recurse |
    GroupEvenly -Property Directory.Parent.FullName.Length

Full function

function Group-Evenly
    {
    <#
    .SYNOPSIS
        Evenly divides input objects into a given number of groups
        optionally weighted by the value of a given property.

    .DESCRIPTION
        Creates specified number of groups (arrays)
        Input object are sorted by value of the specified Property, descending
            (If no property is specified, .Count is used)
        Each object is placed in the group with the smallest totale value of the specified Property

        This algorithm may not always produce an optimal result, but does
        produce a reasonable result quickly compared to the brute force
        required to guarantee an optimal result.

    .OUTPUT
        [array[]]

    .PARAMETER InputObject
        Objects to be grouped
        Accepts pipeline input
        Unlike most commands, accepts Null pipeline input

    .PARAMETER Property
        String - Property to use to determine object size for weighted grouping
        Accepts nested property names, e.g. - Members.Count
        Default to "Count"

    .PARAMETER Number
        Int32 - Number of groups to create
        Defaults to 2

    .EXAMPLE
        $Users = Get-ADUser -Filter *
        $Teams = Group-Evenly -InputObject $Users

        Results in two arrays, each with half of the users.

    .EXAMPLE
        $DataChunks = Get-ChildItem C:\Temp -File |
            Group-Evenly -Property Length -Number 4

        Results in four arrays of files, grouped such that the total file sizes
        of the groups are approximately equal.

    .EXAMPLE
        $Meetings = Get-ADGroup -Filter { Name -like "Dept*" } -Properties Members |
            Group-Evenly -Property Members.Count -Number 6

        Results in six arrays of AD department groups, grouped such that the total
        membership of the grouping are approximately equal

    .EXAMPLE
        $Whatever = Get-ChildItem C:\Temp -File |
            GroupEvenly -Property Directory.Parent.FullName.Length

        Results in two arrays of files, grouped evenly but weighted by the length
        of the full path of the parent of the file's directory. That is, of course,
        completely useless, but I didn't feel like taking the time to come up with
        a better example of using a deeply nested property value.

    .NOTES
        v 1.0 Tim Curwick Created
    #>

    [cmdletbinding()]
    Param (
        [parameter( ValueFromPipeline = $True )]
        [array]$InputObject,
        [string]$Property = 'Count',
        [int]$Number = 2 )

    Begin
        {
        # Initialize array
        $RawItems = @()
        }
    Process
        {
        # If input is from pipeline
        # Treat an array as a single input item
        If ( $PSCmdlet.MyInvocation.ExpectingInput )
            {
            $RawItems += ,$InputObject
            }

        # Else (input is from paramter)
        # Treat an array as a collection of input items
        Else
            {
            $RawItems += $InputObject
            }
        }
    End
        {
        ## Test for code injection

        # Build property string
        $SizeString = "`$_.$Property"

        # Use PowerShell parser to tokensize the property string
        $TokenErrors = [System.Collections.ObjectModel.Collection[System.Management.Automation.PSParseError]]@()
        $Tokens = [System.Management.Automation.PSParser]::Tokenize( $SizeString, [ref]$TokenErrors )

        # If there are errors, it won't work anyway; set to invalid
        $PropertyValid = $TokenErrors.Count -eq 0

        # If there are any tokens after the $_ other than .PropertyName.PropertyName.etc
        # (Bad -Property value (or code injection))
        # Set to invalid
        $Tokens[2..($Tokens.Count-1)].
            Where{
                $_.Type -notin 'Operator', 'Member', 'NewLine' -or
                ( $_.Type -eq 'Operator' -and $_.Content -ne '.' ) }.
            ForEach{ $PropertyValid = $False }
       
        # If property string is valid
        # continue
        If ( $PropertyValid )
            {
            # Initialize array with the desired number of groups
            $Groups = ,@() * $Number

            # Initialize array to hold group sizes
            $Sizes  = @(0* $Number

            # Get highest index number
            $TopIndex = $Number - 1

            # Convert size string to a scriptblock
            $SizeBlock = [ScriptBlock]::Create( $SizeString )

            # Create an array with the items and their calculated sizes
            # Sort by size descending
            $Items = $RawItems |
                Select-Object -Property @(
                    @{ Label = 'Value'; Expression = { $_ } }
                    @{ Label = 'Size' ; Expression = $SizeBlock } ) |
                Sort-Object -Property Size -Descending
   
            # For each item (starting with the largest)
            # Place item in smallest group
            ForEach ( $Item in $Items )
                {
                # Find the index of the smallest group
                $Smallest = 0..$TopIndex | Sort-Object -Property { $Sizes[$_] } | Select-Object -First 1

                # Add the item to the smallest group
                $Groups[$Smallest] += $Item.Value

                # Add the size of the item to the group size
                $Sizes[ $Smallest] += $Item.Size
                }
   
            # Return the results
            return $Groups
            }
   
        # Else (invalid Property value)
        # Throw error (respecting ErrorAction)
        Else
            {
            Write-Error -Message "Invalid Property value."
            }
        }
    }