Open palenshus opened 1 year ago
If you add this function to a module or your $profile:
function New-PsCustomObject
{
[OutputType([pscustomobject])]
[Alias("New")]
Param
(
[Parameter(Mandatory, ValueFromPipeline)]
[hashtable]
$Properties
)
Process
{
[pscustomobject]$Properties
}
}
You can create objects like this:
$Objects = @(
New @{Prop1="Val1";Prop2="Val2"}
New @{Prop1="Val3";Prop2="Val4"}
New @{Prop1="Val5";Prop2="Val6"}
)
One caveat about this approach is that the property order will be random because of the nature of hashtables.
@MartinGC94 it might be better to do [System.Collections.IDictionary]
as the type so you can do
New [Ordered]@{Foo = 'bar'}
But at that point in time why not just do [PSCustomObject]
:).
Anonymous objects should use the argument name as the implicit property name if none other is provided
You can use Select-Object
to do this today:
$obj | Select-Object -Property Name, Length
This will create a brand new object that has the Name and Length property of the input object. You can even combine this logic with custom properties
$obj | Select-Object -Property Name, @{N='Size'; E = [$_.Length}}
Personally IMO [PSCustomObject]@{}
is pretty concise enough, anything else is just special magic that does what you can already achieve today with other things. Note this is a personal opinion and not reflective of everyone.
See also:
new { x.Name, x.Length }
I don't understand are you talking about PowerShell as a shell or as a scripting language? The first implies concise syntax, the second its readability. How to find the optimal solution? In the console, it is quite possible to get by with an anonymous function to create an object, even if it does not have a parameter block.
& { $h = [ordered] @{}; $h.string,$h.integer = $args; [pscustomobject] $h } 'one' 2
& { param([string]$string, [int]$integer) $h = @{}; $h.string,$h.integer = $string,$integer; [pscustomobject] $h } 'one' 2
Thanks everyone! I'm seeing a bunch of workarounds, none of which are particularly concise in my opinion.
Creating a helper New function is slick, but still requires specifying the field names AND using the ordered keyword if you don't want your properties dumping in random order. And of course, everyone would have to create the helper function themselves.
The Select-Object method gives you implicit field names, but seems to require separate code to define and initialize the object.
@237dmitry, I'm not sure about your question. I'd like something that's more concise AND more readable, which I'd argue the example I pasted is for both. Is { Name = x.Name }
more or less readable than { x.Name }
? Once you're comfortable with the syntax, I'd say it's more readable, and objectively more concise.
The Select-Object method gives you implicit field names, but seems to require separate code to define and initialize the object.
How is that any different from your new { x.Name, x.Length }
syntax? In this case x
must already have defined a Name
and Length
property so this would look like $x | Select Name, Length
. If you are just talking about a constructor to a class then this is already possible with [TypeName]::new($arg1, $arg2, ...)
. If you are talking about a class with an empty constructor but you want to set the properties on init you can do [TypeName]@{Prop1 = $foo; Prop2 = $bar}
.
Once you're comfortable with the syntax, I'd say it's more readable, and objectively more concise.
I would like to say that if there are such changes, it will not be soon. For this, it is necessary to go a long way of evolution. Backward compatibility, Windows powershell compatibility, it's all heavy baggage. All those who expressed their opinion above, they tried to show that preparation is necessary to solve specific problem effectively. Everyone has their own templates, which are used for concise syntax in everyday tasks. Your desire for a sharp and efficient scripting language is in full agreement with the general opinion, but in my opinion, this is like asking the fsharp syntax for the сsharp syntax.
Good point @237dmitry, back-compat could be a big issue here, for sure!
@jborean93, the specific scenario I was trying was I wanted a new, anonymous object, which was partially populated with an existing field, and partly by a calculated one. Here's how I solved it at the time:
get-appxpackage | % { [pscustomobject]@{ Name = $_.Name; Dependencies = $_.Dependencies | ? { $_ -match "\.NET" } } }
With your original syntax, it would look like this (you had a typo in your example):
get-appxpackage | select Name, @{ L="Dependencies"; E={$_.Dependencies | ? { $_ -match "\.NET" } } }
That's shorter, but I think more confusing, with the throwaway property names and all. With my dream syntax it'd be something like:
get-appxpackage | select { $_.Name, Dependencies = $_.Dependencies | ? { $_ -match "\.NET" }
or
get-appxpackage | % { new { $_.Name, Dependencies = $_.Dependencies | ? { $_ -match "\.NET" } }
As for the desire to have inferred property names:
The C# syntax only works for non-calculated properties; calculated properties require repeating the name of the involved property too:
// `Year =` is required since the property value is an *expression*, not just a simple name or property access.
var dt = DateTime.Now; new { dt.Month, Year = (dt.Year + 1) }
Leaving C#'s ability to use the names of variables (e.g., var Num = 42; new { Num }
) out of the picture:
Select-Object
is therefore a reasonable analog to this functionality.
However, as a separate aspect you can argue - and I would agree - that the syntax for PowerShell's calculated properties is too verbose and should therefore be simplified; e.g.:
# HYPOTHETICAL example; the concise equivalent of:
# [datetime]::Now | Select-Object Month, @{ Name = 'Year'; Expression = { $_.Year + 1 } }
[datetime]::Now | Select-Object Month, @{ Year = $_.Year + 1 }
The following preexisting issue proposes this:
If this simplification gets implemented, I'd say Select-Object
is then (mostly) on par with C#'s syntax with respect to inferring property names.
As for the desire to have a more concise syntax for object ([pscustomobject]
) literals:
without first creating a hashmap and then casting it.
Note that [pscustomobject] @{ one=1; two=2; three=3 }
already is syntactic sugar: that is, you're not creating a hashtable by way of a literal @{ ... }
that is then converted to [pscustomobject]
, as evidenced by the fact that this syntax preserves entry definition order, which hashtables themselves do not.
[pscustomobject] [ordered] @{ one=1; two=2; three=3 }
, appears to be faster.)However, I personally agree that [pscustomobject] @{ one=1; two=2; three=3 }
is a lot of ceremony for something as frequently used as (anonymous) object construction, especially given that it involves additional syntax that builds on that of hashtables.
In an ideal world - one that would be allowed to break backward compatibility in substantial ways - PowerShell would make no distinction between hashtables and custom objects, similar to how objects function in JavaScript, in which case you'd only need one object/dictionary-literal form, and something like @{ one=1; two=2; three=3 }
alone would suffice (for less common uses cases such as non-string-typed dictionary keys you'd still need construction of specific .NET types).
System.Dynamic.ExpandoObject
class where used in lieu of [psobject]
/ [pscustomobject]
, it could situationally act as either an object with properties or as a dictionary with keyed entries.Given the backward compatibility constraints, I see only two options for shortening the object-literal syntax:
Option A: Introduce a short type accelerator such as [co]
:
[pscustomobject] @{ one=1; two=2; three=3 }
to [co] @{ one=1; two=2; three=3 }
class
definitions named co
could exist; conceivably, evaluation of the corpus of PowerShell scripts could let to classifying this as an acceptable breaking change, namely one falling into bucket 3.Option B: Preferably, introduce a symbol-based variation of the hashtable-literal syntax.
[pscustomobject] @{ one=1; two=2; three=3 }
to, say, @[ one=1; two=2; three=3 ]
@
: while [ ... ]
is invariably at odds with other languages, in the context of PowerShell it may work due to type literals being enclosed that wayPowerShell isn't meant to be written like C# & those that come to PowerShell infrequently, often voice similar asks.
Personally I don't see this happening but that's more the Language WG to discuss further
I'll admit that even though I must do it about 100x per day, my brain definitely consumes a little extra bandwidth each time I have to type out [pscustomobject]@{...}
without typos.
I am a pretty fast typist, but I'm positively poor at spelling, so whenever I have to type a word that is say >10 chars long, I'm usually sounding it out in my head, in real-time.
So 100x per day I feel like I'm going:
$SomeVar =
(READY BRAIN? GO!) [PS
...CUSTOM
...OBJECT]@{
...
(😅 nailed it! and no, not actually in all-caps)
When prototyping some script in a REPL environment, I'm trying to move fast and break things so I can iterate on an idea, and a typo isn't a worthy reason to have to go back and iterate again. I've made a few efforts to try and reduce this pain-point for myself (like a function similar to what's been mentioned above), but for some reason I cannot get a type accelerator to work with the PSCustomObject
type.
I've been using a number of self-defined accelerators for some time now (mostly aliasing some commonly used types from the System.Collections
namespace), but there must be some idiosyncrasy to this specific type and this issue seemed like the perfect opportunity to learn more!
For instance, this works just fine:
> $TypeAccelerators = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
> $TypeAccelerators::Add('StringSet', [System.Collections.Generic.HashSet[System.String]])
> [StringSet]$MyStrSet = @('one','two','one','three') # should prevent duplicate value 'one'
> $MyStrSet.GetType().FullName
System.Collections.Generic.HashSet`1[[System.String, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
> $MyStringSet
one
two
three
But the same technique will not work with System.Management.Automation.PSCustomObject
type, instead you get this error:
> $TypeAccelerators::Add('psco', [System.Management.Automation.PSCustomObject])
> $MyPSCO = [psco]@{ myprop = 'myval' }
InvalidArgument: Cannot convert the "System.Collections.Hashtable" value of type "System.Collections.Hashtable" to type "System.Management.Automation.PSCustomObject".
I assume it has to do with the implementation details concerning the hashtable->pscustomobject conversion done within the syntactic sugar of [pscustomobject]@{}
as @mklement0 nicely described.
Anyone have any insights on a workaround for this behavior?
Unfortunately there’s no workaround I know. The [PSCustomObject]@{}
expression is treated specially at parse time and isn’t actually doing a cast like you normally see with similar expressions. It’s special because a hashtable doesn’t preserve the order of keys as written so doing a runtime cast will not keep the order of the properties written. By doing it at parse time it can preserve the order of the keys written.
Definitely still an issue!
Definitely still an issue!
Summary of the new feature / enhancement
I'm a semi-frequent PS user, but still frequently forget the syntax to create an anonymous object. I looked it up again today and found this answer: https://stackoverflow.com/questions/36081372/how-do-i-create-an-anonymous-object-in-powershell. I started with their first suggestion of using a hashmap, which is the most concise syntax, but when passing those to
Format-Table
, you get ugly output (see https://stackoverflow.com/questions/20874464/format-table-on-array-of-hash-tables)So then I used this syntax:
But compared to C#, this is so much less concise than it could be:
It would be great if PS had something a little more analogous to that.
Proposed technical implementation details (optional)
Two suggestions:
Thanks for considering!!