Sort-Object ignores substituted switch - powershell

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

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

Use Variable for Select-Object or Format-Table

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.
...

Powershell eq operator saying hashes are different, while Write-Host is showing the opposite

I have a script that periodically generates a list of all files in a directory, and then writes a text file of the results to a different directory.
I'd like to change this so it checks the newest text file in the output directory, and only makes a new one if there's differences. It seemed simple enough.
Here's what I tried:
First I get the most recent file in the directory, grab the hash, and write my variable values to the console:
$lastFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$oldHash = Get-FileHash $lastFile | Select-Object Hash
Write-Host 'lastFile = '$lastFile
Write-Host 'oldHash = '$oldHash
Output:
lastFile = C:\ReportOutputDir\test1.txt
oldHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Then I do the exact same gci on the FileList dir, and create a new file (new_test.txt), then grab the hash of this file:
gci -Path C:\FileLists -File -Recurse -Name -Depth 2 | Sort-Object | out-file C:\ReportOutputDir\new_test.txt
$newFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$newHash = Get-FileHash $newFile | Select-Object Hash
Write-Host 'newFile = '$newFile
Write-Host 'newHash = '$newHash
Output:
newFile = C:\ReportOutputDir\new_test.txt
newHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Finally, I attempt my -eq operator where I'd usually simply remove the newFile if it's equal. For now, I'm just doing a simple :
if ($newHash -eq $oldHash){
'files are equal'
}
else {'files are not equal'}
And somehow, I'm getting
files are not equal
What gives? Also, for the record I was originally trying to save the gci output to a variable and comparing the contents of the last file to the gci output, but was also having trouble with the -eq operator. Fairly new to powershell stuff so I'm sure I'm doing something wrong here.
Select-Object Hash creates an object with a .Hash property and it is that property that contains the hash string.
The object returned is of type [pscustomobject], and two instances of this type never compare as equal - even if all their property names and values are equal:
The reason is that reference equality is tested, because [pscustomobject] is a .NET reference type that doesn't define custom equality-testing logic.
Testing reference equality means that only two references to the very same instance compare as equal.
A quick example:
PS> [pscustomobject] #{ foo = 1 } -eq [pscustomobject] #{ foo = 1 }
False # !! Two distinct instances aren't equal, no matter what they contain.
You have two options:
Compare the .Hash property values, not the objects as a whole:
if ($newHash.Hash -eq $oldHash.Hash) { # ...
If you don't need a [pscustomobject] wrapper for the hash strings, use Select-Object's -ExpandProperty parameter instead of the (possibly positionally implied) -Property parameter:
Select-Object -ExpandProperty Hash
As for why the Write-Host output matched:
When you force objects to be converted to string representations - essentially, Write-Host calls .ToString() on its arguments - the string representations of distinct [pscustomobject] instances that have the same properties and values will be the same:
PS> "$([pscustomobject] #{ foo = 1 })" -eq "$([pscustomobject] #{ foo = 1 })"
True # Same as: '#{foo=1}' -eq '#{foo=1}'
However, you should not rely on these hashtable-like string representations to determine equality of [pscustomobject]s as a whole, because of the inherent limitations of these representations, which can easily yield false positives.
This answer shows how to compare [pscustomobject] instances as a whole, by comparing all of their property values, by passing all property names to Compare-Object -Property - but note that this assumes that all property values are either strings or instances of .NET value types or corresponding properties must again either reference the very same instance of a .NET reference type or be of a type that implements custom equality-comparison logic.

How do I filter objects based on a name match?

I am returning a lot of data I would like to filter. The names of the property have this information. I am used to filtering based on | ? {$_.Name -eq 'Value'}. I would expect to be able to use the hidden .pscustomobject to do this more dynamically.
$x = [pscustomobject]#{
atruevalue = 'sometext'
afalsevalue = 'sometext'
}
$x | ?{$_.psobject.Properties.Name -like '*true*'}
I expect this to return:
> atruevalue
> ----------
> sometext
However, it simply returns every item in the object.
Could anyone explain this behavior?
If you really do want to filter the properties, then moving things around a bit will do it. This would look like:
$x.psobject.Properties | ? {$_.Name -like '*true*'}
If you just want the values rather than the properties, then add another stage to the pipeline:
$x.psobject.Properties | ? {$_.Name -like '*true*'} | % Value
The Where-object filters Rows of the input, what your example does.
To Filter columns you need Select-Object.
$x = [pscustomobject]#{
atruevalue = 'sometext'
afalsevalue = 'sometext'
atruenightmare = 'someothertext'}
> $x|select ($x.psobject.properties|? name -like '*true*').Name
atruevalue atruenightmare
---------- --------------
sometext someothertext
Bruce Payette's helpful answer shows the simplest solution.
As for:
Could anyone explain this behavior?
?, a built-in alias for the Where-Object cmdlet, acts as a filter, which means that if the filter condition in the form of an evaluated-for-each-input script block ({ ... }) evaluates to $True, the input object at hand (represented as $_ inside the script block) is passed through as-is.
To put it differently: it is immaterial what specific properties of the input object your script block examines - if the condition evaluates to $True, the whole input object is passed through.

powershell hashtable key versus object property

I'm confused at how PowerShell treats Hashtable keys versus object properties.
Consider:
$list = #{
'one' = #{
name = 'one'
order = 80
};
'two' = #{
name = 'two'
order = 40
};
'twotwo' = #{
name = 'twotwo'
order = 40
};
'three' = #{
name = 'three'
order = 20
}
}
$list.Values|group-object { $_.order }
$list.Values|group-object -property order
The first Group-Object gives me what I expect (three groups), the second one does not (one big group). Clearly Hashtable keys are not object properties, but syntactically they are referenced in the same manner (var.name).
What is that second group-object actually doing?
What does it think the 'order' property is?
This is understandable confusion, but as you said, hashtable keys are not object properties.
It can be tempting to treat them the same (and sometimes that works), but this is a situation where it definitely won't.
And part of the reason is that you're using a different cmdlet, not the direct language semantics.
Hashtables can use dot notation for their keys but the keys are not properties, and when you use the -Property parameter of Group-Object, you are looking for properties specifically, not just "anything you can access with a dot".
The alternative form of that parameter that takes a scriptblock, as you saw, is code that will be executed and so it's whatever value that block returns that will be grouped on.
To more directly answer your questions:
What is that second group-object actually doing?
It's looking for a property (specifically) on the current object (which is a hashtable).
If you want to see what the properties on one of those objects looks like, try this:
$list.Values[0].PSObject.Properties | ft
What does it think the 'order' property is?
It doesn't think it's anything; it looks for a property with that name and if it finds one it uses that value; otherwise it uses $null.
You'll get the same result with:
$list.Values | group -Property FakeProp
or
$list.Values | group -Property { $null }
Addressing your question in the comment:
Is there any way to know "when you should" and "when you shouldn't",
Should I just defer to using script blocks, When is -Property usage
preferred over { ... }?
-Property (without a scriptblock) is preferred whenever the value you want to group is available as a direct property of the object being inspected. If it's a property of a property, or some calculated value, or a hashtable key/value, or anything else, use a scriptblock. I'll call those "complex values".
If the complex value is useful, you may want to add it as an actual property of the object itself to encapsulate it; then you can reference it directly. This example isn't really appropriate for your hashtable situation but consider objects that represent people. They have 2 properties: Name and DateOfBirth.
$people is an array of these objects, and you want to group people by age (please ignore my inaccurate age-determining code).
$people | Group-Object -Property { ([DateTime]::Now - $_.DateOfBirth).Days / 365 -as [int] }
That's ok if you never need to know the age again; of course that's unlikely and also this looks a bit messy. It should be more immediately clear that you want to "group by age".
Instead, you can add your own (calculated) Age property to the existing objects with Add-Member:
$people |
Add-Member -MemberType ScriptProperty -Name Age -Value {
([DateTime]::Now - $this.DateOfBirth).Days / 365 -as [int]
} -Force
From here on out, each object in the $people array has an Age property that is calculated based on the value of the DateOfBirth property.
Now you can make your code clearer:
$people | Group-Object -Property Age
Again this doesn't really address your hashtable issue; the truth is they don't work that well for grouping. If you're going to do a lot of grouping with them, and you don't really need hashtables, make them into objects:
$objs = $list.Values |
ForEach-Object -Process {
New-Object -TypeName PSObject -Property $_ # takes a hashtable
}
or
$objs = $list.Values |
ForEach-Object -Process {
[PSCustomObject]$_ # converts a hashtable to PSObject
}
Then:
$objs | Group-Object -Property Order