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?
$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.
[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.
[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.
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.