PowerShell / PowerShell

PowerShell for every system!
https://microsoft.com/PowerShell
MIT License
45.25k stars 7.29k forks source link

PowerShell should have more concise anonymous object syntax #18747

Open palenshus opened 1 year ago

palenshus commented 1 year ago

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:

[pscustomobject]@{ Name = $_.Name; $_.Length... }

But compared to C#, this is so much less concise than it could be:

new { x.Name, x.Length }

It would be great if PS had something a little more analogous to that.

Proposed technical implementation details (optional)

Two suggestions:

Thanks for considering!!

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

jborean93 commented 1 year ago

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

mklement0 commented 1 year ago

See also:

237dmitry commented 1 year ago

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
palenshus commented 1 year ago

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.

jborean93 commented 1 year ago

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

237dmitry commented 1 year ago

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.

palenshus commented 1 year ago

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" } }
mklement0 commented 1 year ago

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.

mklement0 commented 1 year ago

As for the desire to have a more concise syntax for object ([pscustomobject]) literals:

without first creating a hashmap and then casting it.


Given the backward compatibility constraints, I see only two options for shortening the object-literal syntax:

kilasuit commented 1 year ago

PowerShell 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

ev-dev commented 1 year ago

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?

jborean93 commented 1 year ago

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.

palenshus commented 11 months ago

Definitely still an issue!

palenshus commented 11 months ago

Definitely still an issue!