Powershell: Allow type constraints in foreach headers

Created on 25 Apr 2018  路  8Comments  路  Source: PowerShell/PowerShell

Putting a type constraint in the LHS of a foreach header (where the enumeration variable is defined) doesn't work. In fact, the parser gets very confused. See below:

Steps to reproduce

> foreach ([int]$i in 1..10) { $i }

Expected behavior

1
2
3
4
5
6
7
8
9
10

Actual behavior

At line:1 char:10
+ foreach ([int]$i in 1..10) { $i }
+          ~
Missing variable name after foreach.
The correct form is: foreach ($a in $b) {...}
At line:1 char:18
+ foreach ([int]$i in 1..10) { $i }
+                  ~~
Unexpected token 'in' in expression or statement.
At line:1 char:26
+ foreach ([int]$i in 1..10) { $i }
+                          ~
Unexpected token ')' in expression or statement.
+ CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingVariableNameAfterForeach

Environment data

> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.1.0-preview.1
PSEdition                      Core
GitCommitId                    v6.1.0-preview.1-61-gca5aacca16a18394427a411d1ca21e88a7d8a37b
OS                             Microsoft Windows 10.0.16299 
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Issue-Enhancement WG-Language

Most helpful comment

A validation failure should be an exception (so e.g. it could be caught), so termination of the loop makes sense.

All 8 comments

@rjmholt I have no objections to adding this but I'm curious why you want to? Completeness perhaps? We've thought about it before but it never seemed particularly compelling, especially since the loop doesn't introduce a new scope the way it does in C#. Also note that you _can_ add a type-constraint to the variable by simply initializing it before the loop as in:

[int] $i = 0
foreach ($i in 1, "two", 3) { "i is $i" }

This code will error when it hits "two".

My only real motivation is the fact that the error messages the parser spits out when you try this are pretty incoherent. So ideally we fix that so the parser has a better concept of what the user's intent was and goes to the correct error state.

But if we're putting the work in there, it seems like we may as well support the actual feature. The $i part of foreach ($i in 1,2,3) {} constitutes a variable definition, so adding this we can better claim that variable definition can by constrained with a type in PowerShell. It's not obvious what we buy in the case of [int], but in a more complex case, perhaps with a heterogeneous list, the type constraint could aid static analysis and completions, or provide a simple mechanism for coercive assignment. There are other ways to achieve it, but this is maybe pithier.

To be clear, I'd like to fix the error messages. The full feature isn't necessarily something I want, but seems worth discussing.

The error message actually seems clear to me. But I'm special :-)

constitutes a variable definition

Well - we don't actually have variable definitions in PowerShell, except for class variables. But the variable in a foreach loop does constitute an assignment and we allow you to attach attributes to a variable during an assignment so it's reasonable to support this

foreach ([int] [validaterange(0, 1000)] $i in 1..100) { $i }

But of course the attributes on a variable are manipulable objects not a fixed definition as in:

PS[1] (131) > [int] [validaterange(0,1000)] $x = 123
PS[1] (132) > (gv x).Attributes

MinRange MaxRange TypeId
-------- -------- ------
       0     1000 System.Management.Automation.ValidateRangeAttribute
                  System.Management.Automation.ArgumentTypeConverterAttribute


PS[1] (133) > $vra = (gv x).Attributes[0]
PS[1] (134) > (gv x).Attributes.Remove($vra)
True
PS[1] (135) > (gv x).Attributes

TransformNullOptionalParameters TypeId
------------------------------- ------
                           True System.Management.Automation.ArgumentTypeConverterAttribute


PS[1] (136) > $x = 2000
PS[1] (137) >

Ah that's really cool! Thanks!

One question I have here is what should happen if a validation attribute on the variable fails? Does the loop terminate or does it keep going and just spit out errors.

I have a working prototype here, which I've implemented with fairly minimal changes.

A couple of examples:

> foreach ([int]$x in 1..10) { $x }
1
2
3
4
5
6
7
8
9
10
> foreach ([int][char]$x in 'a'..'z') { $x }
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

And the current semantics terminate the loop when an error occurs in the validation (I think):

> foreach ([int]$x in '1','2','three','4') { $x }
1
2
Cannot convert value "three" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:10
+ foreach ([int]$x in '1','2','three','4') { $x }
+          ~~~~~~~
+ CategoryInfo          : MetadataError: (:) [], ArgumentTransformationMetadataException
+ FullyQualifiedErrorId : RuntimeException
> foreach ([validaterange(1,5)][int]$x in 1..10) { $x }
1
2
3
4
5
The variable cannot be validated because the value 6 is not a valid value for the x variable.
At line:1 char:10
+ foreach ([validaterange(1,5)][int]$x in 1..10) { $x }
+          ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : MetadataError: (:) [], ValidationMetadataException
+ FullyQualifiedErrorId : ValidateSetFailure

But wondering how others feel about this. Or does this just prove that attributes on enumeration variables are silly?

A validation failure should be an exception (so e.g. it could be caught), so termination of the loop makes sense.

@rjmholt love the look of this -- it's always been a bit of a weird quirk that foreach-defined variables are kind of type-less.

PR when? 馃槃

I have a branch for this, but I was hitting a very odd error in PowerShell's variable analysis code. At that point other priorities caught up with me. I'll see if I can look into it again at some point.

Was this page helpful?
0 / 5 - 0 ratings