Preserving order when you can't format right - powershell

I'm trying to create a function to document some settings in a word doc and to make the information in the document easy to read I need to preserve the order of the properties of the original object. Unfortunately this means I can't 'format right'.
$o = [PSCustomObject]#{
number = 1
fruit = 'Orange'
Clothing = 'Shirt'
Colour = 'blue'
}
$p = $o | get-member -type NoteProperty | Select-Object -ExpandProperty Name
foreach ($n in $p) {
Write-Output "$n $($o.$n)" #this line is actually function to write a line into the word doc taking $n and $($o.$n) as parameters
}
The part of the code which has the issue is '$o | get-member -type NoteProperty' as Powershell reorders the list at this point. I'm not sure that my approach is correct.
This function will actually be a recursive function to walk an object tree and output the results. Any ideas on a new approach or to correct my original approach would be very welcome.

Try it this way:
$p = $o.psobject.Properties | select -ExpandProperty Name

I don't understand what you mean by
format right
But since PowerShell 3 you can create ordered hashtables:
[ordered]#{
number = 1
fruit = 'Orange'
Clothing = 'Shirt'
Colour = 'blue'
}

Related

How to to have an array as a return type on a custom PSObject

I've got the following:
# Compare the 2 lists and return the ones that exist in the top and children (meaning they're redundant).
$redundantUsers = $usersAssignedToThisGroup | where { $usersAssignedToGroupsInThisGroup -contains $_ }
# Build the results to output
$results += New-Object PSObject -property #{
group = $group.name #group assigned out of scope
users = #($redundantUsers)
}
I'm expecting to be able to call my script like:
$users = ./MyScript -myParam "something" | Select users
Then I'm expecting to be able to type $users[1] in the console and get the first element of that array.
But instead, I'm having to do $users[0].users[1].
How do I change my data and or call to do what I want?
I'm essentially just trying to let the script return the data in a useful way.

Export data in the order it was added - PowerShell Export-Csv

I have this code
function get-data()
{
$rec=[PSCustomObject]#()
$DLGP = "" | Select "Name","Grade","Score"
foreach($record in $data)
{
$DLGP.Name=$record.name
$DLGP.Grade=$record.grade
$DLGP.Score=$record.score
$rec += $DLGP
}
return $rec
}
$mydata=get-data
$mydata | Export-Csv -Path $outputPath -NoTypeInformation
The problem is, the data is not exported in the order that I have added it to $rec
How can I get it exported in the order it was added?
Without even using your function, a simple
$mydata = $data | select Name,Grade,Score
would yield the desired result.
Your function is IMO overcomplicated:
$Data = #"
Name,Grade,Score,Dummy
Anthoni,A,10,v
Brandon,B,20,x
Christian,C,30,y
David,D,40,z
"# | ConvertFrom-Csv
function get-data() {
ForEach($record in $data) {
[PSCustomObject]#{
Name =$record.name
Grade=$record.grade
Score=$record.score
}
}
}
$mydata=get-data
$mydata # | Export-Csv -Path $outputPath -NoTypeInformation
Returns here the pretty same order:
Name Grade Score
---- ----- -----
Anthoni A 10
Brandon B 20
Christian C 30
David D 40
LotPings' helpful answer provides effective solutions.
As for what you tried:
By constructing only a single [pscustomobject] instance outside the loop:
$DLGP = "" | Select "Name","Grade","Score"
and then only updating that one instance's properties in each loop iteration:
$DLGP.Name=$record.name
# ....
you effectively added the very same [pscustomobject] instance multiple times to the result array instead of creating a distinct object in each iteration.
Since the same object was being updated repeatedly, that object ended up having the properties of the last object in the input collection, $data.
As an aside:
[PSCustomObject] #() is effectively the same as #(): the [PSCustomObject] is ignored and you get an [object[]] array.
To type the array as one containing [PSCustomObject] instances, you'd have to use an array-typed cast: [PSCustomObject[]] #().
However, given that instances of any type can be cast to [PSCustomObject] - which is really the same as [psobject] - this offers no type safety and no performance benefit.
Also, since your $rec variable isn't type-constrained (it is defined as $rec = [<type>] ... rather than [<type>] $rec = ...) and you're using += to "add to" the array (which invariably requires creation of a new instance behind the scenes), $rec would revert to an [object[]] array after the first += operation.

In Powershell, is there a better way to store/find data in an n-dimensional array than a custom object

I find myself continually faced with the need to store mixed-type data in some kind of a structure for later lookup.
For a recent example, I am performing data migration and I will store the old UUID, new UUID, source environment, target environment, and schema for an unknown number of entries.
I have been meeting this need by creating an array and inserting System.Objects with NoteProperty members for each of the columns of data.
This strikes me as a very clumsy approach but I feel like I may be limited by Powershell's functionality. If I need to, for example, locate all entries that used a particular schema, I write a foreach loop that sticks each entry with a matching schema name in a whole new array that I can return. I would really like the ability to more easily search for all objects that contain a member matching a particular value, modify existing members, etc.
Is there a better built-in data structure that will suit my needs, or is creating a custom object the right thing to do?
For reference, I'm doing something like this to create my structure:
$objectArray= #();
foreach(thing to process){
$tempObj = New-Object System.Object;
$tempObj | Add-Member -MemberType NoteProperty -Name "membername" -Value xxxxx
....repeat for each member...
$objectArray += $tempObj
}
If I need to find something in it, I then have to:
$matchingObjs = #()
foreach ($obj in $objectArray){
if($obj.thing -eq value){$matchingObjs += $obj}
}
This really sucks and I know there has to be a more elegant way. I'm still fairly new to powershell so I don't know what utilities it has to help me. I'm using v5.
With PowerShell 3.0 you could use a [PSCustomObject], here's an article on the different object creation methods.
Also setting the array equal to the output of the foreach loop will be more efficient than repeatedly recreating an array with +=.
$objectArray = foreach ($item in $collection) {
[pscustomobject]#{
"membername" = "xxxxx"
}
}
The Where-Object cmdlet or the .where() method looks like what you need in your second loop.
$matchingObjs = $objectArray | Where-Object {$_.thing -eq "value"}
It also sounds like you could use Where-Object/.where() to filter the initial data and just create an object which matches what you are looking for. For example:
$matchingObjs = $InputData |
Where-Object {$_.thing -eq "value"} |
ForEach-Object {
[pscustomobject]#{
"membername" = xxxxx
}
}
If your data can be expressed as key value pairs, then a hashtable will be the most efficient, see about_Hash_Tables for more info.
There is no built-in way to do what you are asking. One way is to segment your data into separate hashtables so you can do easy lookups by a common key, say the ID.
# Create a hastable for the IDs
$ids = #{};
foreach(thing to process){
$ids.Add($uid, 'Value')
}
# Find the $uid exists
$keyExists = $ids.Keys -Contains $uid
# Find value of stored for $uid
$keyValue = $ids[$uid]
As a side note, you don't have to create Syste.Object, you can simple do this:
$objectArray = #();
gci | % {
$objectArray += #{
'Key1' = 'Value 1'
'Key2' = 'Value 2'
}
}
If you need to compare complex objects, you can build them with #{} and then use Compare-Object on the two objects, just another idea.
For example, this will get a file listing of two different directories, and tell me what file exists or doesn't exist between the two directories:
$packages = (gci $boxStarterRepo -Recurse *.nuspec | Select-Object -ExpandProperty Name) -replace '.nuspec', ''
$packages += (gci $boxStarterPrivateRepo -Recurse *.nuspec | Select-Object -ExpandProperty Name) -replace '.nuspec', ''
$packages = $packages | Sort-Object
Compare-Object $packages $done

Powershell: Child Property Deflation

Not sure if "deflating" is the right term, but I was wondering if there's a nicer way to specify that you only want to return a selection of an object's children's properties whilst using Select-Object? I'm using Powershell v5.
Here's a fully working example - the $test variable is hard coded here for demonstration purposes; in my use-case, the $test array is being returned from an API. I want to remove the name property from the testChild object during the projection.
The only way I could figure out how to do this is by using a foreach loop whilst piping into $result, then re-creating each child object and setting only the required properties, but it feels like powershell should have nicer way to do this.
$test = #(
[PSCustomObject]#{
testProp = "test1"
testChild = [PSCustomObject]#{
id = 1
name = "test name 1"
}
},
[PSCustomObject]#{
testProp = "test2"
testChild = [PSCustomObject]#{
id = 2
name = "test name 2"
}
}
)
$result = $test | select testProp, testChild | % {
$_.testChild = [PSCustomObject]#{
id = $_.testChild.id
}
$_
}
$result
This outputs:
testProp testChild
-------- ---------
test1 #{id=1}
test2 #{id=2}
Which is exactly what I want. I'd be nice if I could do something like this:
$result = $test | select testProp, testChild.id
Bearing in mind that I do not want flat properties returned - I want the original object with some properties removed / only the properties I specify included.
You can do this with Select-Object:
$result = $test | select testProp, #{
Name = 'id'
Expression = { $_.testChild.id }
}
Basically, Select-Object allows you to define your own properties, where the value is determined by an expression (a [ScriptBlock]), so you can run any code you want to determine the value. Within the block, $_ refers to the "current object" the same way it would in a ForEach-Object call.

Powershell Multidimensional Arrays

I have a way of doing Arrays in other languagues like this:
$x = "David"
$arr = #()
$arr[$x]["TSHIRTS"]["SIZE"] = "M"
This generates an error.
You are trying to create an associative array (hash). Try out the following
sequence of commands
$arr=#{}
$arr["david"] = #{}
$arr["david"]["TSHIRTS"] = #{}
$arr["david"]["TSHIRTS"]["SIZE"] ="M"
$arr.david.tshirts.size
Note the difference between hashes and arrays
$a = #{} # hash
$a = #() # array
Arrays can only have non-negative integers as indexes
from powershell.com:
PowerShell supports two types of multi-dimensional arrays: jagged arrays and true multidimensional arrays.
Jagged arrays are normal PowerShell arrays that store arrays as elements. This is very cost-effective storage because dimensions can be of different size:
$array1 = 1,2,(1,2,3),3
$array1[0]
$array1[1]
$array1[2]
$array1[2][0]
$array1[2][1]
True multi-dimensional arrays always resemble a square matrix. To create such an array, you will need to access .NET. The next line creates a two-dimensional array with 10 and 20 elements resembling a 10x20 matrix:
$array2 = New-Object 'object[,]' 10,20
$array2[4,8] = 'Hello'
$array2[9,16] = 'Test'
$array2
for a 3-dimensioanl array 10*20*10
$array3 = New-Object 'object[,,]' 10,20,10
To extend on what manojlds said above is that you can nest Hashtables. It may not be a true multi-dimensional array but give you some ideas about how to structure the data. An example:
$hash = #{}
$computers | %{
$hash.Add(($_.Name),(#{
"Status" = ($_.Status)
"Date" = ($_.Date)
}))
}
What's cool about this is that you can reference things like:
($hash."Name1").Status
Also, it is far faster than arrays for finding stuff. I use this to compare data rather than use matching in Arrays.
$hash.ContainsKey("Name1")
Hope some of that helps!
-Adam
Knowing that PowerShell pipes objects between cmdlets, it is more common in PowerShell to use an array of PSCustomObjects:
$arr = #(
[PSCustomObject]#{Name = 'David'; Article = 'TShirt'; Size = 'M'}
[PSCustomObject]#{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
Or for older PowerShell Versions (PSv2):
$arr = #(
New-Object PSObject -Property #{Name = 'David'; Article = 'TShirt'; Size = 'M'}
New-Object PSObject -Property #{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
And grep your selection like:
$arr | Where {$_.Name -eq 'David' -and $_.Article -eq 'TShirt'} | Select Size
Or in newer PowerShell (Core) versions:
$arr | Where Name -eq 'David' | Where Article -eq 'TShirt' | Select Size
Or (just get the size):
$arr.Where{$_.Name -eq 'David' -and $_.Article -eq 'TShirt'}.Size
Addendum 2020-07-13
Syntax and readability
As mentioned in the comments, using an array of custom objects is straighter and saves typing, if you like to exhaust this further you might even use the ConvertForm-Csv (or the Import-Csv) cmdlet for building the array:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Or more readable:
$arr = ConvertFrom-Csv #'
Name, Article, Size
David, TShirt, M
Eduard, Trouwsers, S
'#
Note: values that contain spaces or special characters need to be double quoted
Or use an external cmdlet like ConvertFrom-SourceTable which reads fixed width table formats:
$arr = ConvertFrom-SourceTable '
Name Article Size
David TShirt M
Eduard Trouwsers S
'
Indexing
The disadvantage of using an array of custom objects is that it is slower than a hash table which uses a binary search algorithm.
Note that the advantage of using an array of custom objects is that can easily search for anything else e.g. everybody that wears a TShirt with size M:
$arr | Where Article -eq 'TShirt' | Where Size -eq 'M' | Select Name
To build an binary search index from the array of objects:
$h = #{}
$arr | ForEach-Object {
If (!$h.ContainsKey($_.Name)) { $h[$_.Name] = #{} }
If (!$h[$_.Name].ContainsKey($_.Article)) { $h[$_.Name][$_.Article] = #{} }
$h[$_.Name][$_.Article] = $_ # Or: $h[$_.Name][$_.Article]['Size'] = $_.Size
}
$h.david.tshirt.size
M
Note: referencing a hash table key that doesn't exist in Set-StrictMode will cause an error:
Set-StrictMode -Version 2
$h.John.tshirt.size
PropertyNotFoundException: The property 'John' cannot be found on this object. Verify that the property exists.
Here is a simple multidimensional array of strings.
$psarray = #(
('Line' ,'One' ),
('Line' ,'Two')
)
foreach($item in $psarray)
{
$item[0]
$item[1]
}
Output:
Line
One
Line
Two
Two-dimensional arrays can be defined this way too as jagged array:
$array = New-Object system.Array[][] 5,5
This has the nice feature that
$array[0]
outputs a one-dimensional array, containing $array[0][0] to $array[0][4].
Depending on your situation you might prefer it over $array = New-Object 'object[,]' 5,5.
(I would have commented to CB above, but stackoverflow does not let me yet)
you could also uses System.Collections.ArrayList to make a and array of arrays or whatever you want.
Here is an example:
$resultsArray= New-Object System.Collections.ArrayList
[void] $resultsArray.Add(#(#('$hello'),2,0,0,0,0,0,0,1,1))
[void] $resultsArray.Add(#(#('$test', '$testagain'),3,0,0,1,0,0,0,1,2))
[void] $resultsArray.Add("ERROR")
[void] $resultsArray.Add(#(#('$var', '$result'),5,1,1,0,1,1,0,2,3))
[void] $resultsArray.Add(#(#('$num', '$number'),3,0,0,0,0,0,1,1,2))
One problem, if you would call it a problem, you cannot set a limit. Also, you need to use [void] or the script will get mad.
Using the .net syntax (like CB pointed above)
you also add coherence to your 'tabular' array...
if you define a array...
and you try to store diferent types
Powershell will 'alert' you:
$a = New-Object 'byte[,]' 4,4
$a[0,0] = 111; // OK
$a[0,1] = 1111; // Error
Of course Powershell will 'help' you
in the obvious conversions:
$a = New-Object 'string[,]' 2,2
$a[0,0] = "1111"; // OK
$a[0,1] = 111; // OK also
Another thread pointed here about how to add to a multidimensional array in Powershell. I don't know if there is some reason not to use this method, but it worked for my purposes.
$array = #()
$array += ,#( "1", "test1","a" )
$array += ,#( "2", "test2", "b" )
$array += ,#( "3", "test3", "c" )
Im found pretty cool solvation for making arrays in array.
$GroupArray = #()
foreach ( $Array in $ArrayList ){
$GroupArray += #($Array , $null)
}
$GroupArray = $GroupArray | Where-Object {$_ -ne $null}
Lent from above:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Print the $arr:
$arr
Name Article Size
---- ------- ----
David TShirt M
Eduard Trouwsers S
Now select 'David'
$arr.Where({$_.Name -eq "david"})
Name Article Size
---- ------- ----
David TShirt M
Now if you want to know the Size of 'David'
$arr.Where({$_.Name -eq "david"}).size
M