Use Variable for Select-Object or Format-Table - powershell

I have a a similar question like this: Question 44460843
I want to use variables for the fields.
This example works well:
$x = ("Id,ProcessName,CPU").split(",")
Get-Process | ft -a $x
But how do i get it, if i want custom fields like this?
Get-Process | ft -a Id,ProcessName,#{Name = 'CPUx' ; Expression = {$_.CPU}}
I tried this, but it don't works:
$x = ("Id,ProcessName,#{Name = 'CPUx' ; Expression = {$_.CPU}}").split(",")
Get-Process | ft -a $x
Does anybody know how to do this?
I am a lazy guy and want to avoid this copy/paste orgies with long text.

Note:
The answer applies analogously to the Select-Object cmdlet, which is what should be used to extract properties for later programmatic processing.
Format-* cmdlets, such as Format-Table in this case (whose built-in alias is ft), are only intended to produce for-display formatting; see this answer for more information.
Theo has provided the solution in a comment:
$x = 'Id', 'ProcessName', #{ Name = 'CPUx' ; Expression = { $_.CPU } }
Get-Process | Format-Table -AutoSize $x # same as: ft -a $x
That is, the answer to the linked question applies in your case as well, even though it happens to use only literal strings as property names:
Directly construct an array of property names and calculated properties, using ,, the array constructor operator, which allows you to use any mix of literals and variables; e.g.:
$pName = 'processName' # property name
$pCpu = #{ Name = 'CPUx' ; Expression = { $_.CPU } } # calculated property
$x = 'Id', $pName, $pCpu
Do not start with a single string that you split into an array of tokens with .Split(), because it will limit you to property names, given that the tokens are invariably strings.
As an aside:
The array binds positionally to Format-Table's (ft's) -Property parameter, and you can easily discover that as well as the fact that an array of property names / calculated properties is discovered as follows:
PS> Format-Table -?
...
SYNTAX
Format-Table [[-Property] <System.Object[]>] ...
DESCRIPTION
...
The outer [...] tells you that the parameter is optional as a whole.
The [...] around -Property tells you that specifying the parameter name explicitly is optional, i.e. that positional binding is supported.
<System.Object[]> tells you that an array ([]) of System.Object instances is expected as the type of the argument(s).
To get more information about what objects, specifically, may be passed, inspect the parameter individually:
PS> Get-Help Format-Table -Parameter Property
-Property <System.Object[]>
Specifies the object properties that appear in the display and the order
in which they appear. Type one or more property names, separated
by commas, or use a hash table to display a calculated property.
Wildcards are permitted.
...

Related

Issue with using properties in an array and custom calculated property

So I have the following code that ingests AD users on a domain controller. The following throws an error:
# User Props to select
$user_props = #(
'Name',
'DistinguishedName',
'SamAccountName',
'Enabled',
'SID'
)
# Get AD groups an AD user is a member of
$user_groups = #{ label = 'GroupMemberships'; expression = { (Get-ADPrincipalGroupMembership -Identity $_.DistinguishedName).Name } }
# Get AD Users
$users = Get-ADUser -Filter * -Property $user_props | Select-Object $user_props, $user_groups -ErrorAction Stop -ErrorVariable _error
However, if I were to change $users to the following:
$users = Get-ADUser -Filter * -Property $user_props | Select-Object Name, DistinguishedName, SamAccountName, Enabled, SID, $user_groups -ErrorAction Stop -ErrorVariable _error
I no longer get this error. Is there a way I can define $user_props such that I don't need to type out each property and still use my custom calculated property $user_groups?
I believe the issue has to do with mixing an array ($user_props) with a hashtable ($user_groups) but I'm unsure how to best write this. Thank you for the help!
The easiest way to "concatenate" two variables into one flat array is to use the #(...) array subexpression operator:
... |Select-Object #($user_props;$user_groups) ...
Since this issue keeps coming up, let me complement Mathias R. Jessen's effective solution with some background information:
Select-Object's (potentially positionally implied) -Property parameter requires a flat array of property names and/or calculated properties (for brevity, both referred to as just property names below).
Therefore, if you need to combine two variables containing arrays of property names, or combine literally enumerated property name(s) with such variables, you must explicitly construct a flat array.
Therefore, simply placing , between your array variables does not work, because it creates a jagged (nested) array:
# Two arrays with property names.
$user_props = #('propA', 'propB')
$user_groups = #('grpA', 'grpB')
# !! WRONG: This passes a *jagged* array, not a flat one.
# -> ERROR: "Cannot convert System.Object[] to one of the following types
# {System.String, System.Management.Automation.ScriptBlock}."
'foo' | Select-Object $user_props, $user_groups
# !! WRONG:
# The same applies if you tried to combine one or more *literal* property names
# with an array variable.
'foo' | Select-Object 'propA', $user_groups
'foo' | Select-Object $user_groups, 'propA', 'propB'
That is, $user_props, $user_groups effectively passes
#( #('propA', 'propB'), #('grpA', 'grpB') ), i.e. a jagged (nested) array,
whereas what you need to pass is
#('propA', 'propB', 'grpA', 'grpB'), i.e. a flat array.
Mathias' solution is convenient in that you needn't know or care whether $user_props and $user_groups contain arrays or just a single property name, due to how #(...), the array-subexpression operator works - the result will be a flat array:
# Note how the variable references are *separate statements*, whose
# output #(...) collects in an array:
'foo' | Select-Object #($user_props; $user_groups)
# Ditto, with a literal property name.
# Note the need to *quote* the literal name in this case.
'foo' | Select-Object #('propA'; $user_groups)
In practice it won't make a difference for this use case, so this is a convenient and pragmatic solution, but generally it's worth noting that #(...) enumerates array variables used as statements inside it, and then collects the results in a new array. That is, both $user_props and $user_groups are sent to the pipeline element by element, and the resulting, combined elements are collected in a new array.
A direct way to flatly concatenate arrays (or append a single element to an array) is to use the + operator with (at least) an array-valued LHS. This, of necessity, returns a new array that is copy of the LHS array with the element(s) of the RHS directly appended:
# Because $user_props is an *array*, "+" returns an array with the RHS
# element(s) appended to the LHS element.
'foo' | Select-Object ($user_props + $user_groups)
If you're not sure if $user_props is an array, you can simply cast to [array], which also works with a single, literal property name:
# The [array] cast ensures that $user_props is treated as an array, even if it isn't one.
# Note:
# 'foo' | Select-Object (#($user_props) + $user_groups)
# would work too, but would again needlessly enumerate the array first.
'foo' | Select-Object ([array] $user_props + $user_groups)
# Ditto, with a single, literal property name
'foo' | Select-Object ([array] 'propA' + $user_groups)
# With *multiple* literal property names (assuming they come first), the
# cast is optional:
'foo' | Select-Object ('propA', 'propB' + $user_groups)
Note:
The above uses (...), the grouping operator, in order to pass expressions as arguments to a command - while you frequently see $(...), the subexpression operator used instead, this is not necessary and can have unwanted side effects - see this answer.
#(...) isn't strictly needed to declare array literals, such as #('foo', 'bar') - 'foo', 'bar' is sufficient, though you may prefers enclosing in #(...) for visual clarity. In argument parsing mode, quoting is optional for simple strings, so that Write-Output foo, name is the simpler alternative to Write-Output #('foo', 'name')
,, the array constructor operator, has perhaps surprisingly high precedence, so that 1, 2 + 3 is parsed as (1, 2) + 3, resulting in 1, 2, 3

Sort-Object ignores substituted switch

I'm trying to adapt the sorting order of Sort-Object to a situation.
However, it seems to ignore my variable as the sort order switch -Descending
The following code shows my problem:
I've also tried some variants on the actual substitution, e.g. $($SortOrder) but haven't found a combination that works.
$dlist = ("Zeta", "Beta", "Foo", "Alpha","Yada" )
$sorted1 = $dlist | Sort-Object -Descending
Write-Host $sorted1
$sortOrder = "-Descending"
$sorted2 = $dlist | Sort-Object $sortOrder
Write-Host $sorted2
This produces the following two lines: The first is sorted the second is not
Zeta Yada Foo Beta Alpha
Zeta Beta Foo Alpha Yada
What am I failing to do?
To control the direction of sorting programmatically, use a [bool] variable and pass it to the -Descending switch; $true sorts in descending order, $false in ascending order:
$descending = $true # Setting this to $false will sort in ascending order.
'Zeta', 'Beta', 'Foo', 'Alpha', 'Yada' | Sort-Object -Descending:$descending
Note how : rather than the usual space must be used to separate the parameter name from the value in this case (this syntax is generally supported, but otherwise rarely used).[1]
Alternatively, use splatting:
$params = #{ Descending = $true }
'Zeta', 'Beta', 'Foo', 'Alpha', 'Yada' | Sort-Object #params
Both commands yield the desired (descending) order:
Zeta
Yada
Foo
Beta
Alpha
As for what you tried:
You cannot pass a switch parameter, such as -Descending, as a string (variable).
If you do, Sort-Object will consider it a positional argument that binds to the -Property parameter, indicating what property/ies on the input objects to sort by.
If no such property exists, all input objects compare the same (the value they're compared by is $null for all of them), with no guaranteed output order.
[1] The : unconditionally tells PowerShell that the next token is the argument for that parameter. If spaces were used, PowerShell would consider the next token a separate positional argument, given that switch parameters normally do not take arguments (the mere presence of a switch parameter implies that its value is $true).
There is a possibility to do it with Strings:
$desc='-descending'
Invoke-Expression ls | sort $desc
But the solution above is better

Weird CSV Output [duplicate]

I'm running the Test-AdfsServerHealth (Ref.)
The problem is, one of the output values (value name Output) is an array that shows up as System.Collection.Hashtable and I'm trying to find a way to get this in a neat Excel format.
For instance, this is one of the actual values on the CSV when I export:
Name Result Detail Output
TestServiceAccountProperties Pass "" System.Collections.Hashtable
But PowerShell displays:
Name : TestServiceAccountProperties
Result : Pass
Detail :
Output : {AdfsServiceAccount, AdfsServiceAccountDisabled, AdfsServiceAccountLockedOut,
AdfsServiceAccountPwdExpired...}
ExceptionMessage :
The actual command I'm running is:
$ServerResult = Test-AdfsServerHealth
tl;dr:
Test-AdfsServerHealth |
Select-Object Name, Result, Detail, #{
n='Output'
e={ $_.prop2.GetEnumerator().ForEach({ '{0}={1}' -f $_.Key, $_.Value }) -join ' ' }
} | ExportTo-Csv out.csv
The above serializes each .Output hashtable's entries into single-line string composed of space-separated <key>=<value> pairs (PSv4+ syntax) that should work reasonably well in CSV output.
Since CSV is a text format, PowerShell serializes objects to be exported by calling their .ToString() method.
Complex objects such as [hashtable] instances often yield just their full type name (System.Collections.Hashtable) for .ToString(), which isn't useful in a CSV.
A simplified example (I'm using ConvertTo-Csv, but the example applies analogously to Export-Csv):
# Create a custom object whose .col2 property is a hashtable with 2
# sample entries and convert it to CSV
PS> [pscustomobject] #{ prop1 = 1; Output = #{ name='foo'; ID=666 } } | ConvertTo-Csv
"prop1","Output"
"1","System.Collections.Hashtable"
If all output objects from Test-AdfsServerHealth had the same hashtable structure in their .Output property, you could try to flatten the hashtable by making its entries columns in their own right, but it sounds like that is not the case.
You must therefore manually transform the hashtable into a text representation that fits into a single CSV column:
You can do this with Select-Object and a calculated property that performs the transformation for you, but you need to decide on a text representation that makes sense in the context of a CSV file.
In the following example, a single-line string composed of space-separated <key>=<value> pairs is created (PSv4+ syntax).
[pscustomobject] #{ prop1 = 1; Output = #{ name='foo'; ID=666 } } |
Select-Object prop1, #{
n='Output'
e={ $_.prop2.GetEnumerator().ForEach({ '{0}={1}' -f $_.Key, $_.Value }) -join ' ' }
} | ConvertTo-Csv
For an explanation of the hashtable format that creates the calculated prop2 property, see this answer of mine.
The above yields:
"prop1","prop2"
"1","ID=666 name=foo"
Note, however, that if the values in your hashtables are again complex objects that serialize to their type name only, you'd have to apply the approach recursively.
Optional reading: Flattening a hashtable property into individual columns
If the hashtable-valued properties of the objects to export to a CSV file all have the same structure, you can opt to make the hashtable entries each their own output column.
Let's take the following sample input: a collection of 2 custom objects whose .prop2 value is a hashtable with a uniform set of keys (entries):
$coll = [pscustomobject] #{ prop1 = 1; prop2 = #{ name='foo1'; ID=666 } },
[pscustomobject] #{ prop1 = 2; prop2 = #{ name='foo2'; ID=667 } }
If you know the key names (of interest) up front, you can simply use an explicit list of calculated properties to create the individual columns:
$coll | select prop1, #{ n='name'; e={ $_.prop2.name } }, #{ n='ID'; e={ $_.prop2.ID } } |
ConvertTo-Csv
The above yields the following, showing that the hashtable entries became their own columns, name and ID:
"prop1","name","ID"
"1","foo1","666"
"2","foo2","667"
More advanced techniques are required if you do not know the key names up front:
# Create the list of calculated properties dynamically, from the 1st input
# object's .prop2 hashtable.
$propList = foreach ($key in $coll[0].prop2.Keys) {
# The script block for the calculated property must be created from a
# *string* in this case, so we can "bake" the key name into it.
#{ n=$key; e=[scriptblock]::Create("`$_.prop2.$key") }
}
$coll | Select-Object (, 'prop1' + $propList) | ConvertTo-Csv
This yields the same output as the previous command with the fixed list of calculated properties.
This won't be significantly difficult, just going to be annoying to do. The reason you are getting "System.collections.hashtable" is because is unable to display everything in that property in a single format like that, there is way to much information. You will have to create another object and put whatever information you want in there.
This prob won't work exactly like you want, but with some tweaking it should get you there.
$ServerResult = Test-ADFSServerHealth
$Object = New-PSObject -Property #{
'Name' = $ServerResult.name
'Result' = $ServerResult.Result
'Detail' = $ServerResult.Detail
'Output' = ($ServerResult.Output | out-string -stream)
'ExceptionMessage' = $ServerResult.ExceptionMessage
}
If your interested, here are the resources I used to find this answer.
Converting hashtable to array of strings
https://devops-collective-inc.gitbooks.io/the-big-book-of-powershell-gotchas/content/manuscript/new-object_psobject_vs_pscustomobject.html

PowerShell: How to change table headers in loop?

I want to collect data remotely and adapt the table headers later with the help of an xml-file. This should happen in a loop, looking like that:
foreach($tableheader in $table) {
$table.$tableheader = $xmlFile.$tableheader
}
Amongst others I tried the following:
$x = 0
$sitesonfig = Get-ConfigSite -AdminAddress localhost
foreach($Prop in ($siteconfig |get-member -MemberType Property | select -Property name))
{
$x += 1;
$siteconfig = $siteconfig | Select-Object * | format-table #{l="Smile$x";e={$_.$Prop}}
}
Yes, I know this looks silly, but I've got really no idea, how to change the headers one by one without listing each time all the other headers, too.
One possibility is to use a loop to create the header map that you pass into Format-Table.
Here is your second example modified to demonstrate this concept. You should be able to adapt this to grab the header info from your XML file.
$x = 0
$siteconfig = Get-ConfigSite -AdminAddress localhost
$headerMap = #()
foreach($Prop in ($siteconfig |get-member -MemberType Property | select -ExpandProperty name))
{
$x += 1;
$headerMap += #{
l="Smile$x";
e={ $_.$Prop }.GetNewClosure()
}
}
$siteconfig | Format-Table $headerMap
Important Points
Select -Property name needed to be changed to Select -ExpandProperty name. The reason for this is that Select-Object in PowerShell will return an object filtered down to the selected member but you need a string for grabbing the property value by name. The -ExpandProperty parameter will expand this to be the string value instead.
The expression block needs GetNewClosure() called on it to capture the value of $Prop at the time of script block creation versus at the time of calling. This will probably be a little confusing if you are new to the concept of closures and PowerShell's scoping rules. Without this, due to PowerShell's scoping rules, $Prop will evaluate to the value of $Prop at the time it is used by Format-Table. By calling GetNewClosure(), the value of $Prop is captured when GetNewClosure() is called which is what we want in this case.

PowerShell update nested value

I have the following output from a PowerShell command and want to update the value for EmployeeID
I can filter the output with $test.identifiers | where {$_.name -like "EmployeeID" }
But if I try to update the value with
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {$_.values.value = "098324"}
I get an error
How can I update this nested value?
$_.values contains an array (or collection) objects, which explains why you can get (read) the .value property, but not set (write) it (see below).
If you expect the array to have just one element, simply use [0] to access that element directly:
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {
$_.values[0].value = '098324'
}
If there are multiple elements, use
$_.values | foreach { $_.value = '098324' } to assign to them all, or, alternatively in PSv4+,
$_.values.ForEach('value', '098324')
In PSv3+ a feature called member-access enumeration allows you to access a property on a collection and have the property values from the individual elements returned as an array.
However, that only works for getting properties, not for setting them.
When you try to set, only the collection's own properties are considered, which explains the error you saw - an array itself has no .value property.
While this asymmetry is by design, to avoid potentially unwanted bulk modification, the error message could certainly be more helpful.
Simple reproduction of the problem:
Create an object with property one containing a single-element array with another object, with property two:
$obj = [pscustomobject] #{ one = , [pscustomobject] #{ two = 2 } }
The default output looks as follows:
PS> $obj
one
---
{#{two=2}}
The outer {...} indicate an array, as in your case, and what's inside is a
hashtable-like notation that PowerShell uses to represent custom objects.
Getting the nested-inside-an-array object's two property works as intended:
PS> $obj.two
2
Trying to set it fails:
PS> $obj.two = 2.1
The property 'two' cannot be found on this object. Verify that the property exists and can be set.
To set, use .ForEach(), for instance:
PS> $obj.ForEach('two', 2.1); $obj
one
---
{#{two=2.1}}
Have you tried it this way with the full object path:
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {$_.identifiers.values.value = "098324"}