Author Archives: Jeffery Hicks

PowerShell PopUp


popupAt the recent PowerShell Summit I presented a session on adding graphical elements to your script without the need for WinForms or WPF. One of the items I demonstrated is a graphical popup that can either require the user to click a button or automatically dismiss after a set time period. If this sounds familiar, yes, it is our old friend VBScript and the Popup method of the Wscript.Shell object. Because PowerShell can create and use COM objects, why not take advantage of this?

All it really takes is two lines. One line to create the Wscript.Shell COM object and one to invoke the Popup method.  (Yes, I’m sure you could do this in one line, but that’s not the point.)

$wshell = New-Object -ComObject Wscript.Shell -ErrorAction Stop
$wshell.Popup("Are you looking at me?",0,"Hey!",48+4)

The Popup method needs parameters for the message, title, a timeout value, and an integer value that represents a combination of buttons and icons.

popup2The challenging part has always been trying to remember the integer values. So I wrote a quick function called New-Popup.

#requires -version 2.0

Function New-Popup {

<#
.Synopsis
Display a Popup Message
.Description
This command uses the Wscript.Shell PopUp method to display a graphical message
box. You can customize its appearance of icons and buttons. By default the user
must click a button to dismiss but you can set a timeout value in seconds to 
automatically dismiss the popup. 

The command will write the return value of the clicked button to the pipeline:
  OK     = 1
  Cancel = 2
  Abort  = 3
  Retry  = 4
  Ignore = 5
  Yes    = 6
  No     = 7

If no button is clicked, the return value is -1.
.Example
PS C:\> new-popup -message "The update script has completed" -title "Finished" -time 5

This will display a popup message using the default OK button and default 
Information icon. The popup will automatically dismiss after 5 seconds.
.Notes
Last Updated: April 8, 2013
Version     : 1.0

.Inputs
None
.Outputs
integer

Null   = -1
OK     = 1
Cancel = 2
Abort  = 3
Retry  = 4
Ignore = 5
Yes    = 6
No     = 7
#>

Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter a message for the popup")]
[ValidateNotNullorEmpty()]
[string]$Message,
[Parameter(Position=1,Mandatory=$True,HelpMessage="Enter a title for the popup")]
[ValidateNotNullorEmpty()]
[string]$Title,
[Parameter(Position=2,HelpMessage="How many seconds to display? Use 0 require a button click.")]
[ValidateScript({$_ -ge 0})]
[int]$Time=0,
[Parameter(Position=3,HelpMessage="Enter a button group")]
[ValidateNotNullorEmpty()]
[ValidateSet("OK","OKCancel","AbortRetryIgnore","YesNo","YesNoCancel","RetryCancel")]
[string]$Buttons="OK",
[Parameter(Position=4,HelpMessage="Enter an icon set")]
[ValidateNotNullorEmpty()]
[ValidateSet("Stop","Question","Exclamation","Information" )]
[string]$Icon="Information"
)

#convert buttons to their integer equivalents
Switch ($Buttons) {
    "OK"               {$ButtonValue = 0}
    "OKCancel"         {$ButtonValue = 1}
    "AbortRetryIgnore" {$ButtonValue = 2}
    "YesNo"            {$ButtonValue = 4}
    "YesNoCancel"      {$ButtonValue = 3}
    "RetryCancel"      {$ButtonValue = 5}
}

#set an integer value for Icon type
Switch ($Icon) {
    "Stop"        {$iconValue = 16}
    "Question"    {$iconValue = 32}
    "Exclamation" {$iconValue = 48}
    "Information" {$iconValue = 64}
}

#create the COM Object
Try {
    $wshell = New-Object -ComObject Wscript.Shell -ErrorAction Stop
    #Button and icon type values are added together to create an integer value
    $wshell.Popup($Message,$Time,$Title,$ButtonValue+$iconValue)
}
Catch {
    #You should never really run into an exception in normal usage
    Write-Warning "Failed to create Wscript.Shell COM object"
    Write-Warning $_.exception.message
}

} #end function

The function lets you use text descriptions for the buttons and icons. In PowerShell 3.0 you will also get tab completion for the possible values. This makes it easy to create a command like this:

new-popup "Do you want to get work done with PowerShell?" -Title "Hey, you!" -Buttons YesNo -Icon Question

The function writes the value of the clicked button to the pipeline. I expect this is something you are more apt to use in a script. Perhaps to display an error message or even to prompt the user for an action. Maybe you’d like to use it in your PowerShell 3.0 profile:

$r = New-Popup -Title "Help Update" -Message "Do you want to update help now?" -Buttons YesNo -Time 5 -Icon Question
if ($r -eq 6) {
  Update-Help -SourcePath \\jdh-nvnas\files\PowerShell_Help -Force
}

The popup will automatically dismiss after 5 seconds unless I click Yes or No. You should be able to copy the function text from the listing above by toggling to plain code, select all and copy.

I hope you’ll let me know where you use this.

PowerShell Summit 2013 Kicks Off


I’m very excited to be in Redmond for a few days as part of the PowerShell Summit. I love catching up with old friends and making new ones all over PowerShell. If you couldn’t make it this year, and I know many of you will feel you are missing out, there’s always next year. We are making plans to accommodate more people in 2014.

I’ll also be trying to tweet and blog what I can over the next few days.

Friday Fun PowerShell Commands by Noun


One of PowerShell’s greatest strength’s is discoverability. Once you know how, it is very easy to discover what  you can do with PowerShell and how. One reason this works is because PowerShell commands follow a consistent verb-noun naming convention. With this in mind, you can see all of the commands organized by noun.

get-command -CommandType cmdlet | sort noun,verb | format-table -group noun

This will work in both v2 and v3.

get-command-noun-01In PowerShell 3.0, this will display commands from all modules, even those not currently loaded. If you want to limit your display to only those modules currently imported use this:

get-command -CommandType cmdlet -ListImported | sort noun,verb | Format-Table -group noun

Or you can use this same idea to organize cmdlets in a specific module.

get-command -mod hyper-v | sort Noun,Verb | format-table -group Noun

get-command-noun-02Now that you know what you can do, go forth and do it!

Friday Fun: Get-Anniversary


Recently I celebrated a wedding anniversary. Even though I didn’t forget I could have used a little help. So I figured since I’m in PowerShell all the time anyway, it could help. I built a little script to remind me of important dates. Let me walk you through the key steps.

First, I’ll define a variable for the anniversary date.

[datetime]$anndate="5/6/2007"

I picked a date coming up. Next, it isn’t too difficult to calculate the number of days between two dates. But what I needed to do is find May 6th for this year. Here’s how I did it:

[datetime]$thisYear = "$($anndate.month)/$($anndate.day)"

$thisYear is now May 6, 2013. Excellent, because now I can get the number of days until that date.

$when = $thisYear - (Get-Date)

$when is a TimeSpan object.

Days              : 23
Hours             : 11
Minutes           : 25
Seconds           : 42
Milliseconds      : 300
Ticks             : 20283423003384
TotalDays         : 23.4761840316944
TotalHours        : 563.428416760667
TotalMinutes      : 33805.70500564
TotalSeconds      : 2028342.3003384
TotalMilliseconds : 2028342300.3384

I can use the Days property in my message. Although the other item of information I wanted is the number of years for the anniversary. Very important. Again, because we’re working with objects all I need to do is subtract the Year properties between the anniversary date and today.

$NumYears = (Get-Date).year - $anndate.Year

At this point I could simply display a message that tells me how many days are remaining until anniversary #X. But let’s go a step further. How about displaying the number of years as an ordinal? For example, 5th or 23rd anniversary. To do that I did a quick search to see if someone had already figured this out, since there is no .NET specific method to accomplish this. I found some code samples and converted them into a PowerShell function.

Function Get-Ordinal {

Param([int]$i)

Switch ($i %100) {
 #handle special cases
 11 {$sfx = "th" } 
 12 {$sfx = "th" } 
 13 {$sfx = "th" } 
 default {
    Switch ($i % 10) {
        1  { $sfx = "st" }
        2  { $sfx = "nd" }
        3  { $sfx = "rd" }
        default { $sfx = "th" }
    } #inner switch
 } #default
} #outerswitch
 #write the result to the pipeline
 "$i$sfx"
} #end Get-Ordinal

Basically you take the number and perform a modulo operation. Based on the result I know what suffix to use and the function writes the ordinal string to the pipeline, like 1st or 7th.  With that, all that is left to do is display my message.

$msg = "Your {0} anniversary is in {1} days." -f (Get-Ordinal $NumYears),$when.Days

Let me show you the complete script.

Param (
#what is the anniversary date
[datetime]$anndate="5/6/2007"
)

#a function to create ordinal numbers
Function Get-Ordinal {

Param([int]$i)

Switch ($i %100) {
 #handle special cases
 11 { $sfx = "th" } 
 12 { $sfx = "th" } 
 13 { $sfx = "th" } 
 default {
    Switch ($i % 10) {
        1  { $sfx = "st" }
        2  { $sfx = "nd" }
        3  { $sfx = "rd" }
        default { $sfx = "th"}
    } #inner switch
 } #default
} #outerswitch
 #write the result to the pipeline
 "$i$sfx"
} #end Get-Ordinal

#how many years
$NumYears = (Get-Date).year - $anndate.Year

#create the anniversary date for this year
[datetime]$thisYear = "$($anndate.month)/$($anndate.day)"

#is anniversary next year?
if ($thisYear -lt (Get-Date)) {
  #add a year"
  $thisYear=$thisYear.AddYears(1)
  $NumYears++
}

#how soon is the anniversary?
$when = $thisYear - (Get-Date)

#define an empty hashtable for parameters
$phash = @{}

if ($when.Days -gt 0) {
  $msg = "Your {0} anniversary is in {1} days." -f (Get-Ordinal $NumYears),$when.Days
  $phash.Add("Object",$msg)
  $phash.Add("Foregroundcolor","Green")
}
else {
  $msg = "Your {0} anniversary is in {1} hours and {2} minutes!" -f (Get-Ordinal $NumYears),$when.hours,$when.minutes
  $phash.Add("object",$msg)
  $phash.Add("Foregroundcolor","Red")
  #Find a florist!!
  start "http://www.google.com/#hl=en&output=search&q=florist"
}

Write-Host @phash

The only other special feature of this script is that if the number of days is greater than one, the message is display using Write-Host in green.

get-anniversary

BUT, if you are down to 1 day or less you get the message in red AND your browser will open up to a Google search for Florists. I’m trying to help you out as much as I can. You could call this script from your PowerShell profile and (hopefully) never forget another anniversary or important date. Or you might simply take away some tidbits about datetime objects, timespans and splatting. Either way I think you come out ahead.

Enjoy.

UPDATE April 16, 2013
I realized my original code had a problem: if the anniversary date was next year you would get a negative result. For example if the anniversary was yesterday, the script would say your anniversary was in -1 days. That won’t work. So I added some code to test the anniversary date compared to the current date. If it is less than today, meaning it has already passed, then I need to add a year.

if ($thisYear -lt (Get-Date)) {
  #add a year"
  $thisYear=$thisYear.AddYears(1)
  $NumYears++
}

Now when I calculate the difference between $thisYear and today, I’ll get a positive number. I suppose I should rename the variables because $thisYear, in this case, is actually the date for the anniversary next year. I’m also incrementing the value of the number of years is updated.

The other thing to take away from this, and something that I neglected to do (shame on me) is to test based on data that will fail as well as succeed. For this script, I neglected to test for dates that have already passed.

Get CIMInstance from PowerShell 2.0


I love the new CIM cmdlets in PowerShell 3.0. Querying WMI is a little faster because the CIM cmdlets query WMI using the WSMAN protocol instead of DCOM. The catch is that remote computers must be running PowerShell 3 which includes the latest version of the WSMAN protocol and the WinRM service. But if your computers are running 3.0 then you can simply run a command like this:

get-content computers.txt | get-ciminstance win32_operatingsystem

However, if one of the computers is running PowerShell 2.0, you’ll get an error.

get-ciminstance-error

In this example, CHI-DC02 is not running PowerShell 3.0. The solution is to create a CIMSession using a CIMSessionOption for DCOM.

$opt = New-CimSessionOption -Protocol Dcom
$cs = New-CimSession -ComputerName CHI-DC02 -SessionOption $opt
$cs | get-ciminstance win32_operatingsystem

get-ciminstance-dcom

So there is a workaround, but you have to know ahead of time which computers are not running PowerShell 3.0. When you use Get-CimInstance and specify a computername, the cmdlet setups up a temporary CIMSession. So why not create the temporary CIMSession with the DCOM option if it is needed? So I wrote a “wrapper” function called Get-MyCimInstance to do just that.

The heart of the function is a nested function to test if a remote computer is running WSMAN 3.0.

Function Test-IsWsman3 {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline)]
[string]$Computername=$env:computername
)

Begin {
    #a regular expression pattern to match the ending
    [regex]$rx="\d\.\d$"
}
Process {
    Try {
        $result = Test-WSMan -ComputerName $Computername -ErrorAction Stop
    }
    Catch {
        Write-Error $_.exception.message
    }
    if ($result) {
        $m = $rx.match($result.productversion).value
        if ($m -eq '3.0') {
            $True
        }
        else {
            $False
        }
    }
} #process
End {
 #not used
}
} #end Test-IsWSMan

The function uses Test-WSMan and a regular expression to get the remoting version. If it is 3.0 the function returns True. In Get-MyCIMInstance I test each computer and if not running 3.0, create the CIMSession option and include it when creating the temporary CIMSession.

Try {
#test if computer is running WSMAN 2
$isWSMAN3 = Test-IsWsman3 -Computername $computer -ErrorAction Stop

if (-NOT $isWSMAN3) {
    #create a CIM session using the DCOM protocol
    Write-Verbose "Creating a DCOM option"
    $opt = New-CimSessionOption -Protocol Dcom
    $sessparam.Add("SessionOption",$opt)
}
Else {
        Write-Verbose "Confirmed WSMAN 3.0"
}

Try {               
    $session = New-CimSession @sessParam
}
Catch {
    Write-Warning "Failed to create a CIM session to $computer"
    Write-Warning $_.Exception.Message
}

I’m using a Try/Catch block because if the computer is offline, my test function will throw an exception which I can catch.

Catch {
        Write-Warning "Unable to verify WSMAN on $Computer"
     }

Otherwise, all is good and  I can pass the rest of the parameters to Get-CimInstance.

#create the parameters to pass to Get-CIMInstance
        $paramHash=@{
         CimSession= $session
         Class = $class
        }

        $cimParams = "Filter","KeyOnly","Shallow","OperationTimeOutSec","Namespace"
        foreach ($param in $cimParams) {
          if ($PSBoundParameters.ContainsKey($param)) {
            Write-Verbose "Adding $param"
            $paramhash.Add($param,$PSBoundParameters.Item($param))
          } #if
        } #foreach param

        #execute the query
        Write-Verbose "Querying $class"
        Get-CimInstance @paramhash

At the end of the process, I remove the temporary CIMSession. With this, now I can query both v2 and v3 computers.
get-myciminstance01
Notice for CHI-DC02 I’m creating the DCOM option. Here’s the command without all the verboseness.
get-myciminstance02
I could have created a proxy function for Get-CimInstance, but not only are they more complicated, I didn’t want that much transparency. I wanted to know that I’m querying using my function and not Get-CimInstance. Here’s the complete script.

#requires -version 3.0

<#
  ****************************************************************
  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
  ****************************************************************
 #>

Function Get-MyCimInstance {

<#
.Synopsis
Create on-the-fly CIMSessions to retrieve WMI data
.Description
The Get-CimInstance cmdlet in PowerShell 3 can be used to retrieve WMI information
from a remote computer using the WSMAN protocol instead of the legacy WMI service
that uses DCOM and RPC. However, the remote computers must be running PowerShell
3 and the latest version of the WSMAN protocol. When querying a remote computer,
Get-CIMInstance setups a temporary CIMSession. However, if the remote computer is
running PowerShell 2.0 this will fail. You have to manually create a CIMSession
with a CIMSessionOption to use the DCOM protocol.

This command does that for you automatically. It is designed to use computernames.
The computer is tested and if it is running PowerShell 2.0 then a temporary session
is created using DCOM. Otherwise a standard CIMSession is created. The remaining 
CIM parameters are then passed to Get-CIMInstance.

Get-MyCimInstance is essentially a wrapper around Get-CimInstance to make it easier
to query data from a mix of computers.
.Example
PS C:\> get-content computers.txt | get-myciminstance -class win32_logicaldisk -filter "drivetype=3"
.Notes
Last Updated: April 11, 2013
Version     : 1.0
Author      : Jeffery Hicks (@JeffHicks)

Read PowerShell:
Learn Windows PowerShell 3 in a Month of Lunches
Learn PowerShell Toolmaking in a Month of Lunches
PowerShell in Depth: An Administrator's Guide

.Link

http://jdhitsolutions.com/blog/2013/04/get-ciminstance-from-powershell-2-0

.Link
Get-CimInstance
New-CimSession
New-CimsessionOption

.Inputs
string

.Outputs
CIMInstance

#>

[cmdletbinding()]

Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter a class name",
ValueFromPipelineByPropertyName)]
[ValidateNotNullorEmpty()]
[string]$Class,
[Parameter(Position=1,ValueFromPipelineByPropertyName,ValueFromPipeline)]
[ValidateNotNullorEmpty()]
[string[]]$Computername=$env:computername,
[Parameter(ValueFromPipelineByPropertyName)]
[string]$Filter,
[Parameter(ValueFromPipelineByPropertyName)]
[string[]]$Property,
[Parameter(ValueFromPipelineByPropertyName)]
[ValidateNotNullorEmpty()]
[string]$Namespace="root\cimv2",
[switch]$KeyOnly,
[uint32]$OperationTimeoutSec,
[switch]$Shallow,
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
)

Begin {
    Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"  
    Write-verbose -Message ($PSBoundParameters | out-string)

    Function Test-IsWsman3 {
        [cmdletbinding()]
        Param(
        [Parameter(Position=0,ValueFromPipeline)]
        [string]$Computername=$env:computername
        )

        Begin {
            #a regular expression pattern to match the ending
            [regex]$rx="\d\.\d$"
        }
        Process {
            Try {
                $result = Test-WSMan -ComputerName $Computername -ErrorAction Stop
            }
            Catch {
                #Write the error to the pipeline if the computer is offline
                #or there is some other issue
                write-Error $_.exception.message
            }
            if ($result) {
                $m = $rx.match($result.productversion).value
                if ($m -eq '3.0') {
                    $True
                }
                else {
                    $False
                }
            }
        } #process
        End {
         #not used
        }
        } #end Test-IsWSMan

} #begin

Process {
    foreach ($computer in $computername) {
        Write-Verbose "Processing $computer"

        #hashtable of parameters for New-CimSession
        $sessParam=@{Computername=$computer;ErrorAction='Stop'}
        if ($credential) {
            Write-Verbose "Adding alternate credential for CIMSession"
            $sessParam.Add("Credential",$Credential)
        }
        Try {
        #test if computer is running WSMAN 2
        $isWSMAN3 = Test-IsWsman3 -Computername $computer -ErrorAction Stop

        if (-NOT $isWSMAN3) {
            #create a CIM session using the DCOM protocol
            Write-Verbose "Creating a DCOM option"
            $opt = New-CimSessionOption -Protocol Dcom
            $sessparam.Add("SessionOption",$opt)
        }
        Else {
                Write-Verbose "Confirmed WSMAN 3.0"
        }

        Try {               
            $session = New-CimSession @sessParam
        }
        Catch {
            Write-Warning "Failed to create a CIM session to $computer"
            Write-Warning $_.Exception.Message
        }

        #create the parameters to pass to Get-CIMInstance
        $paramHash=@{
         CimSession= $session
         Class = $class
        }

        $cimParams = "Filter","KeyOnly","Shallow","OperationTimeOutSec","Namespace"
        foreach ($param in $cimParams) {
          if ($PSBoundParameters.ContainsKey($param)) {
            Write-Verbose "Adding $param"
            $paramhash.Add($param,$PSBoundParameters.Item($param))
          } #if
        } #foreach param

        #execute the query
        Write-Verbose "Querying $class"
        Get-CimInstance @paramhash

        #remove the temporary cimsession
        Remove-CimSession $session
     } #Try
     Catch {
        Write-Warning "Unable to verify WSMAN on $Computer"
     }
    } #foreach computer

} #process

End {
    Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end

} #end Get-MyCimInstance

I hope you’ll let me know what you think and if you find this useful.

UPDATE: I’ve revised this script and article since it’s original posting to better handle errors if you can’t test WSMAN. I also added support for alternate credentials, which is something you can’t do with Get-CimInstance.

File Age Groupings with PowerShell


I’m always talking about how much the object-nature of PowerShell makes all the difference in the world. Today, I have another example. Let’s say you want to analyze a directory, perhaps a shared group folder for a department. And you want to identify files that haven’t been modified in a while. I like this topic because it is real world and offers a good framework for demonstrating PowerShell techniques.

You would like to divide the files into aging “buckets”. Let’s begin by getting all of the files. I’m using PowerShell 3.0 so you’ll have to adjust parameters if you are using 2.0. You can run all of this interactively in the console, but I think you’ll find using a script much easier.

$files = dir c:\work -recurse -file

Now, let’s add a new property, or member, to the file object called FileAgeDays which will be the value of the number of days since the file was last modified, based on the LastWriteTime property. We’ll use the Add-Member cmdlet to define this property.

$files | Add-Member ScriptProperty -Name FileAgeDays -Value {
 [int]((Get-Date) - ($this.LastWriteTime)).TotalDays }

The new property is technically a ScriptProperty so that we can run a scriptblock to define the value. In this case we’re subtracting the LastwriteTime value of the each object from the current date and time. This will return a TimeStamp object but all we need is the TotalDays property which is cast as an integer, effectively rounding the value. In a pipelined expression like Select-Object you would use $_ to indicate the current object in the pipeline. Here, we can use $this.

Next, we’ll add another script property to define our “bucket” property.

$files | Add-Member ScriptProperty -Name FileAge -Value {
    if ($this.FileAgeDays -ge 365) {
        "1year"
    }
    elseif ($this.FileAgeDays -ge 180) {
        "6Months"
    }
    elseif ($this.FileAgeDays -ge 90) {
        "90Days"
    }
    elseif ($this.FileAgeDays -ge 45) {
        "45Days"
    }
    else {
        "Current"
    }
}

The script block can be as long as you need it to be. Here, we’re using an If/ElseIf construct based on the FileAgeDays property we just created. If we look at $files now, we won’t see these new properties.

fileage-01

But that is because the new properties aren’t part of the default display settings. So we need to specify them.

fileage-02

Now, we can group the objects based on these new properties.

$files | Group FileAge -NoElement | sort Count -Descending

fileage-03Or perhaps we’d like to drill down a bit more.

$grouped = $files | Group FileAge | 
Add-Member -MemberType ScriptProperty -Name SizeMB -Value {
  ($this.Group | Measure-Object Length -sum).Sum / 1MB
} -PassThru

Now we’ve added a new member to the GroupInfo object that will show the total size of all files in each group by MB. Don’t forget to use -Passthru to force PowerShell to write the new object back to the pipeline so it can be saved in the grouped variable. Finally, the result:

$grouped | Sort SizeMB -Descending | Format-Table SizeMB,Count,Name -AutoSize

fileage-04

And there you go. Because we’re working with objects, adding new information is really quite easy. Certainly much easier than trying to do something like this in VBScript! And even if you don’t need this specific solution, I hope that you picked up a technique or two.

File Age Groupings with PowerShell


I’m always talking about how much the object-nature of PowerShell makes all the difference in the world. Today, I have another example. Let’s say you want to analyze a directory, perhaps a shared group folder for a department. And you want to identify files that haven’t been modified in a while. I like this topic because it is real world and offers a good framework for demonstrating PowerShell techniques.

You would like to divide the files into aging “buckets”. Let’s begin by getting all of the files. I’m using PowerShell 3.0 so you’ll have to adjust parameters if you are using 2.0. You can run all of this interactively in the console, but I think you’ll find using a script much easier.

$files = dir c:\work -recurse -file

Now, let’s add a new property, or member, to the file object called FileAgeDays which will be the value of the number of days since the file was last modified, based on the LastWriteTime property. We’ll use the Add-Member cmdlet to define this property.

$files | Add-Member ScriptProperty -Name FileAgeDays -Value {
 [int]((Get-Date) - ($this.LastWriteTime)).TotalDays }

The new property is technically a ScriptProperty so that we can run a scriptblock to define the value. In this case we’re subtracting the LastwriteTime value of the each object from the current date and time. This will return a TimeStamp object but all we need is the TotalDays property which is cast as an integer, effectively rounding the value. In a pipelined expression like Select-Object you would use $_ to indicate the current object in the pipeline. Here, we can use $this.

Next, we’ll add another script property to define our “bucket” property.

$files | Add-Member ScriptProperty -Name FileAge -Value {
    if ($this.FileAgeDays -ge 365) {
        "1year"
    }
    elseif ($this.FileAgeDays -ge 180) {
        "6Months"
    }
    elseif ($this.FileAgeDays -ge 90) {
        "90Days"
    }
    elseif ($this.FileAgeDays -ge 45) {
        "45Days"
    }
    else {
        "Current"
    }
}

The script block can be as long as you need it to be. Here, we’re using an If/ElseIf construct based on the FileAgeDays property we just created. If we look at $files now, we won’t see these new properties.

fileage-01

But that is because the new properties aren’t part of the default display settings. So we need to specify them.

fileage-02

Now, we can group the objects based on these new properties.

$files | Group FileAge -NoElement | sort Count -Descending

fileage-03Or perhaps we’d like to drill down a bit more.

$grouped = $files | Group FileAge | 
Add-Member -MemberType ScriptProperty -Name SizeMB -Value {
  ($this.Group | Measure-Object Length -sum).Sum / 1MB
} -PassThru

Now we’ve added a new member to the GroupInfo object that will show the total size of all files in each group by MB. Don’t forget to use -Passthru to force PowerShell to write the new object back to the pipeline so it can be saved in the grouped variable. Finally, the result:

$grouped | Sort SizeMB -Descending | Format-Table SizeMB,Count,Name -AutoSize

fileage-04

And there you go. Because we’re working with objects, adding new information is really quite easy. Certainly much easier than trying to do something like this in VBScript! And even if you don’t need this specific solution, I hope that you picked up a technique or two.

Half off Learn Windows PowerShell 3 in a Month of Lunches


Learn PowerShell 3 in a Month of Lunches Today, Learn Windows PowerShell 3 in a Month of Lunches is part of Manning’s Deal of the Day. This is the book for the absolute beginner and will teach you literally in a month of lunch breaks all you need to know to get started with PowerShell 3.0.

Not convinced? Take a moment to read the Amazon reviews. Even though you can probably get a good deal at Amazon, the Manning Deal of the Day is only good through their store. So visit the book’s page and use promo code dotd0404au when you check out to save 50%. By the way, this promo code also applies to Learn Windows IIS in a Month of Lunches by Jason Helmick (http://www.manning.com/helmick/).

Half off Learn Windows PowerShell 3 in a Month of Lunches


Learn PowerShell 3 in a Month of Lunches Today, Learn Windows PowerShell 3 in a Month of Lunches is part of Manning’s Deal of the Day. This is the book for the absolute beginner and will teach you literally in a month of lunch breaks all you need to know to get started with PowerShell 3.0.

Not convinced? Take a moment to read the Amazon reviews. Even though you can probably get a good deal at Amazon, the Manning Deal of the Day is only good through their store. So visit the book’s page and use promo code dotd0404au when you check out to save 50%. By the way, this promo code also applies to Learn Windows IIS in a Month of Lunches by Jason Helmick (http://www.manning.com/helmick/).

Get Local Admin Group Members in a New Old Way


Yesterday I posted a quick article on getting the age of the local administrator account password. It seemed appropropriate to follow up on a quick and dirty way to list all members of the local administrator group. Normally, I would turn to WMI (and have written about this in the past). But WMI is relatively slow for this task and even using the new CIM cmdlets in PowerShell 3.0 don’t improve performance. Instead I’m going to return to an old school technique using the NET command.

PS C:\> net localgroup administrators
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Domain Admins
localadmin
The command completed successfully.

It is very easy to see members. To query a remote computer all I need to do is wrap this in Invoke-Command and use PowerShell remoting.

PS C:\> invoke-command {net localgroup administrators} -comp chi-fp01
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins
The command completed successfully.

Yes, there is some overhead for remoting but overall performance is pretty decent. And if you already have an established PSSession, even better. For quick and dirty one-liner it doesn’t get much better. Well, maybe it can.

I have no problem using legacy tools when they still get the job done and this certainly qualifies. To make it more PowerShell friendly though, let’s clean up the output by filtering out blanks, that last line and skipping the “header” lines.

PS C:\> invoke-command {
>> net localgroup administrators | 
>> where {$_ -AND $_ -notmatch "command completed successfully"} | 
>> select -skip 4
>> } -computer chi-fp01
>>
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins

Boom. Now I only get the member names. Let’s go one more level and write an object to the pipeline and be better at handling output from multiple computers. I came up with a scriptblock like this:

$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }

This will create a simple object with a properties for the computername, group name and members. Here’s how I can use it with Invoke-Command.

invoke-command {
$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }
} -computer chi-fp01,chi-win8-01,chi-ex01 -HideComputerName | 
Select * -ExcludeProperty RunspaceID

get-netlocalgroupNow I have objects that I can export to XML, convert to HTML or send to a file. But since I’ve come this far, I might as well take a few more minutes and turn this into a reusable tool.

Function Get-NetLocalGroup {
[cmdletbinding()]

Param(
[Parameter(Position=0)]
[ValidateNotNullorEmpty()]
[object[]]$Computername=$env:computername,
[ValidateNotNullorEmpty()]
[string]$Group = "Administrators",
[switch]$Asjob
)

Write-Verbose "Getting members of local group $Group"

#define the scriptblock
$sb = {
 Param([string]$Name = "Administrators")
$members = net localgroup $Name | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = $Name
 Members=$members
 }
} #end scriptblock

#define a parameter hash table for splatting
$paramhash = @{
 Scriptblock = $sb
 HideComputername=$True
 ArgumentList=$Group
 }

if ($Computername[0] -is [management.automation.runspaces.pssession]) {
    $paramhash.Add("Session",$Computername)
}
else {
    $paramhash.Add("Computername",$Computername)
}

if ($asjob) {
    Write-Verbose "Running as job"
    $paramhash.Add("AsJob",$True)
}

#run the command
Invoke-Command @paramhash | Select * -ExcludeProperty RunspaceID

} #end Get-NetLocalGroup

This function lets me specify a group of computers or PSSessions as well as the local group name. Today I may need to know who belongs to the local administrator’s group but tomorrow it might be Remote Desktop Users.

PS C:\> Get-NetLocalGroup -Group "remote desktop users" -Computername $sessions

Group        : remote desktop users
Computername : CHI-FP01
Members      : 

Group        : remote desktop users
Computername : CHI-WIN8-01
Members      : 

Computername : CHI-EX01
Members      : 
Group        : remote desktop users

Group        : remote desktop users
Computername : CHI-DC01
Members      : jfrost

Sometimes even old school tools can still be a part of your admin toolkit.

 

Get Local Admin Group Members in a New Old Way


Yesterday I posted a quick article on getting the age of the local administrator account password. It seemed appropropriate to follow up on a quick and dirty way to list all members of the local administrator group. Normally, I would turn to WMI (and have written about this in the past). But WMI is relatively slow for this task and even using the new CIM cmdlets in PowerShell 3.0 don’t improve performance. Instead I’m going to return to an old school technique using the NET command.

PS C:\> net localgroup administrators
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Domain Admins
localadmin
The command completed successfully.

It is very easy to see members. To query a remote computer all I need to do is wrap this in Invoke-Command and use PowerShell remoting.

PS C:\> invoke-command {net localgroup administrators} -comp chi-fp01
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins
The command completed successfully.

Yes, there is some overhead for remoting but overall performance is pretty decent. And if you already have an established PSSession, even better. For quick and dirty one-liner it doesn’t get much better. Well, maybe it can.

I have no problem using legacy tools when they still get the job done and this certainly qualifies. To make it more PowerShell friendly though, let’s clean up the output by filtering out blanks, that last line and skipping the “header” lines.

PS C:\> invoke-command {
>> net localgroup administrators | 
>> where {$_ -AND $_ -notmatch "command completed successfully"} | 
>> select -skip 4
>> } -computer chi-fp01
>>
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins

Boom. Now I only get the member names. Let’s go one more level and write an object to the pipeline and be better at handling output from multiple computers. I came up with a scriptblock like this:

$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }

This will create a simple object with a properties for the computername, group name and members. Here’s how I can use it with Invoke-Command.

invoke-command {
$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }
} -computer chi-fp01,chi-win8-01,chi-ex01 -HideComputerName | 
Select * -ExcludeProperty RunspaceID

get-netlocalgroupNow I have objects that I can export to XML, convert to HTML or send to a file. But since I’ve come this far, I might as well take a few more minutes and turn this into a reusable tool.

Function Get-NetLocalGroup {
[cmdletbinding()]

Param(
[Parameter(Position=0)]
[ValidateNotNullorEmpty()]
[object[]]$Computername=$env:computername,
[ValidateNotNullorEmpty()]
[string]$Group = "Administrators",
[switch]$Asjob
)

Write-Verbose "Getting members of local group $Group"

#define the scriptblock
$sb = {
 Param([string]$Name = "Administrators")
$members = net localgroup $Name | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = $Name
 Members=$members
 }
} #end scriptblock

#define a parameter hash table for splatting
$paramhash = @{
 Scriptblock = $sb
 HideComputername=$True
 ArgumentList=$Group
 }

if ($Computername[0] -is [management.automation.runspaces.pssession]) {
    $paramhash.Add("Session",$Computername)
}
else {
    $paramhash.Add("Computername",$Computername)
}

if ($asjob) {
    Write-Verbose "Running as job"
    $paramhash.Add("AsJob",$True)
}

#run the command
Invoke-Command @paramhash | Select * -ExcludeProperty RunspaceID

} #end Get-NetLocalGroup

This function lets me specify a group of computers or PSSessions as well as the local group name. Today I may need to know who belongs to the local administrator’s group but tomorrow it might be Remote Desktop Users.

PS C:\> Get-NetLocalGroup -Group "remote desktop users" -Computername $sessions

Group        : remote desktop users
Computername : CHI-FP01
Members      : 

Group        : remote desktop users
Computername : CHI-WIN8-01
Members      : 

Computername : CHI-EX01
Members      : 
Group        : remote desktop users

Group        : remote desktop users
Computername : CHI-DC01
Members      : jfrost

Sometimes even old school tools can still be a part of your admin toolkit.

 

Get Local Admin Group Members in a New Old Way


Yesterday I posted a quick article on getting the age of the local administrator account password. It seemed appropropriate to follow up on a quick and dirty way to list all members of the local administrator group. Normally, I would turn to WMI (and have written about this in the past). But WMI is relatively slow for this task and even using the new CIM cmdlets in PowerShell 3.0 don’t improve performance. Instead I’m going to return to an old school technique using the NET command.

PS C:\> net localgroup administrators
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Domain Admins
localadmin
The command completed successfully.

It is very easy to see members. To query a remote computer all I need to do is wrap this in Invoke-Command and use PowerShell remoting.

PS C:\> invoke-command {net localgroup administrators} -comp chi-fp01
Alias name     administrators
Comment        Administrators have complete and unrestricted access to the computer/domain

Members

-------------------------------------------------------------------------------
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins
The command completed successfully.

Yes, there is some overhead for remoting but overall performance is pretty decent. And if you already have an established PSSession, even better. For quick and dirty one-liner it doesn’t get much better. Well, maybe it can.

I have no problem using legacy tools when they still get the job done and this certainly qualifies. To make it more PowerShell friendly though, let’s clean up the output by filtering out blanks, that last line and skipping the “header” lines.

PS C:\> invoke-command {
>> net localgroup administrators | 
>> where {$_ -AND $_ -notmatch "command completed successfully"} | 
>> select -skip 4
>> } -computer chi-fp01
>>
Administrator
GLOBOMANTICS\Chicago IT
GLOBOMANTICS\Domain Admins

Boom. Now I only get the member names. Let’s go one more level and write an object to the pipeline and be better at handling output from multiple computers. I came up with a scriptblock like this:

$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }

This will create a simple object with a properties for the computername, group name and members. Here’s how I can use it with Invoke-Command.

invoke-command {
$members = net localgroup administrators | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = "Administrators"
 Members=$members
 }
} -computer chi-fp01,chi-win8-01,chi-ex01 -HideComputerName | 
Select * -ExcludeProperty RunspaceID

get-netlocalgroupNow I have objects that I can export to XML, convert to HTML or send to a file. But since I’ve come this far, I might as well take a few more minutes and turn this into a reusable tool.

Function Get-NetLocalGroup {
[cmdletbinding()]

Param(
[Parameter(Position=0)]
[ValidateNotNullorEmpty()]
[object[]]$Computername=$env:computername,
[ValidateNotNullorEmpty()]
[string]$Group = "Administrators",
[switch]$Asjob
)

Write-Verbose "Getting members of local group $Group"

#define the scriptblock
$sb = {
 Param([string]$Name = "Administrators")
$members = net localgroup $Name | 
 where {$_ -AND $_ -notmatch "command completed successfully"} | 
 select -skip 4
New-Object PSObject -Property @{
 Computername = $env:COMPUTERNAME
 Group = $Name
 Members=$members
 }
} #end scriptblock

#define a parameter hash table for splatting
$paramhash = @{
 Scriptblock = $sb
 HideComputername=$True
 ArgumentList=$Group
 }

if ($Computername[0] -is [management.automation.runspaces.pssession]) {
    $paramhash.Add("Session",$Computername)
}
else {
    $paramhash.Add("Computername",$Computername)
}

if ($asjob) {
    Write-Verbose "Running as job"
    $paramhash.Add("AsJob",$True)
}

#run the command
Invoke-Command @paramhash | Select * -ExcludeProperty RunspaceID

} #end Get-NetLocalGroup

This function lets me specify a group of computers or PSSessions as well as the local group name. Today I may need to know who belongs to the local administrator’s group but tomorrow it might be Remote Desktop Users.

PS C:\> Get-NetLocalGroup -Group "remote desktop users" -Computername $sessions

Group        : remote desktop users
Computername : CHI-FP01
Members      : 

Group        : remote desktop users
Computername : CHI-WIN8-01
Members      : 

Computername : CHI-EX01
Members      : 
Group        : remote desktop users

Group        : remote desktop users
Computername : CHI-DC01
Members      : jfrost

Sometimes even old school tools can still be a part of your admin toolkit.

 

How Old is the Admin Password


Here’s a quick one-liner to find out how old the administrator password age (in days) is on a remote machine.

PS C:\> ([adsi]“WinNT://COMPUTERNAME/administrator”).passwordage.value/86400

This requires RPC/DCOM access to the remote computer. Or use PowerShell remoting with Invoke-Command:

invoke-command {
new-object PSObject -property @{
Computername = $env:computername
AdminAge = ([adsi]“WinNT://$env:computername/administrator”).passwordage.value/86400
}
} -ComputerName $computers | Select Computername,AdminAge

How Old is the Admin Password


Here’s a quick one-liner to find out how old the administrator password age (in days) is on a remote machine.

PS C:\> ([adsi]“WinNT://COMPUTERNAME/administrator”).passwordage.value/86400

This requires RPC/DCOM access to the remote computer. Or use PowerShell remoting with Invoke-Command:

invoke-command {
new-object PSObject -property @{
Computername = $env:computername
AdminAge = ([adsi]“WinNT://$env:computername/administrator”).passwordage.value/86400
}
} -ComputerName $computers | Select Computername,AdminAge

Test Hyper-V VHD Folders with PowerShell


I’ve recently added a USB 3.0 Express Card adapter to my laptop to provide USB 3.0 functionality. The added performance is very useful in my Hyper-V setup. However, I am running into a glitch that I have yet to figure out where the external drives (the Express card has 2 ports) are still attached and recognized, but the file system is wonky. I can see most of the folders, and files but not really. This is a problem when I fire up a Hyper-V virtual machine with a VHD in one of these folders. Even though I can run a DIR command and see the file, it really isn’t there. So I threw together a little function to add to my Hyper-V workflow module to validate folders with VHD and VHDX files.

My virtual machines are spread out among a number of drives. I don’t necessarily need to validate every VHD file, but I would like to know that the paths exist. Now, I could simply pass my list of paths to Test-Path.

PS C:\> "G:\VHDs","F:\VHD","D:\VHD","C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks" | Test-Path
True
True
True
True

But my little glitch can lead to a false positive. So I need to take an extra step and validate one of the VHD/VHDX files using the Test-VHD cmdlet. Here’s my function.

#requires -version 3.0

Function Test-VHDPath {

[cmdletbinding()]
Param (
[ValidateNotNullorEmpty()]
[ValidateScript({Test-Path $_})]
#paths to my virtual hard disks for Hyper-V virtual machines
[string[]]$paths = @("G:\VHDs","F:\VHD","D:\VHD","C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks")
)

Write-Verbose "Starting $($MyInvocation.mycommand)"

foreach ($path in $paths) {
  Try {
    #grab the first VHD\VHDX file and test it. No guarantee other files are OK
    #but at least I know the path has been verified.
    Write-Verbose "Validating $path"
    dir $path\*.vhd,*.vhdx | Select -first 1 | Test-VHD -ErrorAction Stop | out-null
  }
  Catch {
    Write-Error "Failed to validate VHD\VHDX files in $Path. $_.Exception.Message"
    Return
  }
}

#if no errors were found then return a simple True
Write-Verbose "No problems found with VHD paths"
Write $True

} #end function

The function gets the first VHD or VHDX file in each folder and tests it. If the test fails, Test-VHD throws an exception which I catch and then bail out. Otherwise, if there are no errors, the function writes $True to the pipeline. Otherwise, I’ll get an exception.

test-vhdpathBut now I have a tool to quickly verify paths before I start trying to launch virtual machines.

If you’d like to learn more about Test-VHD, take a look at my article on the Altaro Hyper-V blog. If you want to try out my function, you should be able to toggle to plain code and copy and paste.