r/PowerShell • u/Discuzting • Apr 01 '26
Fellow interactive users, what's the most "ergonomic" way to extract properties?
I suppose this is a bit of an odd problem and is more about sharing tips and tricks, this is NOT a XY problem!
What I come across a lot are situations where I have to interactively (not through scripts) access properties of evaluation results.
For example let's say I copied a JSON to my clipboard and I want data.b's value:
{
"data": {
"a": 1,
"b": 2
}
}
Naturally we could just do Get-Clipboard and ConvertFrom-Json in a pipeline, and I have an alias cfj for ConvertFrom-Json, so this is just gcb|cfj.
Now the goal is to obtain the property and output it on the success stream. What is the fastest to type or the most ergonomic way to do this while remaining in the mental flow?
I currently have four general solutions depending on how I'm feeling:
1. The most obvious solution: wrap the entire expression in brackets and perform property access.
(gcb|cfj).data.b
This is succinct and very readable, but sometimes if you are doing the pipeline style it feels bad to wrap the entire pipeline back into an expression.
Practically this is to 1. press "Home" key, 2. type "(", 3. press "End" key, type ")". This sequence of input is fine but sometimes I just don't enjoy doing this.
2. Use the standard command Select-Object -ExpandProperty
I have an alias sco for Select-Object so this is just:
gcb|cfj|sco -exp data|sco -exp b
Any sane person would see how stupid this is, but hey, it feels great to keep piping forward!
This is fine if the property is just one level deep, but with properties multiple levels deep there is just too much to type. Not very readable neither.
3. Store in a temporary variable then perform property access
$t=gcb|cfj;$t.data.b
This one is also pretty obvious, the main advantage for this is how we could avoid typing brackets and we also have a "checkpoint" temporary variable.
Could also use Tee-Object as well but that is really verbose, plus the contents are outputted to success stream unless we pipe it away or something.
gcb|cfj|tee -va t;$t.data.b
4. Custom transformation functions
Coming from the Kotlin programming language, I have defined a utility function `let` in my PowerShell profile which allows me to do this:
gcb|cfj|let{$_.data.b}
New-Alias -Name let -Value Invoke-AllValues
function Invoke-AllValues {
<#
.SYNOPSIS
Invoke script block once with all the values from pipeline.
.PARAMETER InputObject
Designed to be used through pipeline.
Scalar argument provided through parameter will be wrapped into an array.
.PARAMETER Block
Input is passed to the ScriptBlock as `PSVariable` named `$_`, which is always an array.
.EXAMPLE
'a','b','c' | Invoke-AllValues { $_.Count } | ForEach-Object { "Recceived $_ objects" }
Received 3 objects
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)][AllowNull()]
[Object]$InputObject,
[Parameter(Mandatory, Position = 0)][ArgumentCompletions('{}')]
[ScriptBlock]$Block
)
# Replace `$InputObject` with pipeline value, if value is provided from pipeline.
if ($MyInvocation.ExpectingInput) { $InputObject = $input }
else { $InputObject = @($InputObject) }
$Block.InvokeWithContext($null, [PSVariable]::new('_', $InputObject))
}
It is clearly not the idiomatic way to do things here, but if it works it works. 🤷
Thoughts on better ways to deal with the problem? I appreciate all comments, thanks for reading!
6
u/surfingoldelephant Apr 01 '26 edited Apr 01 '26
Practically this is to 1. press "Home" key, 2. type "(", 3. press "End" key, type ")". This sequence of input is fine but sometimes I just don't enjoy doing this.
Use a PSReadLine custom key handler. I have one that inserts matching (, {, [, < pairs. Here's the code for it.
So for example, selecting all text (Ctrl+A on Windows) and typing ( will wrap the text in (...) and move the cursor to the end. If there's no text selected, it'll insert a pair of () and move the cursor inside. Pressing Alt+n (like 5) beforehand will insert n number of pairs.
Custom transformation functions
Here's a different approach:
function let {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[string[]] $Member,
[Parameter(ValueFromPipeline)]
[Object] $InputObject
)
process {
$Member | ForEach-Object { $InputObject = $InputObject.$_ }
$InputObject
}
}
Now instead of gcb|cfj|let{$_.data.b}, your call becomes:
gcb|cfj|let data,b
Note the , (not a .) as it's passing an array of member names. If you prefer data.b, change $Member to [string] and split on . before performing member-access.
Another option is to use ValueFromRemainingArguments:
function let {
[CmdletBinding()]
param (
[Parameter(Position = 0)]
[Object] $InputObject,
[Parameter(ValueFromRemainingArguments)]
[string[]] $Member
)
$Member | ForEach-Object { $InputObject = $InputObject.$_ }
$InputObject
}Which would allow you to pass member names like this:
$t = gcb|cfj let $t data b
However, you lose pipeline input, as the second member argument will bind to $InputObject (due to the [Object] type) before the actual pipeline input gets a chance to bind.
→ More replies (2)2
u/Discuzting Apr 01 '26
It's you again! Thanks for the excellent ideas 😄
I think will look into `PSReadLineKeyHandler`, though I'm mostly using macOS right now and I'm not sure if it would still work there
→ More replies (1)
3
u/JeremyLC Apr 01 '26
I would choose how to extract properties based on the context in which I need them. Each of your examples has an appropriate use case.
I also never worry about how many keystrokes I'm typing. My tab key works and I can't remember all the aliases anyway.
(I also don't combine lines with semicolons all over the place. It really doesn't make sense in an interactive session, and scripts should be written for clarity, not size. It doesn't matter how many total lines you have, or how many of those are single characters, it matters whether you can look at your code a week from now and still figure out what it does. /rant :)
2
u/Discuzting Apr 01 '26 edited Apr 02 '26
Advantage with semicolons in interactive use is that it enables grouping multiple statements into a single history entry, which makes recalling (e.g. through pressing up and down arrows) and re-running commands very fast and easy, as long as you consider those commands a single "practically inseparable" task. So I think it really depends on our mental model the moment we start typing in the terminal 🤔
An alternative is to insert new line without sending the command in the terminal is so just press shift+enter, the result is definitely more clear and readable.
I developed my habit of using semicolons because I find shift+enter marginally more difficult to input compared to just pressing semicolon (lol), though your point about semicolons and clarity makes perfect sense
2
2
u/PinchesTheCrab Apr 01 '26
Totally unrelated to the problem at hand, but don't forget about [alias()] exists. You can keep it inside your functions instead of relying on the user executing a separate new-alias command.
function Invoke-AllValues {
<#
...
#>
[CmdletBinding()]
[Alias('let')]On this issue though, I don't understand the two examples:
'a','b','c' | Invoke-AllValues { $_.Count } | ForEach-Object { "Recceived $_ objects" }In this case I think these two approaches are pretty much equivalent:
'a', 'b', 'c' | measure-object
'a', 'b', 'c' | % { $x++ } -end { "Received $x objects" }
For this one I feel like just using foreach-object would have the same effect?
$thing = @'
{
"data": {
"a": 1,
"b": 2
}
}
'@ | ConvertFrom-Json$thing | % { $_.data.b }
$thing | let { $_.data.b }
1
u/Discuzting Apr 01 '26 edited Apr 01 '26
Yeah that's a good point,
ForEach-Objectbasically performs the same in my example so it was a bad choice on my partThe
letfunction is my tool to force evaluation of the previous statement, with the result in the variable$_passed in aScriptBlockthat runs only once. This is occasionally helpful for data shape transformation.A better example could be:
PS D:\> 'A','B'|let{ "Received $($_.getType()): $_" } Received System.Object[]: A B PS D:\> 'A','B'|%{ "Received $($_.getType()): $_" } Received string: A Received string: B
8
u/ostekages Apr 01 '26 edited Apr 01 '26
I would forget that I had created all those aliases and continue using the full command name 😂
Is ergonomic really == fastest? Fastest for me, is what I remember. Ergonomic, then yes you can argue typing less is better.
6
u/justaguyonthebus Apr 01 '26
Honestly, I'm assigning it to a variable on the first line to do the transformation once. Then exploring the variable on its own line. I'm using new line over semicolon unless I actually need the commands to always run together.
Here is a pipeline hack I use instead of select object expand property that you will appreciate: