-
Notifications
You must be signed in to change notification settings - Fork 129
Multi line continuation #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
53b8b3a
b1880c9
95051f0
ed40b38
aef259b
d8c5ea0
f47942f
f84afbb
7211f19
65e4ba3
bbc4c0a
e23bd9d
2c3339f
c60b35a
a45c1b8
aa0b5ce
40ba368
f03b2fa
8b57249
a1e4c0e
d7f3899
bd5cd89
bfb4d7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
--- | ||
RFC: RFCnnnn | ||
Author: Kirk Munro | ||
Status: Draft | ||
SupercededBy: N/A | ||
Version: 0.3 | ||
Area: Parser/Tokenizer | ||
Comments Due: June 16, 2019 | ||
Plan to implement: Yes | ||
--- | ||
|
||
# Multi-line continuation | ||
|
||
Consider this example of a New-ADUser command invocation: | ||
|
||
```PowerShell | ||
New-ADUser -Name 'Jack Robinson' -GivenName 'Jack' -Surname 'Robinson' -SamAccountName 'J.Robinson' -UserPrincipalName '[email protected]' -Path 'OU=Users,DC=enterprise,DC=com' -AccountPassword (Read-Host -AsSecureString 'Input Password') -Enabled $true | ||
``` | ||
|
||
By itself it's not too much to handle, but in a script commands with many | ||
parameters like this can be difficult to manage. | ||
|
||
To wrap this command across multiple lines, users can either use backticks or | ||
they can use splatting. The former is a syntactical nuisance which should | ||
really only be used in situations when no other option is available. The latter | ||
is helpful, but it puts the parameters before the command, making it more | ||
difficult for less experienced users to learn/use, and all scripters lose the | ||
benefits of tab completion and Intellisense for parameters when they use | ||
splatting. | ||
|
||
As a workaround, they can work out the parameters they want to use for the | ||
command first, and then convert it into a splatted command, but that's onerous. | ||
Even though Visual Studio Code has an extension that makes splatting easier, as | ||
can be seen [here](https://sqldbawithabeard.com/2018/03/11/easily-splatting-powershell-with-vs-code/), once you've converted to splatting you still lose Intellisense | ||
for future updates unless you work from the command first and then add to your | ||
splatted collection, and that's just in Visual Studio Code. Other editors may | ||
or may not support that functionality, and users working in a standalone | ||
terminal won't have that available to them either. | ||
|
||
Instead, why not allow users to wrap commands across multiple lines in a more | ||
intuitive way without having to deal with backticks on every line or splatting, | ||
like this: | ||
|
||
```PowerShell | ||
New-ADUser @ | ||
-Name 'Jack Robinson' | ||
-GivenName 'Jack' | ||
-Surname 'Robinson' | ||
-SamAccountName 'J.Robinson' | ||
-UserPrincipalName '[email protected]' | ||
-Path 'OU=Users,DC=enterprise,DC=com' | ||
-AccountPassword (Read-Host -AsSecureString 'Input Password') | ||
-Enabled $true | ||
|
||
Get-ChildItem @ | ||
$rootFolder | ||
-File | ||
-Filter '*.ps*1' | ||
|
||
``` | ||
|
||
Of course, they could invoke external commands and pass through arguments this | ||
way as well: | ||
|
||
```PowerShell | ||
& "./plink.exe" @ | ||
--% $Hostname -l $Username -pw $Password $Command | ||
|
||
cacls @ | ||
c:\docs\work | ||
/E /T /C /G | ||
"FinanceUsers":F | ||
|
||
``` | ||
|
||
Further, by generalizing multi-line continuation with a `@` character, we're | ||
allowing users to apply line continuation the way they want to, which opens the | ||
door to more C#-like line wrapping when you're working with multiple members or | ||
methods in .NET, one after another. For example, this would work: | ||
|
||
```PowerShell | ||
$string @ | ||
.ToUpper() | ||
.Trim() | ||
.Length | ||
``` | ||
|
||
In each of these examples, the parser starts parsing the command as a | ||
multi-line command when it encounters the `@` token as the last token on the | ||
line, and in this mode command parsing stops once one of the following is | ||
found: | ||
|
||
* end of file | ||
* two newlines (as opposed to the normal one) | ||
* command-terminating token (i.e. all other ways of ending commands work the | ||
same as usual, and this does not affect other elements of the PowerShell | ||
syntax) | ||
|
||
The pros/cons to this new syntax are as follows: | ||
|
||
**Pros:** | ||
|
||
* allows the scripter to wrap commands how they see fit, while still getting | ||
Intellisense and tab completion, without using backticks. | ||
* aside from the `@` character to initiate multi-line continuation, the rest of | ||
the command is entered the exact same way it would be if it was entered on a | ||
single line. | ||
* ad hoc could support this syntax as well (PSReadline could wait for a | ||
double-enter when in multi-line command parsing mode) | ||
* no breaking changes (a standalone `@` is currently an unrecognized token in | ||
PowerShell no matter where it is used). | ||
* users can use a blank line to terminate the command, or they can opt to use | ||
any valid command-terminating token instead, so it has a proper closing | ||
character. | ||
|
||
**Cons:** | ||
|
||
* using a blank line as a statement terminator will be hard for some to accept | ||
(if you're one of those folks, read below to the alternative proposals and | ||
considerations section). | ||
|
||
## Motivation | ||
|
||
As a script/module author, | ||
I can wrap commands across multiple lines easily and intuitively without backticks or splatting | ||
so that my scripts remain easy to write and maintain while still giving me the benefits of Intellisense and tab completion. | ||
|
||
## Specification | ||
|
||
* expand the command parser to accept multi-line commands after an at symbol | ||
(`@`) is encountered at the end of a line | ||
* terminate multi-line commands when the parser encounters two newlines | ||
(rather than one), or when the parser encounters any other command-terminating | ||
token | ||
|
||
Note: | ||
* for commands that do not use the stop-parsing sigil in their arguments, | ||
command-terminating tokens include a pipe symbol, a redirection operator, a | ||
closing enclosure, a semi-colon, or a `&` background operator. | ||
* for commands that do use the stop-parsing sigil in their arguments, | ||
command-terminating tokens include a pipe symbol or a redirection operator. | ||
|
||
## Alternate Proposals and Considerations | ||
|
||
### A different sigil | ||
|
||
The original draft of this RFC included different options for the sigil that | ||
could be used to enter multi-line parameter/argument parsing mode, and others | ||
were presented in the discussion however none of the other sigils that were | ||
presented could be used without breaking changes. When considering an alternate | ||
sigil, it must be something that can be identified as a unique token without | ||
breaking commands that accept multiple strings as positional parameters, such | ||
as Write-Host (which can write many sigils to the console) or commands external | ||
to PowerShell. | ||
|
||
### Enclosures instead of a sigil | ||
|
||
Instead of only requiring a single leading sigil, some users prefer the notion | ||
of enclosures such that the multi-line command parsing mode would have a very | ||
clear and well defined start and end. To meet that need, we could follow the | ||
here-string syntax in PowerShell as an example, offering syntax like the | ||
following: | ||
|
||
```PowerShell | ||
. {"./plink.exe" @` | ||
--% | ||
$Hostname | ||
-l $Username | ||
-pw $Password | ||
$Command | ||
`@} | ||
``` | ||
|
||
All this would do is prevent newline tokens within the enclosures from being | ||
treated as statement terminators in the current statement. | ||
|
||
The closing closure could also be a recognized statement terminator even when | ||
used after the stop parsing sigil, allowing those commands to be wrapped across | ||
multiple lines as well. | ||
|
||
Like here-string enclosures, we could require that the opening closure be the | ||
last token on a line. Unlike here-string enclosures, however, it would be | ||
preferable if the closing closure did not have to be at the start of a line, | ||
since there is no need for it to be. It would simply have to be the first token | ||
on line to close the statement, allowing for indentation within scripts. | ||
|
||
This alternative also allows for blank lines to be used within the enclosures, | ||
such that Scenario 3 in @dragonwolf83's comment could be supported and written | ||
like this: | ||
|
||
```PowerShell | ||
New-ADUser @` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a breaking change because it changes what the trailing backtick means today. Users could absolutely be splitting lines after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not breaking. Here is output from preview 6: Windows PowerShell 5.1 recognizes it as a parser error as well, and since it's a parser error, this is non-breaking as things stand in PowerShell today. Also, this doesn't change the meaning of the backtick (a single |
||
-Name 'Jack Robinson' | ||
-GivenName 'Jack' | ||
-Surname 'Robinson' | ||
-SamAccountName 'J.Robinson' | ||
|
||
-UserPrincipalName ( | ||
'[email protected]' | ||
) | ||
|
||
# Get the list of regions for where a user would reside in to put the user into the correct region | ||
-Path ( | ||
$region = Get-Region -Name 'Jack Robinson' | ||
"OU=Users,OU=$region,DC=enterprise,DC=com" | ||
) | ||
|
||
-AccountPassword ( | ||
Read-Host -AsSecureString 'Input Password' | ||
) | ||
|
||
-Enabled $true | ||
`@ | ||
|
||
Get-ChildItem @` | ||
$rootFolder | ||
-File | ||
-Filter '*.ps*1' | ||
`@ | ||
``` | ||
|
||
The best part is that it doesn't appear this syntax would introduce a breaking | ||
change at all. | ||
|
||
If people feel the backtick is still not visible enough here (and therefore not | ||
desirable for this purpose), we should keep the `@` portion of the enclosures | ||
so that we can avoid breaking changes, and simply replace the backtick with | ||
something else. For example, we could do this instead: | ||
|
||
```PowerShell | ||
Get-ChildItem @- | ||
KirkMunro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$rootFolder | ||
-File | ||
-Filter '*.ps*1' | ||
-@ | ||
``` | ||
|
||
Backticks offer the advantage of continuing what they represent in PowerShell | ||
already (line continuation), and they are syntactically very similar to here- | ||
strings when used with the `@` symbol. That last point could be seen as a | ||
disadvantage as well, because they may be visually harder to distinguish from | ||
a single-quoted here-string. | ||
|
||
The `@-|-@` alternative doesn't pick up on the backtick for line continuation, | ||
but it is visually unique and easy to see in a script. The `-` character could | ||
be seen as representing the line that makes up the statement as well. | ||
|
||
### Inline splatting | ||
|
||
There has also been some discussion about the idea of inline splatting, using a | ||
format like `-@{...}` or `-@(...)`. Inline splatting has also been discussed | ||
separately on [RFC0002: Generalized Splatting](https://github.com/PowerShell/PowerShell-RFC/blob/master/2-Draft-Accepted/RFC0002-Generalized-Splatting.md), but using the syntax `@@{...}` or | ||
`@@(...)`. | ||
|
||
Here is an example showing what that might look like: | ||
|
||
```PowerShell | ||
Get-ChildItem -@{ | ||
LiteralPath = $rootFolder | ||
File = $true | ||
Filter = '*.ps*1' | ||
} | ||
``` | ||
|
||
Using inline splatting to be able to span a single command across multiple | ||
lines like this has several limitations, including: | ||
|
||
1. You cannot transition to/from the inline splatted syntax without a bunch of | ||
manual tweaks to the command (either converting parameter syntax into hashtable | ||
or array syntax or vice versa). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming we implement inline splatting, we believe that this could be implemented using a PSSA rule. E.g. it could say that any number of parameters or column width over some value could trigger a rule with an auto-format to turn the in-line list of params and their values into a splatted inline hashtable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may be helpful, but some questions back to you about this:
|
||
1. You're forced to choose between named parameters or positional | ||
parameters/arguments for each splatted collection. i.e. You can splat in a | ||
hashtable of named parameter/value pairs or an array of positional values, but | ||
you can't mix the two (the example shown just above is also used earlier in | ||
this RFC with positional parameters and switch parameters used without values, | ||
matching the way it is often used as a single-line command). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one's problematic because it's an anti-pattern to use positional parameters in maintained scripts. While it's perfectly fine to use them on ad hoc / interactive basis, all parameters should be named in scripts (and that point is even more applicable if you're talking about invocations that are long and complex enough to need something like multi-line continuation). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recognize that; however, in practice, it seems to me that anti-pattern is very common in maintained scripts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed it is very common, and I don't think it's an anti-pattern: Leaving the unfortunate
is not only perfectly reasonable, but arguably preferable to the needlessly verbose
That is, thoughtfully defined cmdlets:
With that in place, both interactive and script use benefit from the concision of not having to spell out what is intuitively implied. |
||
1. There's no way to include unparsed arguments after the stop-parsing sigil in | ||
splatting. You can add it afterwards, but not include it within. | ||
KirkMunro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
1. Splatting requires a different syntax than typical parameter/argument input, | ||
which is more to learn. In contrast, the proposal above only requires learning | ||
about the `@` sigil (borrowed from splatting, but without specifying hashtables | ||
or arrays -- just allow all content until a newline), reducing the learning | ||
curve and allowing users to use parameters the same way in either case. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mostly the same point as your first in this list, and we think usage and discoverability could be fixed in tooling. Furthermore, new operators are notoriously difficult to discover, so I'm not convinced that you wouldn't be in the same boat with a new sigil. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Splatting and multi-line continuations are really distinct use cases, and you shouldn't have to use the former to achieve the latter:
(a) strikes me as far more common than (b) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another way of looking at it: if we had good support for (a), as proposed, then perhaps we wouldn't need the inline splatting variant at all, as its primary purpose seems to be to (indirectly) address (a) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly this. Inline splatting feels like the wrong solution to the problem. Splatting's intent is not for code readability. |
||
1. Inline splatting attempts to resolve the issue for commands with arguments, | ||
but it does nothing for other scenarios where you want specific line wrapping | ||
other than the defaults that PowerShell implicitly supports. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a little annoying, but you can still do this with inline splatting by using a subexpression ( Get-Foo @@{
a = 1
b = $(
statement1;
statement2;
)
} |
||
|
||
Further, unlike using a leading sigil such as `@`, which would work with | ||
Intellisense and tab expansion as they are coded now, inline splatting would | ||
require special work to make Intellisense and tab expansion work with it. That | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is true, but we believe it's the right work to do: splatting should be better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better, yes. Able to support splatting in members or the results of methods, absolutely, there's a PR for that, and that improvement should be merged in. But when you push inline splatting as a way to make line wrapping better, you're violating the single-responsibility principle. Splatting is for command invocation with dynamic parameters that are chosen based on runtime execution. Line continuation is about more than just cmdlet or advanced function invocation. Take chained method invocation, for example, as shown earlier in this RFC. Also, I just remembered what I was trying to say about the stop-parsing sigil...with inline splatting as the way we wrap things, that's only good for cmdlet/advanced function parameters. If I want to wrap arguments passed after a stop-parsing sigil, I can't, no matter how long the line gets. But with this proposal, I would be able to do that. |
||
is not a reason not to do it, but it is more code to write and maintain. | ||
|
||
### Breaking changes | ||
|
||
No known breaking changes in the original proposal, nor the alternative version | ||
that uses enclosures. | ||
|
||
All previous options from the original RFC and the discussion about it that | ||
would have introduced breaking changes have been removed in favor of a syntax | ||
that just works to the specification without any breaking changes, regardless | ||
of how you use it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't necessarily agree with the statement that backticks should be avoided, especially when used in conjunction with PSSA and the
AvoidTrailingWhitespace
rule.I know that's contentious in the community, but a lot of the remaining justification is based on the assertion that backticks aren't desirable, thought it was worth calling out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Backticks tend to create fragile code, that is generally intolerant to further maintenance. Trailing whitespace is an easy example, but there are no use cases I've seen where their use is excusable.
If there were a dedicated line continuation token, that's a different story. Since backticks are general-purpose escape characters, I don't think it's appropriate to really use backticks for this purpose. It's the kind of tool that, sure, you can use in a pinch, but should never be a long-term solution. Sooner or later someone's going to break it, and they ways in which these break unfortunately tend not to halt execution, so you can easily end up with things breaking in more ways than just a red error message on the screen, with commands called with only half their parameters, quite by accident.
It also doesn't help that backticks are one of the hardest to spot characters available on a standard US keyboard; very easy to forget to add or remove them when editing commands written in this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. It is a solution but not the best solution to the problem.
This past month I had to modify a vbscript where they use underscores for the continuation character. I made the change, included it at first, but realized I could remove unused code and suddenly my ending line is no longer at the end. Did I add the underscore correctly? Nope!
This is a common pitfall of requiring a line-continuation character after each line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've modified the wording to make it more clear that many members consider it to be a syntactical nuisance so that it's not written as an absolute. Thanks for the feedback @joeyaiello, my changes will be in my next commit.