Archive for March, 2013

UPDATE (Feb 8, 2014): If you need to audit or change the security descriptors for services on a regular basis, please check out this module instead of using the code in this post. It allows for quick auditing and/or modifying of security descriptors for files, folder, registry keys, printers, services, shares, processes, and more. If you find the module useful, please leave it a rating. If you have any questions, please use the Q&A section on the linked page.

PowerShell makes viewing and editing ACLs on files and registry paths easy (well, at least doable) via the Get-Acl and Set-Acl cmdlets. To my knowledge, though, there isn’t a way to view or change service ACLs with these cmdlets. Here’s how I deal with viewing the ACLs. I’ll have a separate post on how to change the ACLs soon.

First, we need a way to represent the ACL. It turns out that there is a generic .NET class that can represent any security descriptor: System.Security.AccessControl.RawSecurityDescriptor. Let’s look at how to create a new instance of this class:


PS > [System.Security.AccessControl.RawSecurityDescriptor].GetConstructors() |
>> foreach ToString
>>
Void .ctor(System.Security.AccessControl.ControlFlags, System.Security.Principal.
SecurityIdentifier, System.Security.Principal.SecurityIdentifier, System.Security.
AccessControl.RawAcl, System.Security.AccessControl.RawAcl)
Void .ctor(System.String)
Void .ctor(Byte[], Int32)

There are three constructors. The last two are the most interesting for this post, though. One takes a single string: the string representation of the Security Descriptor Definition Language (SDDL) for the ACL. The other one takes a byte array and an integer: the binary form of the ACL and an offset specifying where to being reading from the byte array.

I know of three ways to get the ACL information for a service object (there are probably more, though):

  1. Using sc.exe with the sdshow argument:
    
    PS > $Sddl = sc.exe sdshow wuauserv | where { $_ }
    PS > New-Object System.Security.AccessControl.RawSecurityDescriptor ($Sddl)
    
    ControlFlags           : DiscretionaryAclPresent, SelfRelative
    Owner                  : 
    Group                  : 
    SystemAcl              : 
    DiscretionaryAcl       : {System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce}
    ResourceManagerControl : 0
    BinaryLength           : 92
    

    The biggest downside with this method is that only the DACL is being returned. We aren’t getting the Owner, Group or SACL information. As far as I can tell, the sc.exe program will only return DACL information.

  2. Reading from the registry:
    
    PS > $RegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv\Security"
    PS > $BinarySddl = Get-ItemProperty $RegPath | select -Expand Security
    PS > New-Object System.Security.AccessControl.RawSecurityDescriptor ($BinarySddl, 0)
    
    ControlFlags           : DiscretionaryAclPresent, SystemAclPresent, SelfRelative
    Owner                  : S-1-5-18
    Group                  : S-1-5-18
    SystemAcl              : {System.Security.AccessControl.CommonAce}
    DiscretionaryAcl       : {System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce, 
                             System.Security.AccessControl.CommonAce}
    ResourceManagerControl : 0
    BinaryLength           : 144
    </code

    With this method, we're getting more information than was available using sc.exe. The biggest downside with this method is that not all services appear to have this information listed in the registry.

  3. Using the QueryServiceObjectSecurity function from 'advapi32.dll':
    This would require platform invoking, and it's much more complicated (and takes a lot more code) than the previous two methods. We're not going to cover this method in this post, but I will likely make a post covering how to do this at some point in the future. (Please leave a comment if you're interested in it)

For the rest of the post, we're going to use the sc.exe method. It only contains the DACL information, but we're not going to worry about the Owner, Group, or SACL for this post. If you're interested in viewing and/or modifying those, feel free to modify the part of the final function that creates the RawSecurityDescriptor object.

Once we have our RawSecurityDescriptor object, we can actually view the DACL (remember, a Discretionary Access Control List is just a collection of Access Control Entries, or ACEs):


PS > # Store the RawSecurityDescriptor in $sd variable:
PS > $Sddl = sc.exe sdshow wuauserv | where { $_ }
PS > $sd = New-Object System.Security.AccessControl.RawSecurityDescriptor ($Sddl)
PS > $sd.DiscretionaryAcl.GetType()
IsPublic IsSerial Name       BaseType
-------- -------- ----       --------
True     False    RawAcl     System.Security.AccessControl.GenericAcl

PS > $sd.DiscretionaryAcl[0].GetType()
IsPublic IsSerial Name       BaseType
-------- -------- ----       --------
True     False    CommonAce  System.Security.AccessControl.QualifiedAce

PS > # ACL contains 3 ACEs
PS > $sd.DiscretionaryAcl.Count
3

PS > # List the first ACE in DACL:
PS > $sd.DiscretionaryAcl[0]
BinaryLength       : 20
AceQualifier       : AccessAllowed
IsCallback         : False
OpaqueLength       : 0
AccessMask         : 131229
SecurityIdentifier : S-1-5-11
AceType            : AccessAllowed
AceFlags           : None
IsInherited        : False
InheritanceFlags   : None
PropagationFlags   : None
AuditFlags         : None

We've got access to the ACEs, but they're not very readable. We can tell that this ACE is allowing access, but that's about it. The AccessMask tells us what kind of access is being allowed, but we (probably) have no idea what 131229 means. The SecurityIdentifier tells us who is getting this access, but we (probably) don't know to what user or group it is referring. When you get file or registry ACLs, they have a nice 'Access' codeproperty that gives you a much friendlier representation of the ACE. Let's see if we can't make one for our service ACEs.

First, we need to figure out how to convert the numeric access mask into something that is readable. An enumeration would be perfect for this since it allows you to convert both ways: numbers to text, and text to numbers. It took a little searching, but I was eventually able to find enough information to build a flags enumeration. I'm not really going to go into what that means, but I will be more than happy to dedicate a post on enumerations if there is enough interest. Here's the code to add the enumeration to our session:


PS > Add-Type  @"
  [System.FlagsAttribute]
  public enum ServiceAccessFlags : uint
  {
      QueryConfig = 1,
      ChangeConfig = 2,
      QueryStatus = 4,
      EnumerateDependents = 8,
      Start = 16,
      Stop = 32,
      PauseContinue = 64,
      Interrogate = 128,
      UserDefinedControl = 256,
      Delete = 65536,
      ReadControl = 131072,
      WriteDac = 262144,
      WriteOwner = 524288,
      Synchronize = 1048576,
      AccessSystemSecurity = 16777216,
      GenericAll = 268435456,
      GenericExecute = 536870912,
      GenericWrite = 1073741824,
      GenericRead = 2147483648
  }
"@

PS > # Find out what access mask of 131229 in previous example means:
PS > [ServiceAccessFlags] 131229
QueryConfig, QueryStatus, EnumerateDependents, Start, Interrogate, ReadControl


Now we're getting somewhere! Now we need to figure out how to translate the SID into a user or group. Thankfully, there is a built-in method on the SecurityIdentifier object that allows us to translate:


PS > $sd.DiscretionaryAcl[0].SecurityIdentifier.Translate.OverloadDefinitions
System.Security.Principal.IdentityReference Translate(type targetType)

PS > $sd.DiscretionaryAcl[0].SecurityIdentifier.Translate(
>> [System.Security.Principal.NTAccount])
>>

Value
-----
NT AUTHORITY\Authenticated Users

So, we can see the ACEs in the DACL, and we can get readable versions of the user/group and the access permissions. Now, let's use what we know to create a function that takes a service name and returns a custom object that contains the service name, the DACL, and a custom ScriptProperty called Access that mimics the Access property returned from Get-Acl and has the ACEs in a more readable format (PowerShell v2 users should remove the two [ordered] type accelerator instances to get this to work):

Add-Type  @"
  [System.FlagsAttribute]
  public enum ServiceAccessFlags : uint
  {
      QueryConfig = 1,
      ChangeConfig = 2,
      QueryStatus = 4,
      EnumerateDependents = 8,
      Start = 16,
      Stop = 32,
      PauseContinue = 64,
      Interrogate = 128,
      UserDefinedControl = 256,
      Delete = 65536,
      ReadControl = 131072,
      WriteDac = 262144,
      WriteOwner = 524288,
      Synchronize = 1048576,
      AccessSystemSecurity = 16777216,
      GenericAll = 268435456,
      GenericExecute = 536870912,
      GenericWrite = 1073741824,
      GenericRead = 2147483648
  }
"@

function Get-ServiceAcl {
    [CmdletBinding(DefaultParameterSetName="ByName")]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ParameterSetName="ByName")]
        [string[]] $Name,
        [Parameter(Mandatory=$true, Position=0, ParameterSetName="ByDisplayName")]
        [string[]] $DisplayName,
        [Parameter(Mandatory=$false, Position=1)]
        [string] $ComputerName = $env:COMPUTERNAME
    )

    # If display name was provided, get the actual service name:
    switch ($PSCmdlet.ParameterSetName) {
        "ByDisplayName" {
            $Name = Get-Service -DisplayName $DisplayName -ComputerName $ComputerName -ErrorAction Stop | 
                Select-Object -ExpandProperty Name
        }
    }

    # Make sure computer has 'sc.exe':
    $ServiceControlCmd = Get-Command "$env:SystemRoot\system32\sc.exe"
    if (-not $ServiceControlCmd) {
        throw "Could not find $env:SystemRoot\system32\sc.exe command!"
    }

    # Get-Service does the work looking up the service the user requested:
    Get-Service -Name $Name | ForEach-Object {
        
        # We might need this info in catch block, so store it to a variable
        $CurrentName = $_.Name

        # Get SDDL using sc.exe
        $Sddl = & $ServiceControlCmd.Definition "\\$ComputerName" sdshow "$CurrentName" | Where-Object { $_ }

        try {
            # Get the DACL from the SDDL string
            $Dacl = New-Object System.Security.AccessControl.RawSecurityDescriptor($Sddl)
        }
        catch {
            Write-Warning "Couldn't get security descriptor for service '$CurrentName': $Sddl"
            return
        }

        # Create the custom object with the note properties
        $CustomObject = New-Object -TypeName PSObject -Property ([ordered] @{ Name = $_.Name
                                                                              Dacl = $Dacl
                                                                            })

        # Add the 'Access' property:
        $CustomObject | Add-Member -MemberType ScriptProperty -Name Access -Value {
            $this.Dacl.DiscretionaryAcl | ForEach-Object {
                $CurrentDacl = $_

                try {
                    $IdentityReference = $CurrentDacl.SecurityIdentifier.Translate([System.Security.Principal.NTAccount])
                }
                catch {
                    $IdentityReference = $CurrentDacl.SecurityIdentifier.Value
                }
                
                New-Object -TypeName PSObject -Property ([ordered] @{ 
                                ServiceRights = [ServiceAccessFlags] $CurrentDacl.AccessMask
                                AccessControlType = $CurrentDacl.AceType
                                IdentityReference = $IdentityReference
                                IsInherited = $CurrentDacl.IsInherited
                                InheritanceFlags = $CurrentDacl.InheritanceFlags
                                PropagationFlags = $CurrentDacl.PropagationFlags
                                                                    })
            }
        }

        # Add 'AccessToString' property that mimics a property of the same name from normal Get-Acl call
        $CustomObject | Add-Member -MemberType ScriptProperty -Name AccessToString -Value {
            $this.Access | ForEach-Object {
                "{0} {1} {2}" -f $_.IdentityReference, $_.AccessControlType, $_.ServiceRights
            } | Out-String
        }

        $CustomObject
    }
}

Here's a quick example of using the previous code:


PS > "wuauserv" | Get-ServiceAcl | select -ExpandProperty Access

ServiceRights     : QueryConfig, QueryStatus, EnumerateDependents, Start, 
                    Interrogate, ReadControl
AccessControlType : AccessAllowed
IdentityReference : NT AUTHORITY\Authenticated Users
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

ServiceRights     : QueryConfig, ChangeConfig, QueryStatus, 
                    EnumerateDependents, Start, Stop, PauseContinue, 
                    Interrogate, UserDefinedControl, Delete, ReadControl, 
                    WriteDac, WriteOwner
AccessControlType : AccessAllowed
IdentityReference : BUILTIN\Administrators
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

ServiceRights     : QueryConfig, ChangeConfig, QueryStatus, 
                    EnumerateDependents, Start, Stop, PauseContinue, 
                    Interrogate, UserDefinedControl, Delete, ReadControl, 
                    WriteDac, WriteOwner
AccessControlType : AccessAllowed
IdentityReference : NT AUTHORITY\SYSTEM
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

There's still a lot that can be done with this. I plan to have a post up soon showing how to easily change the ACLs on services as well. One day I'd also like to convert this into a Proxy function for Get-Acl so that the new function can get ACLs for files, folders, registry keys, and services.

Advertisements

Creating Custom ToString Methods

Posted: March 11, 2013 in PowerShell

There are a lot of times when I want to display a collection of objects, and I want to change the way that some of the properties are displayed. As a quick example, I’m going to create a collection of items that each has three properties: a name, a size, and a date. If you want, you can imagine that each item represents a file. We’ll use this data throughout the rest of the post, so make sure you run this section if you want to follow along on your computer:

$SourceData = @"
Name,Size,Date
Name1,890013842,7/7/2001
Name2,328080899,6/10/2001
Name3,567628652,6/22/2008
Name4,558459174,1/15/2003
Name5,433485744,10/26/2011
Name6,409636452,10/23/2002
Name7,265550001,3/6/2008
Name8,300272180,10/12/2010
Name9,2935701,1/16/2003
Name10,659420106,9/10/2001
"@ | ConvertFrom-Csv

$Data = $SourceData | ForEach-Object {
    $Properties = @{ Name = $_.Name
                     Size = [int] $_.Size
                     Date = [datetime] $_.Date
                   }
    New-Object -TypeName PSObject -Property $Properties

} | select Name, Size, Date

This is taking data that I randomly generated contained in a here string, then converting it from a CSV to an object. That object is then passed to the ForEachObject cmdlet, where a new custom PSObject gets created for each line in the original CSV (the Size and Date columns from the CSV are cast into int and datetime objects). All of the newly created objects are piped to the select statement to control the order of the properties. Hashtables don’t have a guaranteed order, and I used a hashtable to add the properties to the new object. PowerShell v3 fixed this by allowing you to create ordered hashtables via the [ordered] type accelerator, but I’m not using that just in case anyone with PowerShell v2 tries to follow along (the [ordered] type accelerator is really cool, though, and I’m glad it was added to v3).

Now, let’s let PowerShell list the contents of the collection using the default formatting:


PS > $Data

Name           Size    Date                  
----           ----    ----                  
Name1     890013842    7/7/2001 12:00:00 AM  
Name2     328080899    6/10/2001 12:00:00 AM 
Name3     567628652    6/22/2008 12:00:00 AM 
Name4     558459174    1/15/2003 12:00:00 AM 
Name5     433485744    10/26/2011 12:00:00 AM
Name6     409636452    10/23/2002 12:00:00 AM
Name7     265550001    3/6/2008 12:00:00 AM  
Name8     300272180    10/12/2010 12:00:00 AM
Name9       2935701    1/16/2003 12:00:00 AM 
Name10    659420106    9/10/2001 12:00:00 AM 

Personally, I don’t like the way that the Size and Date properties are being displayed. The Size (let’s assume that it’s in bytes) is tough to read since there are no separators and the numbers are very large. You can tell that the source of the Date information didn’t contain a time (so all of the DateTime objects are showing 12:00:00 AM). I would like to format the Size to show how big each item is in MB, and I would also like to format the Date to only show the date portion.

There’s more than one way to accomplish this. One way would be to make two new properties on each object that contain the custom formatting. This is what that would look like if we defined $Data with two extra properties:

$Data = $SourceData | ForEach-Object {
    $Properties = @{ Name = $_.Name
                     Size = [int] $_.Size
                     FormattedSize = ("{0:n2} MB" -f ($_.Size / 1mb))
                     Date = [datetime] $_.Date
                     FormattedDate = ([datetime] $_.Date).ToShortDateString()
                   }
    New-Object -TypeName PSObject -Property $Properties

} | select Name, Size, FormattedSize, Date, FormattedDate

PS > $Data | format-table

Name           Size    FormattedSize    Date                      FormattedDate
----           ----    -------------    ----                      -------------
Name1     890013842    848.78 MB        7/7/2001 12:00:00 AM      7/7/2001     
Name2     328080899    312.88 MB        6/10/2001 12:00:00 AM     6/10/2001    
Name3     567628652    541.33 MB        6/22/2008 12:00:00 AM     6/22/2008    
Name4     558459174    532.59 MB        1/15/2003 12:00:00 AM     1/15/2003    
Name5     433485744    413.40 MB        10/26/2011 12:00:00 AM    10/26/2011   
Name6     409636452    390.66 MB        10/23/2002 12:00:00 AM    10/23/2002   
Name7     265550001    253.25 MB        3/6/2008 12:00:00 AM      3/6/2008     
Name8     300272180    286.36 MB        10/12/2010 12:00:00 AM    10/12/2010   
Name9       2935701    2.80 MB          1/16/2003 12:00:00 AM     1/16/2003    
Name10    659420106    628.87 MB        9/10/2001 12:00:00 AM     9/10/2001    

Now you’ve got five columns instead of the original three. You can fix that by using Select-Object to only show the formatted columns:


PS > $Data | select Name, Formatted*

Name      FormattedSize    FormattedDate
----      -------------    -------------
Name1     848.78 MB        7/7/2001     
Name2     312.88 MB        6/10/2001    
Name3     541.33 MB        6/22/2008    
Name4     532.59 MB        1/15/2003    
Name5     413.40 MB        10/26/2011   
Name6     390.66 MB        10/23/2002   
Name7     253.25 MB        3/6/2008     
Name8     286.36 MB        10/12/2010   
Name9     2.80 MB          1/16/2003    
Name10    628.87 MB        9/10/2001    

There’s a big problem with this solution, though: you can’t operate on these fields the way you would expect. Look at what happens when you try to sort or use the Where-Object cmdlet on them:


PS > $Data | sort FormattedDate | select Name, Formatted*

Name      FormattedSize    FormattedDate
----      -------------    -------------
Name4     532.59 MB        1/15/2003    
Name9     2.80 MB          1/16/2003    
Name8     286.36 MB        10/12/2010   
Name6     390.66 MB        10/23/2002   
Name5     413.40 MB        10/26/2011   
Name7     253.25 MB        3/6/2008     
Name2     312.88 MB        6/10/2001    
Name3     541.33 MB        6/22/2008    
Name1     848.78 MB        7/7/2001     
Name10    628.87 MB        9/10/2001    

PS > $Data | where { $_.FormattedSize -gt 1gb } | select Name, Formatted*

Name      FormattedSize    FormattedDate
----      -------------    -------------
Name1     848.78 MB        7/7/2001     
Name2     312.88 MB        6/10/2001    
Name3     541.33 MB        6/22/2008    
Name4     532.59 MB        1/15/2003    
Name5     413.40 MB        10/26/2011   
Name6     390.66 MB        10/23/2002   
Name7     253.25 MB        3/6/2008     
Name8     286.36 MB        10/12/2010   
Name9     2.80 MB          1/16/2003    
Name10    628.87 MB        9/10/2001    

Sort-Object is sorting the strings, so the dates aren’t in the right order. When trying to search for files greater than 1GB, all files are returned, even though none of them are greater than that size (PowerShell is actually converting 1gb into a string and comparing the strings. If I had put 1gb first in the comparison, I would have gotten an error because the FormattedSize objects can’t be converted to numbers).

There’s a better way to handle the displaying problem: replace the ToString() method on each of the objects. In the .NET Framework (which PowerShell is based on), all objects have a ToString() method. When our collection gets displayed to the screen, PowerShell is calling the ToString() method of each of the objects it displays. To define our own version of the method, we’ll use the Add-Member cmdlet. The cool part about this is that the original object is preserved. It will still behave the same as if we didn’t change the way it is being displayed. Take a look:

$Data = $SourceData | ForEach-Object {
    $Properties = @{ Name = $_.Name
                     Size = [int] $_.Size
                     Date = [datetime] $_.Date
                   }
    $NewObject = New-Object -TypeName PSObject -Property $Properties
    $NewObject.Size = $NewObject.Size | Add-Member -MemberType ScriptMethod -Name ToString -Value {
        "{0:n2} MB" -f ($this / 1mb)
    } -Force -PassThru
    $NewObject.Date = $NewObject.Date | Add-Member -MemberType ScriptMethod -Name ToString -Value {
        $this.ToShortDateString()
    } -Force -PassThru

    $NewObject  # Emit the new object
} | select Name, Size, Date

This time around, we save each new PSObject to a variable, $NewObject, and then we use Add-Member to create a new Size and Date object. We have to pass the -Force parameter because the ToString method already exists, and we’re overwriting it. Note that the scriptblock passed to the -Value parameter uses the $this automatic variable to refer to the object. The last thing to note is the use of the -PassThru variable. Here’s another area where PowerShell v3 offers a simplified way to do this, but we’re keeping it in the v2 format for backwards compatibility. The -PassThru parameter is making Add-Member output an object instead of just operating on the object passed to it.

Now let’s take a look at what we can do with the new version of $Data:


PS > $Data | sort Date

Name           Size    Date      
----           ----    ----      
Name2     312.88 MB    6/10/2001 
Name1     848.78 MB    7/7/2001  
Name10    628.87 MB    9/10/2001 
Name6     390.66 MB    10/23/2002
Name4     532.59 MB    1/15/2003 
Name9       2.80 MB    1/16/2003 
Name7     253.25 MB    3/6/2008  
Name3     541.33 MB    6/22/2008 
Name8     286.36 MB    10/12/2010
Name5     413.40 MB    10/26/2011

PS > $Data | where { $_.Size -gt 500mb } | sort size

Name           Size    Date     
----           ----    ----     
Name4     532.59 MB    1/15/2003
Name3     541.33 MB    6/22/2008
Name10    628.87 MB    9/10/2001
Name1     848.78 MB    7/7/2001 

As you can see, we’re still able to sort on the objects, and we can still use Where-Object as if nothing has changed. You can also do the same types of math operations you would be able to do with the original, unmodified objects:


PS > $Data[0].Size.GetType()

IsPublic IsSerial Name   BaseType
-------- -------- ----   --------
True     True     Int32  System.ValueType

PS > $Data[0].Size
848.78 MB

PS > [int] $Data[0].Size
890013842

PS > $Data[0].Size + 1
890013843

PS > $Data[0].Date.GetType()

IsPublic IsSerial Name      BaseType
-------- -------- ----      --------
True     True     DateTime  System.ValueType

PS > $Data[0].Date

Saturday, July 7, 2001 12:00:00 AM

PS > $Data[0].Date.ToString()
7/7/2001

PS > (Get-Date) - $Data[0].Date

Days              : 4264
Hours             : 15
Minutes           : 11
Seconds           : 35
Milliseconds      : 294
Ticks             : 3684642952940613
TotalDays         : 4264.63304738497
TotalHours        : 102351.193137239
TotalMinutes      : 6141071.58823435
TotalSeconds      : 368464295.294061
TotalMilliseconds : 368464295294.061

We’re still not done yet, though. We have introduced a new problem: the original ToString() methods allowed you to do some pretty sophisticated formatting. Take a look at the different arguments that a DateTime object’s ToString() method can take compared to our new ToString() method:


PS > (Get-Date).ToString.OverloadDefinitions
string ToString()
string ToString(string format)
string ToString(System.IFormatProvider provider)
string ToString(string format, System.IFormatProvider provider)
string IFormattable.ToString(string format, System.IFormatProvider formatProvider)
string IConvertible.ToString(System.IFormatProvider provider)

PS > $Data[0].Date.ToString.OverloadDefinitions
System.Object ToString();

Sad, isn’t it? Don’t worry, we can still fix this. We’ll just do our custom formatting if no arguments are supplied to our version of ToString(), and we’ll call the original ToString() when arguments are supplied. Now, you might be wondering how in the world we can call the original ToString() if we’ve overwritten it. That’s actually a very good question. It turns out that PowerShell allows you to get to the original object through a special member called PsBase. That means we can still access the original ToString() method by referencing the PsBase.ToString() method. Here’s what our updated ToString() methods look like:

$Data = $SourceData | ForEach-Object {
    $Properties = @{ Name = $_.Name
                     Size = [int] $_.Size
                     Date = [datetime] $_.Date
                   }
    $NewObject = New-Object -TypeName PSObject -Property $Properties
    $NewObject.Size = $NewObject.Size | Add-Member -MemberType ScriptMethod -Name ToString -Value {
        if ($args.Count -eq 0) {
            "{0:n2} MB" -f ($this / 1mb)
        }
        else {
            $this.PsBase.ToString($args)
        }
    } -Force -PassThru
    $NewObject.Date = $NewObject.Date | Add-Member -MemberType ScriptMethod -Name ToString -Value {
        if ($args.Count -eq 0) {
            if ((Get-Culture).Name -eq "en-US") { $ShortDatePattern = "MM/dd/yyyy" }
            else { $ShortDatePattern = (Get-Culture).DateTimeFormat.ShortDatePattern }

            # No arguments were passed, so convert date to the ShortDatePattern
            # If I wasn't changing the 'en-US' culture to have a custom ShortDatePattern,
            # the previous if/else statements wouldn't be needed, and the next line could
            # use the .ToShortDateString() method instead
            $this.PsBase.ToString($ShortDatePattern)
        }
        else {
            $this.PsBase.ToString($args)
        }
    } -Force -PassThru

    $NewObject  # Emit the new object
} | select Name, Size, Date

I made one other change to how the date is being displayed for the US culture: I made the dates always have two digits for the month and day. Let’s see how it looks:


PS > $Data

Name           Size    Date      
----           ----    ----      
Name1     848.78 MB    07/07/2001
Name2     312.88 MB    06/10/2001
Name3     541.33 MB    06/22/2008
Name4     532.59 MB    01/15/2003
Name5     413.40 MB    10/26/2011
Name6     390.66 MB    10/23/2002
Name7     253.25 MB    03/06/2008
Name8     286.36 MB    10/12/2010
Name9       2.80 MB    01/16/2003
Name10    628.87 MB    09/10/2001

It looks the same as before (except that months and dates are always two digits now), as it should. We should now be able to still get the old ToString() functionality out of the objects now:


PS > $Data[0].Size.ToString()
848.78 MB

PS > $Data[0].Size.ToString("n0")
890,013,842

PS > $Data[0].Date.ToString()
07/07/2001

PS > $Data[0].Date.ToString("yyyy-MM-dd")
2001-07-07

This might seem like a lot of work just to change the way that data is displayed. It is incredibly powerful when you’re creating custom functions that will return data to the user, though. This post is already much longer than I anticipated, so I’ll have to save how to have PowerShell take care of adding custom members (including our custom ToString() method) automatically via the extended type system for another day.

Welcome!

Posted: March 11, 2013 in General

Welcome! It looks like I’m finally making a blog about PowerShell. I’ve been meaning to do this for almost a year, but I’ve always managed to put it off. A few weeks ago, my friend Mike Robbins contacted me about helping him start a virtual PowerShell User Group in Mississippi. I agreed to do what I could to help him out, and we’ve been in contact ever since (by the way, our first meeting is this Tuesday, March 12, 2013). Over the last few weeks, he managed to talk me into getting off my lazy behind and starting this. I’ve never had a blog before, so please bear with me as I try to figure out how to run this thing. I can’t make any promises, but I’ll try to have semi-regular posts that are interesting and useful. If you have any feedback for me, please leave a comment.

Thanks,
Rohn