I want to generate the following table:
AAA BBB CCC
--- --- ---
10 10 10
10 10 10
10 10 10
10 10 10
10 10 10
So I write the following code using a foreach
loop to generate the column names:
$property = @('AAA', 'BBB', 'CCC') | foreach {
@{ name = $_; expression = { 10 } }
}
@(1..5) | select -Property $property
But I get the following error saying the name
is not a string
:
select : The "name" key has a type, System.Management.Automation.PSObject, that is not valid; expected type is System.String.
At line:4 char:11
+ @(1..5) | select -Property $property
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Select-Object], NotSupportedException
+ FullyQualifiedErrorId : DictionaryKeyIllegalValue2,Microsoft.PowerShell.Commands.SelectObjectCommand
To get the code work, I have to convert the $_
to string
like below:
$property = @('AAA', 'BBB', 'CCC') | foreach {
@{ name = [string]$_; expression = { 10 } }
}
@(1..5) | select -Property $property
Or like below:
$property = @('AAA', 'BBB', 'CCC') | foreach {
@{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name = [string]$_.name }
@(1..5) | select -Property $property
The question is: the $_
is already a string
. Why do I have to convert it to string
again? And why select
thinks that the name
is PSObject
?
To confirm that it's already a string
, I write the following code to print the type of name
:
$property = @('AAA', 'BBB', 'CCC') | foreach {
@{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name.GetType() }
The following result confirms that it's already a string
:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
True True String System.Object
True True String System.Object
I know that there are many other easier ways to generate the table. But I want to understand why I have to convert a string
to string
to make the code work, and why select
doesn't think that the string
is a string
. For what it's worth, my $PSVersionTable.PSVersion
is:
Major Minor Build Revision
----- ----- ----- --------
5 1 18362 1474
You're seeing the unfortunate effects of incidental, normally invisible [psobject]
wrappers PowerShell uses behind the scenes.
In your case, because the input strings are supplied via the pipeline, they get wrapped in and stored as [psobject]
instances in your hashtables, which is the cause of the problem.
The workaround - which is neither obvious nor should it be necessary - is to discard the wrapper by accessing .psobject.BaseObject
:
$property = 'AAA', 'BBB', 'CCC' | ForEach-Object {
@{ name = $_.psobject.BaseObject; expression = { 10 } }
}
1..5 | select -Property $property
Note:
In your case, a simpler alternative to .psobject.BaseObject
(see the conceptual about_Intrinsic Members help topic) would have been to call .ToString()
, given that you want a string.
To test a given value / variable for the presence of such a wrapper, use -is [psobject]
; with your original code, the following yields $true
, for instance:
$property[0].name -is [psobject]
[pscustomobject]
instances, where it is always $true
(custom objects are in essence [psobject]
instances without a .NET base objects - they only have dynamic properties).That the normally invisible [psobject]
wrappers situationally, obscurely result in behavioral differences is arguably a bug and the subject of GitHub issue #5579.
Simpler and faster alternative, using the .ForEach()
array method:
$property = ('AAA', 'BBB', 'CCC').ForEach({
@{ name = $_; expression = { 10 } }
})
1..5 | select -Property $property
Unlike the pipeline, the .ForEach()
method does not wrap $_
in [psobject]
, so the problem doesn't arise and no workaround is needed.
Using the method is also faster, although note that, unlike the pipeline, it must collect all its input in memory up front (clearly not a problem with an array literal).