78

First, as this leads to my question, I'll start by noting that I've worked with XML a fair bit in PowerShell, and like how I can read data from XML files, quickly, into arrays of custom objects. For example, if I had the following XML file:

<stuff>
 <item name="Joe" age="32">
  <info>something about him</info>
 </item>
 <item name="Sue" age="29">
  <info>something about her</info>
 </item>
 <item name="Cat" age="12">
  <info>something else</info>
 </item>
</stuff>

And if I read it in simply, like this:

[xml]$myxml = Get-Content .\my.xml

Then I can get to an array of my items like this:

[array]$myitems = $myxml.stuff.Item
$myitems

name   age  info
----   ---  ----
Joe    32   something about him
Sue    29   something about her
Cat    12   something else

So, now my question:

How can I create a similar structure of an array of custom objects, and initialize them in my script, without reading a file?

I can do lots of looping and/or lots of creating/initializing individual objects, and then add to an array one at a time...

But it seems there should be a way to perform this creation/initialization in a simpler way. Note that the key here, is that my custom objects have more than two elements (otherwise, I'd have used a hash).

I've even looked at creating a big string of XML, and using Select-XML, but I just couldn't get the syntax right (if that was even the right road to be heading down).

abatishchev
  • 98,240
  • 88
  • 296
  • 433
SteveDJ
  • 993
  • 2
  • 7
  • 10
  • 2
    So I think what you are asking is how you can take an array of objects you created on the fly and display them in neat tables like you did with the xml above? – Joel Smith Jun 27 '13 at 21:57
  • It's too bad your requirements include not reading from a file. Import-Csv yields an array of pscustom objects. – Walter Mitty Dec 29 '18 at 11:17

9 Answers9

124

I'd do something along these lines:

$myitems =
@([pscustomobject]@{name="Joe";age=32;info="something about him"},
[pscustomobject]@{name="Sue";age=29;info="something about her"},
[pscustomobject]@{name="Cat";age=12;info="something else"})

Note that this only works in PowerShell 3, but since you did not mention the version in your question I'm assuming this does not matter for you.

Update

It has been mentioned in comments that if you do the following:

$younger = $myitems | Where-Object { $_.age -lt 20 } 
Write-Host "people younger than 20: $($younger.Length)" 

You won't get 1 as you might expect. This happens when a single pscustomobject is returned. Now this is not a problem for most of other objects in PowerShell, because they have surrogate properties for Length and Count. Unfortunately pscustomobject does not. This is fixed in PowerShell 6.1.0. You can work around this by using operator @():

$younger = @($myitems | Where-Object { $_.age -lt 20 })

For more background see here and here.

Update 2

In PowerShell 5 one can use Classes to acheive similar functionality. For example you can define a class like this:

class Person {
    [string]$name
    [int]$age
    [string]$info; `
`
    Person(
    [string]$name,
    [int]$age,
    [string]$info
    ){
        $this.name = $name
        $this.age = $age
        $this.info = $info
    }
}

Backticks here are so that you could copy and paste it to the command line, they are not required in a script. Once the class is defined you can the create an array the usual way:

$myitems =@([Person]::new("Joe",32,"something about him"),
[Person]::new("Sue",29,"something about her"),
[Person]::new("Cat",12,"something else"))

Note that this way does not have the drawback mentioned in the previous update, even in PowerShell 5.

Update 3

You can also intialize a class object with a hashtable, similar to the first example. For that you need to make sure that a default contructor defined. If you do not provide any constructors, one will be created for you, but if you provide a non-default one, default constructor won't be there and you will need to define it explicitly. Here is an example with default constructor that is auto-created:

class Person {
    [string]$name
    [int]$age
    [string]$info;
}

With that you can:

$person = @([Person]@{name='Kevin';age=36;info="something about him"}
[Person]@{name='Sue';age=29;info="something about her"}
[Person]@{name='Cat';age=12;info="something else"})

This is a bit more verbose, but also a bit more explicit. Thanks to @js2010 for pointing this out.

Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
  • Ah, I was missing the [pscustomobject] cast - this does look to be my solution... except... on one machine where I need to run my script, I'm hitting something new: `Error: Cannot convert value to type "...InternalPSCustomObject". Only core types are supported in this language mode. ` I don't see this on my other machines, so maybe it isn't related, but some other config issue? (it is PS 3.0 - but it is an ARM processor running Windows 8 -- ok, it's a Surface RT). – SteveDJ Jun 28 '13 at 14:18
  • P.S. And oddly, my initial XML example works fine on the Surface RT, while your answer gives me that error (I **know** they are different, just odd that the creation of custom objects works one way, but not the other??) – SteveDJ Jun 28 '13 at 16:45
  • Did not have any experience with surface RT, sorry. I'd imagine that it has cut down version of everything including powershell. Anyway if my answers addresses your original question (which never mentioned Windows RT) please consider accepting it. – Andrew Savinykh Jun 28 '13 at 20:15
  • @user2529733 also see here http://stackoverflow.com/questions/13504499/windows-rt-powershell-permissiondenied-on-new-object – Andrew Savinykh Jun 28 '13 at 20:17
  • I love the succinct syntax of this. necro-post, I know, but it's just what I was looking for. – Larry Smith Jul 02 '18 at 21:00
  • Run the following code after the code above: ``` $older = $myitems | Where-Object { $_.age -gt 20 }; Write-Host "people older than 20: $($older.Length)"; $younger = $myitems | Where-Object { $_.age -lt 20 }; Write-Host "people younger than 20: $($younger.Length)"; [pscustomobject[]]$younger2 = $younger; Write-Host "people younger than 20 (array): $($younger2.Length)"; ``` See my answer for a more-correct solution. (the ';'s are just for readability in the comment... – Kody Feb 19 '19 at 00:44
  • 1
    @Kody, thank you for spotting this, updated the answer. – Andrew Savinykh Feb 19 '19 at 01:20
  • You can initialize a class by casting a hashtable. – js2010 Jul 18 '19 at 14:18
42

Here is a concise way to initialize an array of hashtables in PowerShell.

> $body = @( @{ Prop1="1"; Prop2="2"; Prop3="3" }, @{ Prop1="1"; Prop2="2"; Prop3="3" } )
> $body

Name                           Value
----                           -----
Prop2                          2
Prop1                          1
Prop3                          3
Prop2                          2
Prop1                          1
Prop3                          3  
Shaun Luttin
  • 133,272
  • 81
  • 405
  • 467
  • This changed my way from `New-Object PsObject -Property @{ Prop1 = "1"; Prop2 = "2"; etc... }` to `[PsObject]@{ Prop1 = "1"; Prop2 = "2"; etc... }`, thanks! :) – Kody Feb 19 '19 at 01:02
  • 3
    This is of course is an array of hashtables, not an array of _custom objects._ – Andrew Savinykh Mar 03 '21 at 19:10
  • awesome, it works to create an array to initialize my ARM Key Vault with all secrets in Azure `$body = @( @{ SecretName="A"; SecretValue="2" }, @{ SecretName="B"; SecretValue=4 } )` – Luis Raúl Espinoza Barboza Mar 18 '21 at 19:48
31

Maybe you mean like this? I like to make an object and use Format-Table:

> $array = @()
> $object = New-Object -TypeName PSObject
> $object | Add-Member -Name 'Name' -MemberType Noteproperty -Value 'Joe'
> $object | Add-Member -Name 'Age' -MemberType Noteproperty -Value 32
> $object | Add-Member -Name 'Info' -MemberType Noteproperty -Value 'something about him'
> $array += $object
> $array | Format-Table

Name                                                                        Age Info
----                                                                        --- ----
Joe                                                                          32  something about him

This will put all objects you have in the array in columns according to their properties.

Tip: Using -auto sizes the table better

> $array | Format-Table -Auto

Name Age Info
---- --- ----
Joe   32 something about him

You can also specify which properties you want in the table. Just separate each property name with a comma:

> $array | Format-Table Name, Age -Auto

Name Age
---- ---
Joe   32
Nick Kavadias
  • 7,542
  • 2
  • 37
  • 46
Joel Smith
  • 857
  • 9
  • 20
25

The simplest way to initialize an array

Create array

$array = @()

Create your header

$line = "" | select name,age,phone

Fill the line

$line.name = "Leandro"
$line.age = "39"
$line.phone = "555-555555"

Add line to $array

$array += $line

Result

$array

name                                                     age                                                      phone
----                                                     ---                                                      -----
Leandro                                                  39                                                       555-555555
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Leandro Cascão
  • 351
  • 3
  • 4
  • 1
    @BenSewards Nice huh? Very easy to do. Very logical. Very clean. When I'm pulling data, I like to build/import an array, do something that adds a column or modifies an existing one, and output into a new array like this. Then when I'm done, I can export all the results into a CSV, very clean. – Jeter-work Dec 18 '18 at 21:32
10

Here is a more concise version of the accepted answer which avoids repeating the NoteProperty identifiers and the [pscustomobject]-cast:

$myItems =  ("Joe",32,"something about him"), ("Sue",29,"something about her")
            | ForEach-Object {[pscustomobject]@{name = $_[0]; age = $_[1]; info = $_[2]}}

Result:

> $myItems

name           age         info
----           ---         ----
Joe            32          something about him
Sue            29          something about her
davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • 1
    It works. but what if I only have 1 item in the array? – Yiping Apr 25 '17 at 07:41
  • 2
    @Yiping: you have to use the comma operator properly, see [here](http://stackoverflow.com/questions/11138288/how-to-create-array-of-arrays-in-powershell). Basically, it's `$myItems = , ("Sue",29,"something about her") | ...`. – davidhigh Apr 25 '17 at 08:32
3

Given the data above, this is how I would do it:

# initialize the array
[PsObject[]]$people = @()

# populate the array with each object
$people += [PsObject]@{ Name = "Joe"; Age = 32; Info = "something about him" }
$people += [PsObject]@{ Name = "Sue"; Age = 29; Info = "something about her" }
$people += [PsObject]@{ Name = "Cat"; Age = 12; Info = "something else" }

The below code will work even if you only have 1 item after a Where-Object:

# display all people
Write-Host "People:"
foreach($person in $people) {
    Write-Host "  - Name: '$($person.Name)', Age: $($person.Age), Info: '$($person.Info)'"
}

# display with just 1 person (length will be empty if using 'PSCustomObject', so you have to wrap any results in a '@()' as described by Andrew Savinykh in his updated answer)
$youngerPeople = $people | Where-Object { $_.Age -lt 20 }
Write-Host "People younger than 20: $($youngerPeople.Length)"
foreach($youngerPerson in $youngerPeople) {
    Write-Host "  - Name: '$($youngerPerson.Name)'"
}

Result:

People:
  - Name: 'Joe', Age: 32, Info: 'something about him'
  - Name: 'Sue', Age: 29, Info: 'something about her'
  - Name: 'Cat', Age: 12, Info: 'something else'
People younger than 20: 1
  - Name: 'Cat'
Kody
  • 905
  • 9
  • 19
  • The real reason for what you are observing is that `PSCustomObject` does not have surropgate properties for `Count` and `Length` as described [here](https://github.com/nightroman/PowerShellTraps/tree/master/Basic/Count-and-Length/PSCustomObject) and [here](https://github.com/nightroman/PowerShellTraps/tree/master/Basic/Number-of-returned-objects). You are using hashtable instead of what was asked in the question, thus side-stepping this issue. Unfortunately hashtable usage is not always desirable. – Andrew Savinykh Feb 19 '19 at 01:23
  • `If you want to test it with PSCustomObject it will still work as desired.`. No it does not because it cannot as I explained above. Test it yourself and see. I did. – Andrew Savinykh Feb 27 '19 at 23:02
  • You're right. I just verified that unless "Cat" is a `PSObject`, it will not work. Why would you use `PSCustomObject` if you can use `PSObject` then? Furthermore, why would Where-Object behave like Select-Object in this scenario? Where is not a find operation, it's always a collection filtering operation. – Kody Feb 28 '19 at 20:54
  • I've removed the misinformation in the post/comments to not confuse others. – Kody Feb 28 '19 at 21:06
2

Use a "Here-String" and cast to XML.

[xml]$myxml = @"
<stuff>
 <item name="Joe" age="32">
  <info>something about him</info>
 </item>
 <item name="Sue" age="29">
  <info>something about her</info>
 </item>
 <item name="Cat" age="12">
  <info>something else</info>
 </item>
</stuff>
"@

[array]$myitems = $myxml.stuff.Item

$myitems
AMissico
  • 21,470
  • 7
  • 78
  • 106
1

A little variation on classes. Initialize it with hashtables.

class Point { $x; $y }

$a = [Point[]] (@{ x=1; y=2 },@{ x=3; y=4 })

$a

x y        
- -          
1 2
3 4

$a.gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Point[]                                  System.Array

$a[0].gettype()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    Point                                    System.Object
js2010
  • 23,033
  • 6
  • 64
  • 66
0

I had to create an array of a predefined type, and I successfully did as follows:

[System.Data.DataColumn[]]$myitems = ([System.Data.DataColumn]("col1"), 
                [System.Data.DataColumn]("col2"),  [System.Data.DataColumn]("col3"))
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ravi Anand
  • 5,106
  • 9
  • 47
  • 77