Archive for the ‘Scripting’ Category

Getting Admin’s to adopt PowerShell

October 6, 2008

The RTM of PowerShell was released over 2 years ago. But still there are multiple occasions in which I find myself trying to convince someone that they should really start looking into using this incredible new tool but they end up fighting me pretty hard. There have been a couple consistent themes that I have detected in these conversations that I think are very valid points.

First, I would like to point out that my role at work is a System Engineer. I am not the Operations guy down in the trenches with Ninjas on fire chasing me everywhere I go. My role on the Infrastructure team is to figure out how to incorporate new technology into our existing infrastructure without breaking stuff. I also help troubleshoot issues that get escalated to our team. So suffice to say, I am not an Admin. But, here is some feedback I have gotten from our Ops team members.

The first is that Admins are never going to use something unless they need to. Necessity is the mother of invention, or innovation for that matter. Why would anyone switch from something that works to something that may work but they don’t know how to  use yet? I think the PowerShell team is working very hard to address this very issue, although I think sometimes we miss it in the background when we are introducing people to PowerShell. One of the beauties of PowerShell is that it is a solid consistent language that can be used across multiple technologies. Regardless of whether you are administering an Exchange server or Virtual Machine manager, you are going to likely be doing a “get-doohickey | set-Doohickey.” Also there is tremendous power in the consistency of naming parameters and arguments. All Cmdlets are verb-noun -parameter argument.

I think what the Admin is thinking is that PowerShell is just one more thing to learn. We are having a hard time telling them that the investment they make in learning PowerShell to manage Exchange will also help them with VMM, desktop management, AD,Group Policy, and Terminal Services, just to name a few.

The second issue is really a two-edged a sword. There is an awesome community supporting PowerShell and a ton of great content on the web that has been worked on over the years and is really quite mature. The problem here is that newcomers to PowerShell start seeing some of this stuff and get completely overwhelmed right away. If a complete beginner asks “How can I filter the services to see just the ones that are stopped?” and they find something like gsv | ? {$_.status -eq “Stopped”}” they will completely freak out. On the other hand, they may use it but not understand what is going on and just save it as another tool in their tool bag, without understanding that they can filter just as easily on processes or on Virtual Machines.

 

There is one thing that I believe has not been sold quite as much as necessary is Tab-Completion. Take the example below copied from a blog entry from the MS Exchange Team

This is the Exchange Management Shell (EMS) command Tom would enter to generate the cert request to be provided to the 3rd party CA in order to generate the actual certificate:

New-Exchangecertificate -domainname mail.contoso.com, contoso.com, contoso.local, autodiscover.contoso.com, server01.contoso.local, server01 -Friendlyname contosoinc -generaterequest:$true -keysize 1024 -path c:\certrequest.req -privatekeyexportable:$true –subjectname “c=US o=contoso inc, CN=server01.contoso.com”

We have found that the ‘–subjectname’ option is the most confusing. The help contents in EMS are vague as well. The best description is found in the TLS whitepaper mentioned at the beginning of this post so we’re not going to reproduce it here.

 

As an admin I would look at this and say something like, “Crap, who created this huge long command line entry. Editing this is going to suck.” What people don’t get is that they have Tab Completion available, not just for the Cmdlets but for parameters as well. This just doesn’t get conveyed when we are posting examples. Now if you throw in PowerTab or Intellisense from PowerShell Plus, its just gravy.

And so, I would like to pose the question to you, how do you as an engineer or an admin get your teammates to start using PowerShell. What walls have you come up against and what can we as a community to do to help break down these walls.

One thing I have been doing lately is using PowerShell in the build guides and Ops guide that I write, to at least get people started with using it. What else can we do?

PowerShell Function Set-IPAddress

August 13, 2008

I recently came across the need to configure IP information on a Network interface using a script. This is definitely something that can be accomplished with NETSH,  but I was looking for a way to do it with native PowerShell using Get-WmiObject. If there is a choice, I tend to prefer to stick with native Powershell Cmdlets and functions rather than running cmd based tools, but that is just my personal opinion and you can take it or leave it.

My goal was to be able to specify a Network Interface by name, ie “Local Area Connection” and pass it an IP address, mask, gateway, and 1 or 2 DNS servers. In order to accomplish this there were two WMI classes that I needed. The first is Win32_NetworkAdapter. The second is Win32_NetworkAdapterConfiguration. The real key (pun intended) was to figure out a way to join the information from these two classes so that I knew for sure that I was working with the same network interface. It turns out that both classes have a property called InterfaceIndex.

Win32_NetworkAdapter has a property called netconnectionid which is equal to the name of the Adapter, ie “Local Area Connection”.

With that information, here is a function that you can use to specify settings for a NIC based on the name of the NIC.

   1: function Set-IPAddress {
   2:         param(  [string]$networkinterface,
   3:                 [string]$ip,
   4:                 [string]$mask,
   5:                 [string]$gateway,
   6:                 [string]$dns1,
   7:                 [string]$dns2,
   8:                 [string]$registerDns = "TRUE"
   9:          )
  10:         
  11:                 
  12:         #Start writing code here
  13:         $dns = $dns1
  14:         if($dns2){$dns ="$dns1,$dns2"}
  15:         $index = (gwmi Win32_NetworkAdapter | where {$_.netconnectionid -eq $networkinterface}).InterfaceIndex
  16:         $NetInterface = Get-WmiObject Win32_NetworkAdapterConfiguration | where {$_.InterfaceIndex -eq $index}
  17:         $NetInterface.EnableStatic($ip, $mask)
  18:         $NetInterface.SetGateways($gateway)
  19:         $NetInterface.SetDNSServerSearchOrder($dns)
  20:         $NetInterface.SetDynamicDNSRegistration($registerDns)
  21:         
  22: }

Hope you find this useful.

Andy

Credentials in the Console

July 23, 2008

A while back on the Windows PowerShell Team blog, there was a post that describes how to force Get-Credential to prompt for a username and password in the console itself rather than popping up a Windows dialog box asking for a username and password.

The change is a key in the registry, and is permanent, unless of course you change it back to the original setting. The other minor complaint was that the there was no space between where the user needs to type a username and password, and the text prompting for the text.

   1: 119 >  $c = Get-Credential
   2:  
   3: cmdlet Get-Credential at command pipeline position 1
   4: Supply values for the following parameters:
   5: Credential
   6: PromptForCredential_UserAndy
   7: PromptForCredential_Password********
   8:  
   9: 120 >

You can see that the “PromptForCredential” and my username – “Andy” just run together. 

You can accomplish this another way with much more control over the user experience and you don’t have to hack the registry.

So here is my quick and dirty function. I put in a very basic check to see if someone added their domain or not, and added it if necessary.

   1: function Get-Cred {
   2:     Write-Host "";
   3:     $username = Read-Host "Enter username to access some resource (no domain required)"
   4:     if ($username -notlike "MYDOMAIN\*"){$username = "MYDOMAIN\$username"}
   5:         
   6:     Write-Host ""
   7:     $password = Read-Host  -AsSecureString "Password to access some resource"
   8:     
   9:     $credential = New-Object System.Management.Automation.PSCredential($username,$password)
  10:     return $credential
  11: }

And here it is in action:

   1: 130 >  $cred = Get-Cred
   2:  
   3: Enter username to access some resource (no domain required): andy
   4:  
   5: Password to access some resource: **************
   6: 131 >
   7: 131 >  $cred.GetNetworkCredential() | fl *
   8:  
   9:  
  10: UserName : andy
  11: Password : secretpassword
  12: Domain   : MYDOMAIN

Powershell, WPF, and the Science of Great User Experience

May 30, 2008

There has been a lot of talk in the last couple weeks about using WPF to build UI’s for Powershell. Just to name a few

Powershell Team’s Series on WPF

Joel Bennet’s Huddled Masses Series on WPF

I think this is great and I can’t wait to see what we all come up with.

As we begin to build these graphical tools on top of Powershell, UI and user experience is going to be more and more important.

There is a great presentation over on dnrTV (Dot Net Rocks TV) where Mark Miller and Carl Franklin discuss the Science of Great User Experience. There are some really good nuggets in here that even as Powershell Scripters tweaking WPF, we can think about and use.

Also, if you are developer and use Visual Studio, you really should check out Dev Express’s CodeRush. This is the company that Mark works for and they really take User Experience for Visual Studio to the next level. It’s awesome.

Confessions of a Powershell Evangelist

March 5, 2008

For the last year or so I have been trying to get people I work with excited about Powershell. Some people love it right away, but for others, it is a bit overwhelming. That being said, I had an interesting experience the other day. In order to set the context of this, I must confess something.  I have not been a big fan of PowerGui. My attitude was always something like this: “PowerShell is a SHELL, not a GUI, and the last thing in the world that I need is yet another GUI application.”

Don’t get me wrong here. It’s not that I thought PowerGui is a bad application, its just that I personally didn’t see the value in it for me and what I was doing with Powershell on a daily basis.

In spite of my point of view, I let my team know that PowerGui had RTM’d and forwarded a link to my co-workers.

Within about an hour I got an email from one of our Operations guys that included the following:

 

“Dude, this is fricking AWESOME, I installed it with the Active Directory Quest management pack and E2K7 Management tools (32bit) on a Windows 2003 R2 box. Super sweet!”

 

After reading that my attitude about PowerGui started to change 🙂 Another 4 or 5 hours go by and I get another email from the same person:

 

“WOW, I have being playing with some of the AD and E2K7 stuff, this tool kicks so much butt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!”

 

I would like end my confession with an apology to Dimitry and the rest of the PowerGui crew.  I just wish I had started telling people about it earlier. You guys have created an awesome tool and it has absolutely helped me in getting more people in my organization using Powershell. Thank you very much for your hard work and all that you give to the Powershell Community.

By the way, I have been a die hard fan of the Quest AD Cmdlets from the very beginning.

As we – the Powershell Community – work together to get people on board with Powershell, we must remember and realize that what works well for us as individuals may not work well for others and vice versa. We must be able to step into the shoes of people we are working with, understand their concerns, and help them solve their problems, (with Powershell of course.)

Creating an Array of PowerShell Custom Objects

March 4, 2008

During the Scripting Games I found myself creating a lot of custom objects with properties that I could use to sort , select, take averages of, and a number of other cool things. Getting results into a Powershell object can make life a lot easier for a number of reasons.

There was one little piece I was missing. Not only did I want to create a single object, quite often I would want to put all the objects I created into a collection of objects. Did you know you can add collections of like objects to each other ?

   1: PS 13 >  $a = get-process

   2: PS 14 >  $a.count

   3: 66

   4: PS 15 >  $b = get-process

   5: PS 16 >  $c = $a + $b

   6: PS 17 >  $c.count

   7: 131

   8: PS 18 >

The code above shows that I can add two collections of process objects together. Very cool.

So I tried doing this in the scripting games and came across a problem. For instance, in Event 3 we needed to tally up a bunch of votes. So what I really needed was to create a bunch of $vote objects and put them all together in a collection called $votes

Here’s what I came up with at first.

By the way, when I create PS Custom Objects I cheat and use the “” | Select-Object prop1, prop2 nomenclature. My easier than using new-object followed by a bunch of add-member commands.

   1: $votes = "" | Select-Object v1,v2,v3,v4

   2: foreach ($v in Get-Content votes.txt)

   3:     {    

   4:         $vote = "" | Select-Object v1,v2,v3,v4;

   5:         $vote.v1,$vote.v2,$vote.v3,$vote.v4 = $v.split(",")

   6:         $votes += $vote

   7:     } 

Looks nice and shiny until you run it 🙂 I get the following error:

Method invocation failed because [System.Management.Automation.PSObject] doesn’t contain a method named ‘op_Addition’.

Not so shiny

The trick is that we $votes needs to be a collection of $vote objects, not another object identical to $vote.

So we instantiate $votes with a cast to [array] and life is good.

   1: $votes = @()

   2: foreach ($v in Get-Content votes.txt)

   3:     {    

   4:         $vote = "" | Select-Object v1,v2,v3,v4;

   5:         $vote.v1,$vote.v2,$vote.v3,$vote.v4 = $v.split(",")

   6:         $votes = $votes + $vote

   7:     } 

A quick update, thanks to Aleksandar. We should instantiate $votes as $votes = @(). I have updated the example above.

Using System.Char Static Methods

March 4, 2008

Event 5 in the Scripting Games dealt with testing whether or not a password is secure or not.  Among many qualifications, the password had to contain at least one digit, at least one upper case character and at least one lower case character. There also needed to be a check to see if the password contained any non-alphanumeric characters, such as $%^ or (.

All these could be dealt with using regular expressions, but regex skills are weak at best, so I always default to check and see if if someone has done it for me already. Sure enough, there are a bunch of static methods for System.Char that we can use.

   1: [char]::IsUpper()
   2: [char]::IsLower()
   3: [char]::IsDigit()
   4: [char]::IsLetterOrDigit()

From these we can build a filter that will pass the string if it meets the criteria. For the sake of example, lets build “Select-ContainsUpper”

   1: # Create a filter that can be used in a pipeline
   2: filter Select-ContainsUpper {
   3:     
   4:     # coerce the string into an array of chars and then pass each one to IsUpper
   5:     # IsUpper returns $TRUE or $FALSE for each char
   6:     # So ContainsUpper now contains an array of a bunch of Booleans
   7:     $containsUpper = $_.toCharArray() | % {[char]::IsUpper($_)}
   8:     
   9:     # if any char was uppercase, some element in ContainsUpper will be $TRUE
  10:     if ($containsUpper -contains $TRUE) {$_}
  11:     }
  12:     
  13: "one","Two","5","ContainsUpperCase","all-lower-case" | Select-ContainsUpper

Building Select-ContainsDigit and the others is left as an exercise to the reader.

You could also quite easily build a function that takes the string as a password, but lately I have been big on using filters and then putting them all together in a nice pipeline for the sake of clarity and a nice overall PowerShelly look.

Scripting Games Event 6 Select-Prime

February 26, 2008

Event six was a classic programming math problem. Find all the prime numbers in a given range. For this particular problem, the range was between 1 and 200.

Here’s my answer

   1: filter select-prime 
   2: { 
   3:     if ($_ -eq 1) {return $null}
   4:     for ($i=2;$i -le ([int][Math]::Sqrt($_));$i++) 
   5:         { 
   6:         if ($_ % $i -eq 0 ) {return}
   7:         }
   8:     $_
   9: } 
  10: 1..200 | select-prime

It turns out that in order to find if a prime number we can use the modulus operator. This math operator simply returns the remainder when one number is divided by another

If a number n % x  = 0 where x is not 1 or n, then the number will not be prime.

   1: PS U:> 6 % 2
   2: 0
   3: PS U:> 5 % 3
   4: 2
   5: PS U:> 5 % 2.5
   6: 0
   7: PS U:> 9 % 2
   8: 1
   9: PS U:> 9 % 3
  10: 0

So this will work if we just went from 2 to N. But it turns out that is way more work than necessary. We only need to go up to the Square Root of N and we can call it prime.

I really thought I was being super cool and iterated up the square root of the number. Turns out the calls into .NET to calculate the square root were to costly to make my efficiency efficient.

   1: PS C:usersandysDesktop> cat C:usersandysDesktopprimes.ps1
   2: filter select-primeQuickly
   3: {
   4:         if ($_ -eq 1) {return $null}
   5:         for ($i=2;$i -le ([int][Math]::Sqrt($_));$i++)
   6:                 {
   7:                 if ($_ % $i -eq 0 ) {return}
   8:                 }
   9:         $_
  10: }
  11:  
  12: filter select-primeSlowly
  13: {
  14:         if ($_ -eq 1) {return $null}
  15:         for ($i=2;$i -le $_;$i++)
  16:                 {
  17:                 if ($_ % $i -eq 0 ) {return}
  18:                 }
  19:         $_
  20: }
  21:  
  22: "Quickly"
  23: ""
  24: measure-command {1..200 | select-primeQuickly}
  25: "Slowly"
  26: ""
  27: measure-command {1..200 | select-primeSlowly}
  28: PS C:usersandysDesktop> C:usersandysDesktopprimes.ps1
  29: Quickly
  30:  
  31: Days              : 0
  32: Hours             : 0
  33: Minutes           : 0
  34: Seconds           : 0
  35: Milliseconds      : 734
  36: Ticks             : 7346876
  37: TotalDays         : 8.5033287037037E-06
  38: TotalHours        : 0.000204079888888889
  39: TotalMinutes      : 0.0122447933333333
  40: TotalSeconds      : 0.7346876
  41: TotalMilliseconds : 734.6876
  42:  
  43: Slowly
  44:  
  45: Days              : 0
  46: Hours             : 0
  47: Minutes           : 0
  48: Seconds           : 0
  49: Milliseconds      : 346
  50: Ticks             : 3469459
  51: TotalDays         : 4.0155775462963E-06
  52: TotalHours        : 9.63738611111111E-05
  53: TotalMinutes      : 0.00578243166666667
  54: TotalSeconds      : 0.3469459
  55: TotalMilliseconds : 346.9459

Looking at the results, what I thought was going to be quick was actually a lot slower! The version that calls into .NET ran in 736 milliseconds and the version that did not ran in 346 milliseconds.

Goes to show that when you are programming or scripting, what we think may be a gain in efficiency, may very well not be, as I learned in this exercise.

Powershell Function Start-Proc

February 22, 2008

The gentlemen over at the Power Scripting Podcast recently posted a tip on how to start processes in Powershell.

I had run into the same problem they had. How do you pass in both an executable and its arguments to the Start method of a System.Diagnostics.Process object.

Something like this works fine in Powershell:

[System.Diagnostics.Process]::Start(“calc”)

But as soon as you try to pass in something like “ipconfig /all” the thing blows up.

Turns out you can pass in arguments a couple of ways. The Powerscripting guys noted that if you pass in the argument of the executable as a second argument to Start, it will work great.

[System.Diagnostics.Process]::Start(“ipconfig”,”all”)

However, there is another way if you want to get a little bit more fancy and do things like hide windows or redirect output from StdOut or StdErr.

You can use a System.Diagnostic.ProcessStartInfo object. Using get-member we can take a look at all the options we have for such a thing.

   1: PS 124 >  $si = New-Object System.Diagnostics.ProcessStartInfo
   2: PS 125 >  $si | gm  -type property | select Name
   3:  
   4: Name
   5: ----
   6: Arguments
   7: CreateNoWindow
   8: Domain
   9: EnvironmentVariables
  10: ErrorDialog
  11: ErrorDialogParentHandle
  12: FileName
  13: LoadUserProfile
  14: Password
  15: RedirectStandardError
  16: RedirectStandardInput
  17: RedirectStandardOutput
  18: StandardErrorEncoding
  19: StandardOutputEncoding
  20: UserName
  21: UseShellExecute
  22: Verb
  23: Verbs
  24: WindowStyle
  25: WorkingDirectory

Lots of goodness here that we can play with. Once you build up the ProcessStartInfo object, you pass that whole object in as the arg to the Start method of system.diagnostics.process.

I put together a quick function to show how this could be used more generically.

   1: function Start-Proc  {
   2:     param (
   3:             [string]$exe = $(Throw "An executable must be specified"),
   4:             [string]$arguments,
   5:             [switch]$hidden,
   6:             [switch]$waitforexit
   7:             )    
   8:     
   9:     # Build Startinfo and set options according to parameters
  10:     $startinfo = new-object System.Diagnostics.ProcessStartInfo 
  11:     $startinfo.FileName = $exe
  12:     $startinfo.Arguments = $arguments
  13:     if ($hidden){
  14:                 $startinfo.WindowStyle = "Hidden"
  15:                 $startinfo.CreateNoWindow = $TRUE
  16:                 }
  17:     $process = [System.Diagnostics.Process]::Start($startinfo)
  18:     if ($waitforexit) {$process.WaitForExit()}
  19:     
  20: }
  21:  
  22:  Start-Proc calc
  23:  Start-Proc calc -waitforexit
  24:  Start-Proc -exe ipconfig -arguments /all
  25:  Start-Proc ipconfig /all
  26:  Start-Proc ipconfig /all -hidden

I used the alias Start-Proc so that it would not collide with the PSCX Cmdlet Start-Process.

They have done the same writing their CmdLet  in C#, but with all kinds of options.

   1: PS 130 >  gcm Start-Process | fl *
   2:  
   3:  
   4: DLL              : C:\Program Files (x86)\PowerShell Community Extensions\Pscx.dll
   5: Verb             : Start
   6: Noun             : Process
   7: HelpFile         : Pscx.dll-Help.xml
   8: PSSnapIn         : Pscx
   9: ImplementingType : Pscx.Commands.StartProcessCommand
  10: ParameterSets    : {[[-Path] <String>] [[-Arguments] <String>] [-Verb <String>] [-WorkingDirectory <String>] [-Credential <PSCredential>] [-NoShellExecute] [-NoWindow]
  11:                    [-WindowStyle <ProcessWindowStyle>] [-LoadUserProfile] [-WaitTimeout <Int32>] [-Boost] [-Priority <ProcessPriorityClass>] [-Verbose] [-Debug] [-Error
  12:                    Action <ActionPreference>] [-ErrorVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>] [-WhatIf] [-Confirm], [[-ScriptBlock] <ScriptBlock>
  13:                    ] [-NoProfile] [-WorkingDirectory <String>] [-Credential <PSCredential>] [-NoShellExecute] [-NoWindow] [-WindowStyle <ProcessWindowStyle>] [-LoadUser
  14:                    Profile] [-WaitTimeout <Int32>] [-Boost] [-Priority <ProcessPriorityClass>] [-Verbose] [-Debug] [-ErrorAction <ActionPreference>] [-ErrorVariable <St
  15:                    ring>] [-OutVariable <String>] [-OutBuffer <Int32>] [-WhatIf] [-Confirm]}
  16: Definition       : Start-Process [[-Path] <String>] [[-Arguments] <String>] [-Verb <String>] [-WorkingDirectory <String>] [-Credential <PSCredential>] [-NoShellExecute]
  17:                     [-NoWindow] [-WindowStyle <ProcessWindowStyle>] [-LoadUserProfile] [-WaitTimeout <Int32>] [-Boost] [-Priority <ProcessPriorityClass>] [-Verbose] [-D
  18:                    ebug] [-ErrorAction <ActionPreference>] [-ErrorVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>] [-WhatIf] [-Confirm]
  19:                    Start-Process [[-ScriptBlock] <ScriptBlock>] [-NoProfile] [-WorkingDirectory <String>] [-Credential <PSCredential>] [-NoShellExecute] [-NoWindow] [-W
  20:                    indowStyle <ProcessWindowStyle>] [-LoadUserProfile] [-WaitTimeout <Int32>] [-Boost] [-Priority <ProcessPriorityClass>] [-Verbose] [-Debug] [-ErrorAct
  21:                    ion <ActionPreference>] [-ErrorVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>] [-WhatIf] [-Confirm]
  22:  
  23: Name             : Start-Process
  24: CommandType      : Cmdlet
  25: Visibility       : Public

Scripting Games 2008 Event 2

February 20, 2008

I thought event 2 was pretty cool.  We have to take in a list of figure skaters and their scores, drop the highest and lowest score, average them, and assign a gold, silver and bronze medal to the top three competitors.

This problem was extremely conducive to using the power of the pipeline in Powershell.

   1: $competitors = Get-Content c:\scripts\skaters.txt 
   2: [array]$totalScores = "" | Select-Object Name,Score,Medal
   3:  
   4: foreach ($competitor in $competitors)
   5: {
   6:     $scores = "" | Select-Object Name,Score,Medal
   7:     $scores.Name,$scores.Score = $competitor.Split(",")
   8:     $stats = $scores.Score | Measure-Object -Maximum -Minimum
   9:     $scores.Score = $scores.Score -replace $stats.Maximum
  10:     $scores.Score = $scores.Score -replace $stats.Minimum
  11:     $scores.Score = [math]::Round(($scores.Score | Measure-Object -Average).Average,2)
  12:     $totalScores += $scores
  13: }
  14: $winners = $totalScores | Sort-Object Score -Descending | Select-Object -First 3 $winners[0].Medal,$winners[1].Medal,$winners[2].Medal = "Gold","Silver","Bronze"
  15: $winners | Format-Table -AutoSize

First we pull in the contents of the skaters.txt file. Next I use a little shortcut to create a custom PSObject using select-object. This creates a PSObject with three properties, a Name, a Score, and a Medal. This will become a collection of scores objects.

Using a foreach loop, we go through each competitor and create a new score object, assigning its values.Name to the name of the competitor and then the scores as an array in the .score property.

Please check out my post on multi variable assignment to see how this works.

I can pull the minimum and maximum using measure object and drop them. After that I use measure-object again to take the average and stuff it in the .score property.

I should mention here that /\/\o\/\/’s solution was pretty darn slick. I loved how he sorted and then select 1..5, essentially dropping the top and bottom score before he took the average.

The final item is to add the current score object to the collection of score objects called $totalScores.

Now it becomes quite easy to get the winners. We just sort the $totalScores collection by Score, select the top 3, and assign their .medal value accordingly.

Multi variable assignment is so cool. Ever since I found out about it it seems to appear in everything I do.

 

Have fun Powershelling

 

Andy