Skip to content
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

$PSBoundParameters for [switch] integers #19580

Closed
237dmitry opened this issue Apr 27, 2023 · 29 comments
Closed

$PSBoundParameters for [switch] integers #19580

237dmitry opened this issue Apr 27, 2023 · 29 comments
Labels
Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-No Activity Issue has had no activity for 6 months or more

Comments

@237dmitry
Copy link

237dmitry commented Apr 27, 2023

Summary of the new feature / enhancement

Add possibility for integers as switch parameters.

For example I want to use switch parameters like -8, -16, -256 in script.

param (

    [Parameter()] [switch] $8,
    [Parameter()] [switch] $16,
    [Parameter()] [switch] $256
)

switch ($PSBoundParameters.Keys)
{
      '8' { 8 }
     '16' { 16 }
    '256' { 256 }
}

But I get the error:

./script -8
script.ps1: A positional parameter cannot be found that accepts argument '-8'.

That is, -8 is interpreted as an argument

The workaround:

param (

    [Parameter()] [int] $8,
    [Parameter()] [int] $16,
    [Parameter()] [int] $256
)

switch ($PSBoundParameters.Values)   # $PSBoundParameters.Keys works by parameter position, wrong result.
{
      -8 { 8 }
     -16 { 16 }
    -256 { 256 }
}
./script -256 -16
256
16

But it would be better if the names of switch (and not only) parameters of this kind were perceived as they are. Literally.

Proposed technical implementation details (optional)

No response

@237dmitry 237dmitry added Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. labels Apr 27, 2023
@scriptingstudio
Copy link

scriptingstudio commented Apr 27, 2023

from Windows PowerShell Language Specification Version 3.0
https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-02?view=powershell-7.3#234-parameters

2.3.4 Parameters
Syntax:
command-parameter:
dash   first-parameter-char   parameter-chars   colonopt
...

The first parameter's symbol must be a char.

This specification shows the syntax of the PowerShell language using a lexical grammar and a syntactic grammar.

@237dmitry
Copy link
Author

The first parameter's symbol must be a char.

I know.

@mklement0
Copy link
Contributor

mklement0 commented Apr 27, 2023

@237dmitry

To provide context:

  • PowerShell - surprisingly - allows declaring parameter names that start with a digit,
  • but on invocation you cannot bind them with named arguments.

Given that switches are always named (with an implied value), workarounds are limited and cumbersome.
See:

The only - surprising - exception is when calling via pwsh -File, where you can bind such parameters by name, but - and that is unequivocally a bug - you cannot pass values such as -1, i.e. negative numbers, as positional arguments.

@237dmitry
Copy link
Author

your workaround doesn't work

This works in my specific case. This workaround is for that.

This issue is for improvement, not for discussion, it's a desired goal. I just hope that someday these wishes can be realized. Consider this a note.

@mklement0
Copy link
Contributor

mklement0 commented Apr 27, 2023

I understand what you're trying to do.

I was providing context for your feature request - which is important when it comes to deciding whether or not the request should or can be implemented.

Consider this a note too.


As for the workaround:

Yes, it is incidental to the proposal, but it is still of interest to those who'd also like to see your feature request implemented - either for use in the interim or indefinitely, if the feature request gets declined.

This works in my specific case

You're right - I missed that you're actively relying on positional parameter binding, irrespective of what parameter the arguments bind to (I've removed my claim that it doesn't work from my previous comment).

If you're happy with the workaround despite its limitations (discussed below), you can stop reading.
Consider this another note.

Just to note the constraints of this workaround:

  • You can pass imaginary switches such as -42 to your script, which will be accepted, unless you perform manual validation.
  • The syntax diagram of your script won't show switch syntax and will instead suggest that your pretend switches require a value ([[-8] <int>])

The following addresses these constraints, but is hampered by the fact that the auxiliary parameter used cannot currently be hidden from the syntax diagram (you can only exclude it from tab-completion, via NoShow) - which happens to be the subject of another feature request:

param (
    [switch] $8,   # These are now here just for the syntax diagram.
    [switch] $16,
    [switch] $256,
    # Collect all (unbound) positional arguments expected to contain
    # the switch "names" (negative numbers)
    [Parameter(ValueFromRemainingArguments, DontShow)]
    [string[]] ${(ignore)}
)


switch (${(ignore)})
{
      -8 { 8 }
     -16 { 16 }
    -256 { 256 }
    default { 
      if ($null -ne $_) { throw "Unknown switch: $_" }
    }
}

@mklement0
Copy link
Contributor

P.S., @237dmitry:

I suggest updating the title of this issue to be more descriptive, along the lines of "Consider allowing (switch) parameter names that start with a digit / look like numbers"

@jhoneill
Copy link

So if I declare

Function foo {
param {
$a, 
[switch]$1
)
# etc 
}

In foo -1 be is -1 a value for a or the switch .

I'm curious to see a piece of code where having numbers as switch names is a good user experience. As in #17767 it would be better if script writers were told not to do this.

As a style point it is not good things which are possible values for the same thing as different switches (e.g. -HighPriority and -LowPriority) and I would guess that switches here are values. A single array parameter with a validate set might be better/

@237dmitry
Copy link
Author

I'm curious to see a piece of code where having numbers as switch names is a good user experience.

I combined three scripts -- colors-256, colors-16 and colors-8. They output the color palettes of the console. I thought it would be better (prettier) to switch between functions with -8, -16 and -256. The first options were -c8, -c16 and -c256, which is ugly in my taste.

@mklement0
Copy link
Contributor

mklement0 commented Apr 28, 2023

It's a different world - standard Unix utilities - but options that are numbers are not uncommon there; e.g.:

  • xargs -0 causes the input to be treated as NUL-separated
  • ls -1 results in line-by-line output.

But in general I agree that consistent resolution one way or the other is called for:

  • Either: disallow even declaration of parameters with names that look like numbers (in which case the invocation problem is moot).
    As discussed in Parameter declarations are accepted with names that cannot be bound with named arguments, such as those that look like negative numbers #17767, this could break scripts that use $1, ... parameters solely for positional binding.

  • Or: fully support them - at least for switches.

    • As for the potential ambiguity in the foo -1 example: It can be resolved by giving precedence to the interpretation as a parameter name - this is how it already works in commands with ValueFromRemainingArguments logic; e.g., Write-Host foo -NoNewline vs. Write-Host foo -NoSuchParam

    • Things get more complicated for non-switch "number-named" parameters, namely if they're also declared to be positional: e.g., with an [int]-typed positional -1 parameter and a second positional [int] parameter , does foo -1 42 mean "pass 42 to -1" or "pass -1 to -1, and 42 to the second positional parameter"?
      An easy way to avoid this is to limit "number-name" support to switches - but perhaps this constraint is obscure.

@jhoneill
Copy link

jhoneill commented Apr 28, 2023

I'm curious to see a piece of code where having numbers as switch names is a good user experience.

I combined three scripts -- colors-256, colors-16 and colors-8. They output the color palettes of the console. I thought it would be better (prettier) to switch between functions with -8, -16 and -256. The first options were -c8, -c16 and -c256, which is ugly in my taste.

So really it is -colors 8 -colors 16 or -colors 256 , so a colors parameter with a validate set and a default value (and the option to run as cmdName 16 without the - sign) .

I wrote about this :-) https://jhoneill.github.io/powershell/2019/12/31/Data-in-the-data.html possibly the parameter sets thing at the start of that was from batting things back and forth with @mklement0 my memory on that is hazy - but not having a ✔️ for each possible value whether that's boolean DB columns or PowerShell switches is still a good design tip. But as that explains code often evolves that way.

PowerShell (or PWSH if you prefer). Devops (especially Azure Devops), Photography, and general thoughts

@237dmitry
Copy link
Author

Friends, you are taking this too seriously. I wrote that this is not a requirement, but only a point from (to) the wishlist. Maybe someday there will be a question about reworking the system of parameters and arguments, then the developers might pay attention to what ordinary users want. And in pwsh-20.4.0-preview.3 it will be possible to combine alphabetic parameters, numbers, and so on.

It's a different world - standard Unix utilities - but options that are numbers are not uncommon to there

ping and ssh for example.

PowerShell is a shell first and foremost, and its syntax should be short. All aliases are difficult to remember, and parameters are much more difficult. Compare:

ln -s ./file.txt ./dir/file.txt
New-Item -ItemType SymbolicLink -Path ./dir/file.txt -Target ./file.txt

ping -4c 4 example.com
Test-Connection example.com -IPv4

scp -4 user@comp:path/to/file /path/to/local
# Never used *-pssession And do not know.

@mklement0
Copy link
Contributor

I agree that at least allowing switches to have numeric names would be beneficial.

I also agree that concision and interactive convenience are important, and that the parameter-restructuring discussion is incidental:

  • ls is an example of having only one such option, so it would be awkward to require users to specify both a name and a value.
  • Even with multiple such options, it is convenient to have direct, switch-based shortcuts, as in ping -4 and. ping -6 rather than say, ping -protocol 4 and ping -protocol 6
    • And these two approaches aren't even mutually exclusive: in the existing cmdlets there are several examples of such shortcut switches that exist in parallel with the one-parameter-with-multiple-predefined-values approach, as previously discussed; e.g.:

      • In Get-ChildItem: -Directory / for Attributes -Directory, -File for -Attributes !Directory, -Hidden for -Attributes Hidden, -ReadOnly for -Attributes ReadOnly, -System for Attributes System.

      • In Import-Module: -Global for -Scope Global ("The Global parameter is equivalent to the Scope parameter with a value of Global." - help).

      • Get-Location -Stack for Get-Location -StackName ''

@237dmitry
Copy link
Author

such shortcut switches that exist in parallel with the one-parameter-with-multiple-predefined-values approach,

This is where the trouble lies. When a combine is made from one сmdlet. Yes, this is not Unix-way, which, in fairness, has also moved away from the one-command-one-action paradigm a long time ago.

@mklement0
Copy link
Contributor

@237dmitry, it sounds like you're talking about something else.

My point was: We shouldn't focus on how number-named switches may be avoided by different parameter design, but on whether it's useful to allow them in principle, given that a different parameter design is sometimes the wrong solution, and at other times not the only solution.

@237dmitry
Copy link
Author

but on whether it's useful to allow them in principle

I think yes it would be useful, but currently it's not possible due to the very concept of parameter handling. But why not add a parameter and argument parser that will determine which is which? For example, if no arguments are specified for a named parameter, then it may well have any name without delimiters in it. It's just that this name is compared with the declared one.

 $ & {
         param (
             [int] $int,
             [switch] $3,
             [string] $string
     )
     
     $int
     $3.IsPresent
     $string
     } -i -3 -3 -s -3
-3
False     # should be True
-3
-i -3    # -3  is the first argument of -int
-3       # no arguments and declared in param block
-s -3    # -3 is the first argument of -string

@jhoneill
Copy link

My point was: We shouldn't focus on how number-named switches may be avoided by different parameter design, but on whether it's useful to allow them in principle, given that a different parameter design is sometimes the wrong solution, and at other times not the only solution.

Really those are opposite sides of the same coin.

If someone wrote a command with switches -ForegroundRed -ForegroundGreen -ForegroundBlue -ForegroundYellow etc, we'd say WOAH! Stop! have a -Foreground parameter with values. A switch should be "select behaviour" although sometimes it is "Set value to one of two options". So for example in the importexcel module we have -Bold -Italic -Underline and those set properties, and -Bold:$false removes bold. (No switch leaves bold as it is).

Encouraging people to design commands well usually avoids the need for names that start with numbers. (That's avoidance through different design). If the initial design had allowed numbers people writing style guides would say "Don't do that". Is that a reason to say "On principle parameters beginning with numbers should never be made possible" ? Actually I don't think so if bad code is convenient people should write it. But if the language makes people write better code as a happy accident, then that probably should not be undone.

For Ping -Use_IPV4_instead_of_IPV6 being written as -4 the parameter which would begin with a number is truly a switch and PowerShell would need to be -v4 (actually @237dmitry there doesn't seem to be -IPV4 for Test-NetConnection, and really if you want a ping or tracert use ping or tracert. If you want to process ping/traceroute responses, or test connecting to a port, or have a cross platform script - because ping, netstat etc have different switches on Windows and linux thanks 30+ year old choices - then use Test-NetConnection - and for your other example I don't know why there's a wrapper for new-item to create directories, but not one for it to create links.)

Executables which do their own command-line parsing (linux tools and Windows ones) can process arguments any way they please, but this has given rise to some dreadful user experiences. -0 says nul is a delimiter in one command ... OK but other characters can be delimiters and import-csv for example has -Delimiter which can specify semi-colon, tab, or nul in place of "," Sure it could have -0 but do we have -8 for tab and -59 for semi colon. ?
-1 says output one line at a time in another command, but there's no -10 to say output in blocks of 10 (which has the common parameter out-buffer in powerShell), and it doesn't work for ps the way it does for ls.

But of all things PowerShell developers could spend time on where would we rank changing argument parsing to work with parameters which start with a digit? I'd say it's too low to ever get time assigned to doing it.

@237dmitry
Copy link
Author

But of all things PowerShell developers could spend time on where would we rank changing argument parsing to work with parameters which start with a digit? I'd say it's too low to ever get time assigned to doing it.

Maybe someday they will be rewrite powershell concepts and then will be changed rules of parameters and arguments. Once more time, I do not demand it right now, I only express hope for the future. In a year, after ten years. Why are you discussing something that you think will never happen? The developers themselves say that the declarative agreement on backward compatibility with Windows PowerShell is ending, which means that the syntax and rules will inevitably change. Already, there are thousands of differences, and in fact only visibility remains from compatibility.

@mklement0
Copy link
Contributor

Really those are opposite sides of the same coin.

  • Again: Given that in a given case only a single number-named switch may be required and that even in other cases distinct convenience switches are useful (who would want to give up Get-ChildItem -File and Get-ChildItem -Directory for Get-ChildItem -Attributes ...? I certainly wouldn't), the parameter-design discussion is a distraction.

  • So the only questions are:

    • Should parameter names such as -1 be properly supported - not half-supported, they way they currently are (positionally only)?
    • If so, should they be limited to [switch] parameters?
      • If so, trying to even declare non-[switch] parameters with such names should be prevented.
    • If not:
      • Should the positional-binding loophole be closed, to avoid confusion?
      • If not, the current confusing half-support must be documented.
        • However, it's important to note that documenting counterintuitive and/or inconsistent is no substitute for intuitive, consistent beahvior.

@237dmitry
Copy link
Author

should they be limited to [switch] parameters?

Yes, the request is only for [switch]es, I don't see another role for the -\d+-like parameters. I made this request after failing and looking for a workaround (that actually shouldn't work either).

@ImportTaste
Copy link
Contributor

ImportTaste commented May 2, 2023

@mklement0

The only - surprising - exception is when calling via pwsh -File, where you can bind such parameters by name, but - and that is unequivocally a bug - you cannot pass values such as -1, i.e. negative numbers, as positional arguments.

Actually, there is another exception, and I'm surprised no one else has noticed this: parameter splatting.

function Test-IntParam {
  param( [switch]$2 )
  $PSBoundParameters
}

$Params = @{
  2 = $true
}

Test-IntParam @Params

Implementing this elsewhere seems like it would break something fundamental with its parsing of negative numbers as integers.

What I would recommend advocating for instead is in-line splatting, so you could do something like Test-IntParam $(@{2=$true}), or something with a more convenient splatting syntax like $@{2=$true} or @@{2=$true}, but the last time it was proposed (#10073), back in 2019-2020, it was rejected, as stated here:

#10073 (comment)

@PowerShell/powershell-committee discussed this in context of RFC0002 where we decided that we would withdraw RFC0002 with the exception of fixing the issue to allow explicit parameters to override a splatted hashtable and discuss named parameters for .NET method invocation as a separate issue. So in light of that, we will be closing this PR as discussed with @bergmeister

Honestly, I'm bothered that the ruling only touched on it in the context of explicit parameters overriding splatted hashtables. It feels like its other merits were lost in the weeds. Might be worth bringing up again, and not just because it would allow for what you're wanting, but it would be a nice bonus.

@mklement0
Copy link
Contributor

@ImportTaste

Good point, re splatting - I had never tried that.
So let's summarize the current behavior with number-named [switch] parameters, specifically:

# Create sample script with a switch named -1
'[CmdletBinding()] param([switch] $1) "-1 value: $1"' > t.ps1

# OK: CLI call with -File
# -> '-1 value: True'
pwsh -NoProfile -File t.ps1 -1

# OK: splatting
# -> '-1 value: True'
$namedArgs = @{ 1 = $true }
./t.ps1 @namedArgs

# !! BROKEN: direct invocation
# -> "A positional parameter cannot be found that accepts argument '-1'"
./t.ps1 -1

Two out of three ain't bad!

To me, the splatting discussion is a separate one (I liked @KirkMunro's proposal - see PowerShell/PowerShell-RFC#179 - which was an alternative to splatting without an intermediate variable, but it was declined), because a parameter that can solely be bound via splatting (in-session) is not worth having.

Implementing this elsewhere seems like it would break something fundamental with its parsing of negative numbers as integers.

Not if you limit support to [switch] parameters, from what I can tell - simply give precedence to interpreting -<number> tokens as parameter names, and fall back to positional negative numbers.
(It does get tricky with other types, as discussed above).

To narrow my previous summary down to what I think are the only sensible options:

  • Either: Close the current loophole and prevent even declaring number-named parameters (of any type); i.e., fail at parse time.

    • This assumes that the risk of breaking existing code is so small that it makes it a bucket 3 change.
  • Or: Accept number-named parameters, but for [switch] parameters only (for attempts to use other types, fail at parse time too).

    • Given that there wouldn't be ill effects (unless I'm missing something) and given that such switches are well-established in the world of common CLIs, that makes sense to me.

Either way, documentation is needed - at the very least to document the current inconsistencies / limitations.
And if no action is taken, this documentation becomes all the more important.

@IISResetMe
Copy link
Collaborator

Here's another way to work around this limitation - accept the negative integer values as-is:

function f {
  param(
    [Parameter(Mandatory=$false, DontShow = $true, ValueFromRemainingArguments = $true)]
    [int[]]$NumSwitches
  )

  foreach($opt in -8, -16, -256){
    if ($NumSwitches -contains $opt){
      Write-Host "'$opt' present" -ForegroundColor Green
    }
  }
}

f -8 -256

Outputs:

'-8' present
'-256' present

@mklement0
Copy link
Contributor

True, but unfortunately you neither get tab completion that way nor do the permitted switches show up in the syntax diagram. Hence my attempt above, with nominal switches to support tab-completion and for display in the syntax diagram - with the blemish that you can't hide the aux. ValueFromRemainingArguments parameter from the syntax diagram (as noted, the ability to hide parameters from the syntax display would be generally beneficial).

@IISResetMe
Copy link
Collaborator

IISResetMe commented May 4, 2023

You can get sorta-janky tab completion with a bit of help from [ValidateSet]:

param(
    [Parameter(Mandatory=$false, DontShow = $true, ValueFromRemainingArguments = $true)]
    [ValidateSet(-8, -16, -256)]
    [int[]]$NumSwitches
  )

I recognize it's not quite what's being requested here, but I'm far from convinced we have to choose between disallowing digit-only variable path parameters or make a special case for [switch]-typed parameters - the status quo strikes a much better balance in my view. It seemlessly enables the probably-way-more-common use case of passing negative integer values to script functions and cmdlets as positional arguments, without affecting external applications that might want to consume such tokens as switch flags.

@mklement0
Copy link
Contributor

Fair enough, but that still leaves the syntax-diagram problem.

  • For a workaround that doesn't compromise the user experience, the ability to hide aux. parameters from the syntax diagram would have to be implemented (which is worth doing either way) - see Can we hide obsolete or internal-use-only helper parameters from syntax diagrams? #7868

    • This still makes for a cumbersome author experience.
    • And it relies on the continued to ability to define - largely useless - number-named [switch] parameters such as -1; their primary use would then be to facilitate the workaround.
    • The fact that they can be bound via splatting and via the CLI makes the workaround even more cumbersome.
  • To me there is no balance in the status quo: it is clearly a broken state of affairs: you're allowed to declare parameters you cannot fully use as such - except if you jump through obscure hoops.

  • If such parameters are disallowed altogether, a non-end-user-experience-compromising workaround isn't possible anymore (no syntax-diagram support, only tab completion) - but if the intent is to discourage such parameters, that should be acceptable.

  • If the loophole is partly closed and number-named switch parameters, specifically, are allowed and fully supported, we have an exception on our hand, true, but it strike me as a sensible one: it's hard to imagine anyone wanting to design a -1 parameter that accepts negative numbers, for instance. Even if they want to, it would be a sensible restriction, and one that would be enforced by the language, allowing for a specific parse-time error message.

I'm unclear on how you think external applications factor into this, given that the concept of parameters and named arguments doesn't apply there.

Copy link
Contributor

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

1 similar comment
Copy link
Contributor

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Resolution-No Activity Issue has had no activity for 6 months or more label Nov 15, 2023
Copy link
Contributor

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

Copy link
Contributor

This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-No Activity Issue has had no activity for 6 months or more
Projects
None yet
Development

No branches or pull requests

6 participants