Search code examples
powershellpropertiesalias

Adding alias properties (AliasProperty members)


I just wonder why this works:

$s = "hi there"
$s = Add-Member -InputObject $s -MemberType AliasProperty -Name Length2 -Value Length -PassThru
$s | gm -MemberType AliasProperty #the property is there
$s.Length2 #8

means: the aliasproperty Length2 is there, but this doesn't work:

$s = dir
$s = Add-Member -InputObject $s -MemberType AliasProperty -Name Length2 -Value Length -PassThru
$s | gm -MemberType AliasProperty #the property is not there

means: there is no aliasproperty Length2, but suprisingly enough:

$s.Length2 #458

works just fine?


Solution

  • $s = dir

    Given that dir is a built-in alias for Get-ChildItem, this typically results in an array being stored in $s, i.e. whenever two or more child items (files or subdirectories) are present.[1]

    $s = Add-Member -InputObject $s ...

    Due to use of Add-Member's -InputObject parameter, you're adding the alias property (as an ETS (Extended Type System) member) to the array itself.

    $s | gm -MemberType AliasProperty

    By contrast, due to using the pipeline here, which enumerates arrays, it is the elements of the array stored in $s that are being sent to Get-Member (whose built-in alias is gm) - and these do not have your alias property.

    $s.Length2 #458

    Here you're accessing the alias property directly on the array, which therefore works.


    As for your first attempt (which worked as expected):

    $s = "hi there"
    $s = Add-Member -InputObject $s -MemberType AliasProperty -Name Length2 -Value Length -PassThru
    • Here $s is a scalar (single object), namely a [string] instance, and you're attaching the alias property directly to it.

      • As an aside: See the next section for why avoiding custom members to [string] instances is best avoided.
    • Sending a scalar through the pipeline ($s | gm -MemberType AliasProperty) sends it as-is, so Get-Member (gm) was able to find its .Length2 alias property.


    Optional reading: Why attaching ETS (Extended Type System) members to instances of [string] and .NET value types is best avoided:

    Note:

    • The following recommendation applies to ETS members of any type that Add-Member is capable of adding to instances of .NET types, notably also to the more common NoteProperty members.

    • See the next section for possibly bypassing the problem via type-level ETS members.

    Avoid attaching ETS instance members in the following cases:

    • to [string] instances, because they require an invisible [psobject] wrapper that is easily lost, such as during string operations and strongly-typed parameter binding, resulting in loss of the ETS members; e.g.:

      $s = "hi there"
      
      # Add a .Length2 alias property that refers to .Length
      $s = Add-Member -InputObject $s -MemberType AliasProperty -Name Length2 -Value Length -PassThru -Force
      
      # OK: access the property directly on the decorated instance.
      $s.Length2 # -> 8, same as $s.Length
      
      # LOSS OF THE PROPERTY due to string operation.
      $s = $s -replace '$', '!'
      $s.Length2 # !! Property was LOST in the -replace operation.
      
      # LOSS OF THE PROPERTY during strongly typed parameter binding
      $s = Add-Member -InputObject $s -MemberType AliasProperty -Name Length2 -Value Length -PassThru -Force
      & { param([string] $str) $str.Length2 } $s
      
    • to instances of .NET value types (e.g, [int] (System.Int32); call .IsValueType on a type to check) for similar reasons: they too can be lost during strongly-typed parameter binding and, conversely - because PowerShell internally caches boxed [int] instances from 0 to 100, inclusively - it can make the ETS member surface unexpectedly when these numbers are used as literals.

      $n = 42
      
      # Add a .Foo instance property of type NoteProperty with value 'Bar'
      $n = Add-Member -InputObject $n -MemberType NoteProperty -Name  Foo -Value Bar -PassThru
      
      # OK: access the property directly on the decorated instance.
      $n.Foo # -> 'Bar'
      
      # LOSS OF THE PROPERTY during strongly typed parameter binding
      & { param([int] $num) $num.Foo } $n
      
      # UNEXPECTED SURFACING OF THE PROPERTY in *integer literals*,
      # because the value is between 0 and 100.
      [int] $nCopy = 42
      $nCopy.Foo # !! -> 'Bar'
      

    Note:

    • It is the necessity of an invisible [psobject] wrapper that requires the use of -PassThru in the Add-Member calls above, along with re-assigning the result to the original variable ($s = Add-Member -PassThru -InputObject $s ...)

    • By contrast, this invocation form is not necessary for decorating instance of .NET reference types other than [string], and such instances aren't affected by the problems above.


    Optional reading: Instance-level vs. type-level ETS members:

    ETS members can also be defined at the type level (rather than at the instance level, i.e. for a particular object only), namely via Update-TypeData, which is often preferable, though not always called for / possible.

    • In the case of an AliasProperty member, a type-level definition is the better choice, given that all instances of the given type then automatically have this property:

      # Add an alias property 'Length2' to the [string] (System.String) *type*
      Update-TypeData -TypeName System.String -MemberType AliasProperty -MemberName Length2 -Value Length
      
      # Now every string instance has this property.
      'foo'.Length2 # -> 3
      
    • In the case of an instance-specific NoteProperty member value, a type-level definition as an alternative is only an option if the value can be derived from the instance's type-native properties, via a ScriptProperty member, i.e., a property whose value is dynamically calculated, via a script block ({ ... }) that can act on the instance at hand via the automatic $this variable; e.g.:

      # Add a `.MaxIndex` script property that is the string's
      # length - 1, i.e. the index of the last char. in the string.
      Update-TypeData -TypeName System.String -MemberType ScriptProperty -MemberName MaxIndex -Value { $this.Length - 1 }
      
      # Now every string instance has this - dynamically calculated - property.
      'foo'.MaxIndex # -> 2
      'food'.MaxIndex # -> 3
      

    [1] Specifically, it is an [object[]] array containing System.IO.FileInfo and/or System.IO.DirectoryInfo instances.