Powershell: Splatting is messed up by Powershell's implicit coercion of arrays to non-arrays

Created on 16 Oct 2018  路  6Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

$x = @("foo") | % { "Hello, $_" }
echo @x

Expected behavior

Output:

Hello, foo

Actual behavior

PS /home/ankh> echo @x
H
e
l
l
o
,

f
o
o

Environment data

> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.1.0
PSEdition                      Core
GitCommitId                    6.1.0
OS                             Linux 4.18.12-arch1-1-ARCH #1 SMP PREEMPT Thu Oct 4 01:01:27 UTC 2018
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Workaround

echo @($x)
Issue-Question WG-Language

All 6 comments

This is a bit of an edge case, really. It requires that one splat a single string as though it contained multiple arguments.

Pretty uncommon, especially given that the primary use case is for hashtable splatting, with array splatting being used by practically nobody.

Still... Weird, I suppose, but you've not stored an array into the variable.

It's fairly common knowledge that PowerShell unwraps one-element arrays. The reason for that is down to how the pipeline works, fundamentally. It unwraps the array and sends each object down the pipeline, individually. As soon as this output begins being sent over a pipeline, the original array is gone. There is no data of whether it originated form a pipeline command, single input, array, whatever.

The only thing PS has to work with to determine the final shape of data once you start using a pipeline is the number of objects you're working with. Thus, one-element arrays are indistinguishable from a bare single object,

Pretty uncommon, especially given that the primary use case is for hashtable splatting, with array splatting being used by practically nobody.

I don't know what data you're relying on for this, but I'm pretty sure I'm somebody, and have been using array splatting frequently 馃檪. A lot of non-Powershell executables rely on positional arguments, so maybe if you're only working within the domain of Powershell cmdlets and scripts it's unnecessary.

you've not stored the array into the variable

How would I do this? I thought $x = @("foo") | % { "Hello, $_" } was storing the array into a variable. Just in case I tried $x = @("foo"); $result = $x | % { "Hello, $_" }; echo @result to store stuff in variables extra hard, but that doesn't seem to work either. In the original use case the array (or apparently non-array, if single element) is ConvertFrom-Json-ed and stored into a variable, which is then splatted on the next line.

Thus, one-element arrays are indistinguishable from a bare single object

Yes, that is what this issue is about. Another way to put this is that one-element arrays don't exist at all. Most of PowerShell's machinery is therefore built to help non-array values pretend they're one-element arrays (e.g. "foo".Count is 1), but in the case of @ this falls apart.

Yeah, I avoid external executables when possible. It's much more painful to have to deal with the text output, and there's almost always a PowerShell native option these days.

One-element arrays do exist, but if you want to pass them over the pipeline as-is and retain the array you need to prevent them being unwrapped.

@(10) | % { $_.GetType() } # unwraps
,@(10) | % { $_.GetType() } # unwraps, leaving the array intact
@(@(10)) | % { $_.GetType() } # unwraps first layer, leaving inner array untouched

So, as I was saying, you weren't really storing the array into your variable in your examples. You were storing the result of the pipeline expression, which is not an array.

Indeed, array-based splatting is useful for external programs, given that hashtable-based splatting is virtually useless in that case: #9343

Array-based splatting has additional challenges: #6360

@masaeedu

The simplest way to avoid the problem is to simply pass the array (or scalar) _as-is_ - you generally don't need the @ syntax for external programs:

$x = @("foo") | % { "Hello, $_" }
echo $x   # works with a scalar $x as well as an array

The only time use of an array as-is vs. with @ makes a difference is that you need the latter syntax to pass --% symbol (stop-parsing symbol) through (thought I'd generally avoid that, not least due to its awkwardness on Unix-like platforms).

In that event, it's simplest to make $x an array by type constraint:

[array] $x = @("foo") | % { "Hello, $_" }
echo @x   # OK

@var trying to enumerate a single _string_ argument and passing its individual _characters_ as arguments is definitely pointless, and, to me, worth fixing.

@vexx32

Yeah, I avoid external executables when possible.

That may work fine on Windows, where few capable CLIs are natively available - though in an increasingly cross-platform world installing 3rd-party CLIs is increasingly common there too.

The Unix world has always had a wealth of capable (terminal-based) utilities, and while they are usually not as sophisticated as PowerShell cmdlets - and text-based, as you point out - they have one crucial advantage: vastly superior speed.

My guess is that there are many tasks that PowerShell is capable of performing in principle - and typically more elegantly - but are impractical with large datasets due to poor performance; Select-String vs. grep performance is one example that comes to mind.

Therefore, I think that frictionless integration with external utilities is an important aspect of PowerShell adoption on Unix-like platforms, and we currently definitely fall short there, most notably due to the broken passing of arguments with embedded " chars. to external programs, as detailed in this docs issue and this Stack Overflow answer.

A quick aside, @vexx32

@(@(10)) | % { $_.GetType() } # unwraps first layer, leaving inner array untouched

Nesting @(...) has no effect, so @(@(10)) is the same as @(10), i.e., a single-element array that is unwrapped in the pipeline.

Yep, that wouldn't work heh. Needs a unary comma in there somewhere. :smile:

Was this page helpful?
0 / 5 - 0 ratings