Treat `-Switch:$false` the same as omitting a switch when selecting a parameter set · Issue #25027 · PowerShell/PowerShell · GitHub
Skip to content

Treat -Switch:$false the same as omitting a switch when selecting a parameter set #25027

Closed

Description

Summary of the new feature / enhancement

Note:

  • The problem affects cmdlets that use [switch] parameters as mandatory in the context of an explicit parameter set, such as Get-UpTime

    • See Get-Uptime -Since ignores an explicit $false value #25015
    • In short: What this issue proposes is to treat -Switch:$false as if -Switch had not been specified, as these two forms should generally be equivalent, with the former often being used for programmatically building arguments, especially via splatting.
      • Indeed, checking $Switch.IsPresent yields $false in both cases (for thoughts about the naming of this property, see this comment).
    • Currently, -SomeSwitch:$false acts the same as passing -SomeSwitch (i.e., implied :$true) in terms of parameter-set selection, which causes all cmdlets that only check whether the relevant parameter set was selected to in effect ignore the $false argument.
      • MartinGC94's analysis here shows that quite a few existing cmdlets misbehave in this way, and the proposal at hand would presumably automatically fix that.
  • It is debatable whether this proposal should be considered a feature request or a bug report.

  • Either way, it is technically a breaking change, but - given the counterintuitive behavior and the presumed rarity of passing negated switches (-SomeSwitch:$false), it hopefully falls into bucket 3

    • In the vast majority of cases, omitting -SomeSwitch and passing -SomeSwitch:$false (which is the only way to emulate omitting the switch when splatting is used, short of conditionally constructing the splatting hashtable iteratively) should be considered equivalent.
    • (In edge cases, where -SomeSwitch:$false is designed to override a default value set elsewhere, notably via a preference variable (e.g., -Confirm:$false overriding the $ConfirmPreference variable), a different approach is already necessary, namely if, in addition to $Switch.IsPresent returning $false (which is the case when -Switch:$false is passed), whether this "non-presence" was explicitly signaled via a -Switch:$false argument rather than being implied by omission.)

To illustrate the problem:

function foo {
  [CmdletBinding(DefaultParameterSetName='NoSwitchSet')]
  param(
    [Parameter(ParameterSetName='SwitchSet')]
    [switch] $Switch
  )
  [pscustomobject] @{
    SwitchValue = $Switch
    ParameterSetName = $PSCmdlet.ParameterSetName
  }
}

# Invocation without -Switch
foo 
# Invocation with negated -Switch, which _should_ be equivalent.
foo -Switch:$false
# Ditto, via splatting
$splat = @{ Switch = $false }
foo @splat

Expected / proposed behavior:

SwitchValue ParameterSetName
----------- ----------------
False       NoSwitchSet
False       NoSwitchSet
False       NoSwitchSet

Current behavior:

SwitchValue ParameterSetName
----------- ----------------
False       NoSwitchSet
False       SwitchSet
False       SwitchSet

That is, the -Switch:$false invocations (whether directly or via splatting) unexpectedly still selected the SwitchSet parameter set.

Proposed technical implementation details (optional)

No response

Activity

  1. similar-issues-ai commented on Feb 15, 2025

    @similar-issues-ai

    We've found some similar issues:

    If any of the above are duplicates, please consider closing this issue out and adding additional context in the original issue.

    Note: You can give me feedback by 👍 or 👎 this comment.

  2. anonhostpi commented on Feb 15, 2025

    @anonhostpi

    Prior discussions:

    The main conversation point from the prior issue is what does -switch:$false mean semantically? Do the negative of the switch behavior or omit the switch behavior?

    For example, how should this be determined semantically?

    Format-OutString -italic:$true "italicize me in the console"
    Format-OutString -italic:$false "normalize me in the console"
    

    Does -italic:$false mean:

    • "DO send the ascii escape sequence for formatting reset" (negative behavior)
    • "DON'T send the ascii escape sequence for mandating italics" (omitting behavior)

    Majority of the time, the omission and negative of a switch are the same behavior, but as highlighted in the previous discussions, this may not always be the case. This can be reflected in my example. Both behaviors should result in a non-itacized string, but achieve it through 2 different modes (or behaviors)

    My personal take:

    However, despite the arguments brought up against it, I think that making -switch:$false follow an omission behavior is much more valuable, because of 2 things:

    1. Like OP stated, this could help resolve parameter sets more accurately
    2. Why would you want to use -switch:$false as a negating behavior when things like -noSwitch,-stopSwitch, or -unSwitch are shorter and require less typing?

    The only reason I can see -switch:$false being useful as a negating behavior is when a variable can be used to determine the switching behavior: -switch:$Condition.

    However, if you are designing your functions to accept boolean values, why would it not be more appropriate to just use a boolean?

    If you are designing your function to accept both a boolean or a switch, why not just use 2 parameters: -switchForConditionA -booleanForConditionA $true/$false?

  3. anonhostpi commented on Feb 15, 2025

    @anonhostpi

    This conversation gave me this idea: #25029

  4. mklement0 commented on Feb 15, 2025

    @mklement0
    ContributorAuthor

    how should this be determined semantically?

    Ultimately, that's up to each cmdlet to decide, and this proposal is unrelated to this aspect (except that the implementation of this logic may have to change if any current implementation relies on inferring the -italic:$false case from the selected parameter set - such cases, which are limited to cmdlets where the switch in question is a mandatory part of a parameter set - would have to be the subject of a bucket-3 analysis).

    That said, to me such extended semantics are best avoided (-Confirm:$false is a rare example among built-in functionality), given that the behavior is obscure and that you'll need to memorize / consult the help to find out which cmdlets apply which logic.

    Taking the Get-UpTime -Since:$false call as an example (but it applies analogously to probably most of the similarly broken cmdlets): there is no meaningful interpretation other than to consider it the same as Get-UpTime (i.e. without an argument).

    This interpretation is currently not met - i.e. the call is broken, due to ignoring the $false value - owing to the justifiable but currently mistaken assumption that selecting the relevant parameter set implies that the switch value is $true

    Therefore, making this assumption hold, as proposed here, implicitly fixes all cmdlets that currently mistakenly rely on it.


    Why would you want to use -switch:$false

    As stated in the initial post, this is about constructing arguments programmatically - whether with or without splatting.

    Yes, you can avoid the problem with additional conditionals (have two variations of the same command line / conditionally add a splatting-hashtable entry after the fact only if the switch is to be passed) - but that is a maintenance headache / an annoying inconvenience.


    why would it not be more appropriate to just use a boolean?

    [bool] parameters, while technically supported, are best avoided - users don't expect them; they're used to the switch-parameter idiom, which makes for a unified way to represent opt-in behavior. Mixing these two idioms is likely to create confusion.

  5. anonhostpi commented on Feb 15, 2025

    @anonhostpi

    What you propose is a contradictory behavior.

    I want to set/use a switch, but I don't want the function to think it was set/used?

    Additionally, switches can already be programmatically applied/omitted using splatting (which is part of why I proposed that new feature request)

  6. mklement0 commented on Feb 15, 2025

    @mklement0
    ContributorAuthor

    I'm unclear on how your comment relates to my previous one.
    (In case you're thinking that the -switch:$false case cannot be detected in the absence of the selected parameter set implying its having been specified: (a) it can, and (b) note that a switch parameter need not be (and often isn't) a mandatory part of a distinct parameter set.)

  7. anonhostpi commented on Feb 15, 2025

    @anonhostpi

    What I'm saying is that a ParameterSet is primarily dictated by what is set and used, not by the value.

    So if you use -switch, regardless of false or true, switchset is selected.

    What you are asking for is to set/use a [switch] value, but make the parameterset believe it wasn't.

    This issue might fall under, "you can't have your cake and eat it too." (Observe both omitting and negating behavior)

    Plus, say pwsh did allow for this, this would just confuse developers. Is -switch:$false supposed to be negating, omitting, or both? Or maybe there's no consistency (which is possibly a burden on both the end-user and the developer)?

  8. mklement0 commented on Feb 16, 2025

    @mklement0
    ContributorAuthor

    So if you use -switch, regardless of false or true, switchset is selected.

    That's the de facto behavior - and this proposal aims to change it.

    Conceptualizing -switch:$false as omitting -switch is straightforward, and matches the design intent of splatting / programmatic construction of an argument with -switch:$valueThatMayBeTrueorFalse.

    As noted, implementing this change will make countless bugs go away that currently - justifiably, but mistakenly - assume that the parameter set only gets selected if the switch value is, in effect, $true (whether via -switch (typically) or via -switch:$true (programmatically)), and will also prevent future bugs of the same kind.

    And, yes, that makes [switch] parameters special - they always have been.

    "you can't have your cake and eat it too."

    Yes, you can. But I advise against it, precisely for the potential for confusion, as I've previously noted, and as you seem to agree.

    In short:

    • Make -switch:$false the same as omitting -switch for the purposes of parameter-set selection - a simple rule that not only matches what cmdlet authors likely expected, but also simplifies the implementation in the typical use case, where no additional logic needs to be superimposed on the -switch:$false case.

    • Due to the obscurity of such selective superimpositions, they are best avoided (though -Confirm:$false is a high-profile exception that won't go away). However, implementing this proposal does not prevent their implementation.

  9. surfingoldelephant commented on Feb 16, 2025

    @surfingoldelephant

    Using MartinGC94's code here as a starting point, I tested the 22 commands shipped with PS v7.6.0-preview.2 in Windows that use a mandatory switch parameter to dictate the selected parameter set.

    Modules shipped with PS v7.6.0
    • CimCmdlets
    • Microsoft.PowerShell.Archive
    • Microsoft.PowerShell.Diagnostics
    • Microsoft.PowerShell.Host
    • Microsoft.PowerShell.Management
    • Microsoft.PowerShell.PSResourceGet
    • Microsoft.PowerShell.Security
    • Microsoft.PowerShell.Utility
    • Microsoft.WSMan.Management
    • PackageManagement
    • PowerShellGet
    • PSDiagnostics
    • PSReadLine
    • ThreadJob
    Results
    Command                       Parameter            FalseSwitchIgnored Source
    -------                       ---------            ------------------ ------
    Compress-Archive              Update                            False Microsoft.PowerShell.Archive
    Compress-Archive              Force                             False Microsoft.PowerShell.Archive
    help                          Detailed                          False
    help                          Examples                          False
    help                          Online                            False
    help                          ShowWindow                        False
    Register-PSRepository         Default                           False PowerShellGet
    ConvertFrom-Csv               UseCulture                        False Microsoft.PowerShell.Utility
    Get-Help                      Detailed                          False Microsoft.PowerShell.Core
    Get-Help                      Examples                          False Microsoft.PowerShell.Core
    Get-Help                      Online                            False Microsoft.PowerShell.Core
    Get-Help                      ShowWindow                        False Microsoft.PowerShell.Core
    Get-Module                    ListAvailable                     False Microsoft.PowerShell.Core
    Get-Process                   IncludeUserName                   False Microsoft.PowerShell.Management
    Get-Random                    Shuffle                            True Microsoft.PowerShell.Utility
    Get-SecureRandom              Shuffle                            True Microsoft.PowerShell.Utility
    Get-TimeZone                  ListAvailable                      True Microsoft.PowerShell.Management
    Get-WSManInstance             Enumerate                         False Microsoft.WSMan.Management
    Import-Csv                    UseCulture                        False Microsoft.PowerShell.Utility
    Import-Module                 UseWindowsPowerShell              False Microsoft.PowerShell.Core
    Invoke-RestMethod             NoProxy                           False Microsoft.PowerShell.Utility
    Invoke-WebRequest             NoProxy                           False Microsoft.PowerShell.Utility
    New-PSSession                 UseWindowsPowerShell               True Microsoft.PowerShell.Core
    Register-PSResourceRepository PSGallery                          True Microsoft.PowerShell.PSResourceGet
    Select-String                 Raw                               False Microsoft.PowerShell.Utility
    Set-StrictMode                Off                               False Microsoft.PowerShell.Core
    Split-Path                    Qualifier                          True Microsoft.PowerShell.Management
    Split-Path                    NoQualifier                        True Microsoft.PowerShell.Management
    Split-Path                    Leaf                               True Microsoft.PowerShell.Management
    Split-Path                    LeafBase                          False Microsoft.PowerShell.Management
    Split-Path                    Extension                         False Microsoft.PowerShell.Management
    Split-Path                    IsAbsolute                         True Microsoft.PowerShell.Management
    Test-Connection               Repeat                             True Microsoft.PowerShell.Management
    Test-Connection               MtuSize                            True Microsoft.PowerShell.Management
    Test-Connection               Traceroute                         True Microsoft.PowerShell.Management
    Where-Object                  CEQ                                True Microsoft.PowerShell.Core
    Where-Object                  NE                                 True Microsoft.PowerShell.Core
    Where-Object                  CNE                                True Microsoft.PowerShell.Core
    Where-Object                  GT                                 True Microsoft.PowerShell.Core
    Where-Object                  CGT                                True Microsoft.PowerShell.Core
    Where-Object                  LT                                 True Microsoft.PowerShell.Core
    Where-Object                  CLT                                True Microsoft.PowerShell.Core
    Where-Object                  GE                                 True Microsoft.PowerShell.Core
    Where-Object                  CGE                                True Microsoft.PowerShell.Core
    Where-Object                  LE                                 True Microsoft.PowerShell.Core
    Where-Object                  CLE                                True Microsoft.PowerShell.Core
    Where-Object                  Like                               True Microsoft.PowerShell.Core
    Where-Object                  CLike                              True Microsoft.PowerShell.Core
    Where-Object                  NotLike                            True Microsoft.PowerShell.Core
    Where-Object                  CNotLike                           True Microsoft.PowerShell.Core
    Where-Object                  Match                              True Microsoft.PowerShell.Core
    Where-Object                  CMatch                             True Microsoft.PowerShell.Core
    Where-Object                  NotMatch                           True Microsoft.PowerShell.Core
    Where-Object                  CNotMatch                          True Microsoft.PowerShell.Core
    Where-Object                  Contains                           True Microsoft.PowerShell.Core
    Where-Object                  CContains                          True Microsoft.PowerShell.Core
    Where-Object                  NotContains                        True Microsoft.PowerShell.Core
    Where-Object                  CNotContains                       True Microsoft.PowerShell.Core
    Where-Object                  In                                 True Microsoft.PowerShell.Core
    Where-Object                  CIn                                True Microsoft.PowerShell.Core
    Where-Object                  NotIn                              True Microsoft.PowerShell.Core
    Where-Object                  CNotIn                             True Microsoft.PowerShell.Core
    Where-Object                  Is                                 True Microsoft.PowerShell.Core
    Where-Object                  IsNot                              True Microsoft.PowerShell.Core
    Where-Object                  Not                                True Microsoft.PowerShell.Core
    

    The results indicate there are 8 commands that exhibit the problem described in this issue (FalseSwitchIgnored is True in the results above). That is, for those commands, reasonable user expectation is subverted when certain switch parameters are passed a $false value.

    E.g., I suspect most PS users of any experience level would not expect the following to create a new Windows PowerShell session, yet that's precisely what happens.

    # The *presence* of the switch selects UseWindowsPowerShellParameterSet.
    # Irrespective of the switch *value*, CreateRunspacesForUseWindowsPowerShellParameterSet() is called.
    New-PSSession -UseWindowsPowerShell:$false

    The results above do not include:

    • Non-mandatory switch parameters. There are at least 2 other commands that exhibit the problem (see below).
    • Commands from modules shipped exclusively with Windows PowerShell (such as Get-WindowsPackage from Dism mentioned here).

    Excluding common/optional common parameters, I count a total of 707 switch parameters in commands shipped with PS v7.6.0.

    Code
    Get-Module -ListAvailable | Where-Object Path -like $PSHome* | Import-Module
    $commonParams = @(
        [Management.Automation.PSCmdlet]::CommonParameters
        [Management.Automation.PSCmdlet]::OptionalCommonParameters
    )
    
    $items = foreach ($command in Get-Command -CommandType Cmdlet, Function, Filter) {
        foreach ($parameter in $command.Parameters.Values) {
            if ($parameter.SwitchParameter -and $parameter.Name -notin $commonParams) {
                [pscustomobject] @{ 
                    Command      = $command
                    Parameter    = $parameter.Name 
                    Source       = $command.Source                
                }
            }
        }
    }
    
    $items.Count

    The effort to test the complete set would obviously be considerable, but I did pick out ~40 (not included in the results above) for manual testing. Of those ~40, I found one new instance in which an explicit $false value is ignored: New-Guid -Empty:$false, which brings the total to 10 known problematic commands (including Get-Uptime from #25015).

    Summary

    Instances in which an explicit $false value passed to a mandatory switch parameter is not respected:

    • Get-Random -Shuffle
    • Get-SecureRandom -Shuffle
    • Get-TimeZone -ListAvailable
    • New-PSSession -UseWindowsPowerShell
    • Register-PSResourceRepository -PSGallery
    • Split-Path -Qualifier/-NoQualifier/-Leaf/-IsAbsolute (not -Extension/-LeafBase)
    • Test-Connection -Repeat/-MtuSize/-Traceroute
    • Where-Object (all non-common switch parameters)

    Other known instances:

    Rough test code
    $results = @(
        # Get-Random:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name Get-Random)
            Parameter          = 'Shuffle'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -InputObject (1..10) -Shuffle); SwitchFalse = ($switchFalse = & $cmd -InputObject (1..10) -Shuffle:$false) }
            FalseSwitchIgnored = $switchOn.Count -eq $switchFalse.Count
        }
    
        # Get-SecureRandom:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name Get-SecureRandom)
            Parameter          = 'Shuffle'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -InputObject (1..10) -Shuffle); SwitchFalse = ($switchFalse = & $cmd -InputObject (1..10) -Shuffle:$false) }
            FalseSwitchIgnored = $switchOn.Count -eq $switchFalse.Count
        }
    
        # Get-TimeZone:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name Get-TimeZone)
            Parameter          = 'ListAvailable'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -ListAvailable); SwitchFalse = ($switchFalse = & $cmd -ListAvailable:$false) }
            FalseSwitchIgnored = $switchOn.Count -eq $switchFalse.Count
        }
    
        # New-PSSession:
        $cmd = Get-Command -Name New-PSSession
        $pss1 = & $cmd -UseWindowsPowerShell
        $pss2 = & $cmd -UseWindowsPowerShell:$false
    
        [pscustomobject] @{
            Command            = $cmd
            Parameter          = 'UseWindowsPowerShell'
            Behavior           = @{ SwitchOn = ($switchOn = $pss1.ApplicationPrivateData.PSVersionTable.PSEdition); SwitchFalse = ($switchFalse = $pss2.ApplicationPrivateData.PSVersionTable.PSEdition) }
            FalseSwitchIgnored = $switchOn -eq $switchFalse
        }
    
        $pss1, $pss2 | Remove-PSSession
    
        # Register-PSResourceRepository:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name Register-PSResourceRepository)
            Parameter          = 'PSGallery'
            Behavior           = @{ SwitchOn = ($switchOn = try { & $cmd -PSGallery } catch { 'Err' }); SwitchFalse = ($switchFalse = try { & $cmd -PSGallery:$false } catch { 'Err' }) }
            FalseSwitchIgnored = $switchOn -eq $switchFalse
        }
    
        # Split-Path:
        $cmd = Get-Command -Name Split-Path
        $otherParams = @{ Path = $PSHOME }
        
        'Qualifier', 'NoQualifier', 'Leaf', 'IsAbsolute' | ForEach-Object {
            $trueParam   = @{ $_ = $true }
            $falseParam  = @{ $_ = $false }
    
            [pscustomobject] @{
                Command            = $cmd
                Parameter          = $_
                Behavior           = @{ SwitchOn = ($switchOn = & $cmd @trueParam @otherParams); SwitchFalse = ($switchFalse = & $cmd @falseParam @otherParams) }
                FalseSwitchIgnored = [object]::Equals($switchOn, $switchFalse)
            }
        }
    
        # Test-Connection:
        $cmd = Get-Command -Name Test-Connection
        $otherParams = @{ TargetName = '127.0.0.1' }
        
        'MtuSize', 'Traceroute' | ForEach-Object {
            $trueParam  = @{ $_ = $true }
            $falseParam = @{ $_ = $false }
            
            [pscustomobject] @{
                Command            = $cmd
                Parameter          = $_
                Behavior           = @{ SwitchOn = ($switchOn = & $cmd @trueParam @otherParams); SwitchFalse = ($switchFalse = & $cmd @falseParam @otherParams) }
                FalseSwitchIgnored = $switchOn[0].GetType() -eq $switchFalse[0].GetType()
            }
        }
    
        # Where-Object:
        $cmd          = Get-Command -Name Where-Object
        $commonParams = [Management.Automation.PSCmdlet]::CommonParameters + [Management.Automation.PSCmdlet]::OptionalCommonParameters
        $paramNames   = $cmd.Parameters.Values.Where{ $_.SwitchParameter -and $_.Name -notin $commonParams + 'Not' }.Name
        $otherParams  = @{ InputObject = [pscustomobject] @{ Foo = [string] }; Property = 'Foo'; Value = [string] }
        
        $paramNames | ForEach-Object {
            $trueParam   = @{ $_ = $true }
            $falseParam  = @{ $_ = $false }
    
            [pscustomobject] @{
                Command            = $cmd
                Parameter          = $_
                Behavior           = @{ SwitchOn = ($switchOn = & $cmd @trueParam @otherParams); SwitchFalse = ($switchFalse = & $cmd @falseParam @otherParams) }
                FalseSwitchIgnored = $switchOn.Count -eq $switchFalse.Count
            }
        }
        
        [pscustomobject] @{
            Command            = $cmd
            Parameter          = 'Not'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -Not Bar); SwitchFalse = ($switchFalse = & $cmd -Not:$false Bar) }
            FalseSwitchIgnored = $switchOn.Count -eq $switchFalse.Count
        }    
    
        # Get-Uptime:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name Get-Uptime)
            Parameter          = 'Since'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -Since); SwitchFalse = ($switchFalse = & $cmd -Since:$false) }
            FalseSwitchIgnored = $switchOn.GetType() -eq $switchFalse.GetType()
        }
    
        # New-Guid:
        [pscustomobject] @{
            Command            = ($cmd = Get-Command -Name New-Guid)
            Parameter          = 'Empty'
            Behavior           = @{ SwitchOn = ($switchOn = & $cmd -Empty); SwitchFalse = ($switchFalse = & $cmd -Empty:$false) }
            FalseSwitchIgnored = $switchOn -eq $switchFalse
        }
    )
    
    $results | Select-Object -Property *, @{ N = 'Source'; E = { $_.Command.Source } } -ExcludeProperty Behavior
    
    
    # Manual test:
    Test-Connection -Ping 127.0.0.1 -Repeat
    Test-Connection -Ping 127.0.0.1 -Repeat:$false
  10. surfingoldelephant commented on Feb 16, 2025

    @surfingoldelephant

    I suspect the total number of affected commands in the wild is very large.

    From the perspective of a command author, defining multiple, mutually exclusive switches, switching on ParameterSetName within the command body and modifying behaviour accordingly is extremely convenient, yet breaks down with splatting and -Param:$bool syntax. Reworking the switches into a single, enum/set-based parameter is the natural alternative, but is not always viable and requires knowledge of the problem discussed in this issue.

    For that reason, I fully support a systematic change that reconciles the selected parameter set with explicitly $false switches.

    In the meantime, the following issues asks to document the implications of this practice.


    Also, with this proposal treating -NoQualifier:$false equivalent to not specifying -NoQualifier, parameter binding involving mutually exclusive switches will presumably be possible.

    For example, the following currently results in an AmbiguousParameterSet parameter binding exception.

    $params = @{
        Qualifier   = $true
        NoQualifier = $false
    }
    
    Split-Path $PSHOME @params
    # Parameter set cannot be resolved using the specified named parameters. 
    # One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
  11. anonhostpi commented on Feb 17, 2025

    @anonhostpi

    I know that this issue brings up bugs with the omitting behavior, but that just raises another question:

    Would this introduce a potential for bugs in cmdlets that use -switch:$false as a negating behavior? Are we just trading bugs?

    We would also need a list of functions/cmdlets exist that expect -switch:$false to be negating instead of omitting.

  12. surfingoldelephant commented on Feb 17, 2025

    @surfingoldelephant

    Would this introduce a potential for bugs in cmdlets that use -switch:$false as a negating behavior? Are we just trading bugs?

    That's a fair question.

    As noted in the comment below, checking for -Switch:$false explicitly will still be possible, with a combination of $PSBoundParameters/InvocationInfo.BoundParameters and the value of $Switch/$Switch.IsPresent.

    This issue proposes that -Switch:$false doesn't influence the selected parameter set, which means PSCmdlet.ParameterSetName won't reflect the parameter set that would otherwise have been selected.

    In PowerShell code:

    function Test1 { 
        param ([Parameter(ParameterSetName = 'SwitchParam')] [switch] $SwitchParam)  
    
        # Note: !$SwitchParam and !$SwitchParam.IsPresent are equivalent. 
        if ($PSCmdlet.ParameterSetName -eq 'SwitchParam' -and !$SwitchParam) { 
            'Parameter Set: Switch explicitly False'
        }
    
        if ($PSBoundParameters.ContainsKey('SwitchParam') -and !$SwitchParam) {
            'Bound Parameters: Switch explicitly False'
        }
    }
    
    Test1 -SwitchParam:$false
    
    # Current behavior: 
    # Parameter Set: Switch explicitly False
    # Bound Parameters: Switch explicitly False
    
    # With proposal:
    # Bound Parameters: Switch explicitly False

    I skimmed through a naive GitHub search and didn't see any code that would be broken (no doubt this search could be improved).

    • Some results such as this resemble the function above, but the use case isn't the same and the proposal wouldn't affect anything.
    • Other results such as this would actually benefit from the proposal.
  13. mklement0 commented on Feb 17, 2025

    @mklement0
    ContributorAuthor

    Thanks for digging deeper.

    InvocationInfo.BoundParameters won't contain the switch.

    It should, however, just as it does now - precisely so as to continue to support the (ill-advised) "negated switch" logic.

    With that in mind, the only existing code that could be broken is code that checks for an explicitly specified negated switch inside an if ($PSCmdlet.ParameterSetName -eq 'SwitchParam')-like conditional, i.e. code that doesn't just check for $PSBoundParameters.ContainsKey('SwitchParam') -and !$SwitchParam irrespective of the selected parameter set.

    To spell it out:

    Add-Type -ReferencedAssemblies System.Management.Automation, System.Collections  @'
     using System.Management.Automation;
     [Cmdlet("Get", "Foo")] 
     [CmdletBinding(DefaultParameterSetName="Default")]
     public class GetFooCmdlet : PSCmdlet {
      [Parameter(ParameterSetName="Default")] public object Unused { get; set; }
      [Parameter(ParameterSetName="Bar", Mandatory=true)] public SwitchParameter Bar { get; set; }
       protected override void ProcessRecord() {
        WriteObject(this.ParameterSetName);  
        WriteObject(this.MyInvocation.BoundParameters);
       }
     }
    '@ -PassThru | % Assembly | Import-Module
    
    Get-Foo -Bar:$false 

    currently yields:

    Bar # parameter set 'Bar' *was* selected
    
    Key Value
    --- -----
    Bar False

    with the proposed change implemented will yield:

    Default  # parameter set 'Bar' *was not* selected
    
    Key Value
    --- -----
    Bar False
  14. surfingoldelephant commented on Feb 17, 2025

    @surfingoldelephant

    It should, however, just as it does now

    That certainly limits the extent of the breaking change drastically (which is in line with my findings from the earlier Test1 search).

    Just to be clear, in the case of -Switch:$false, while the parameter will be bound, it won't influence the parameter set chosen thereafter. Or in other words, the parameter will be treated as if it doesn't have a defined parameter set and thus belongs to all parameter sets.

    I've adjusted my previous comment to avoid potential confusion.

  15. sirtao commented on Mar 6, 2025

    @sirtao

    For whatever my opinion might be worth: I do agree with parsing -switch:$false as the same as -switch not being passed in context of parameter set selecting.

    I cannot find a reason for, as general rule, a switch used to select a Parameter Set to be passed as $false and still select the parameter set.

    I also bring to point the .IsPresent property of [switch] objects: as Michael Klement (@mklement0) noted elsewhere, it returns $false when the switch is set to $false, exactly like when there is no switch.

    This compound confusion: using -switch:$false enables the relative Parameter Set, but if I check its presence with .IsPresent I get a $false.
    Completely contradictory.

  16. SeeminglyScience commented on Mar 10, 2025

    @SeeminglyScience
    Contributor

    The Engine WG discussed this. While we agree the behavior can be confusing (and we agree in general that the idea of the negated switch has a lot of issues), we think special casing parameter set selection now would be potentially just as confusing (e.g. a value is clearly being sent, can be seen in bound parameters, yet parameter set selection is unaffected).

    We also can't change it across the board due to the many commands that differentiate between the three states (not specified, present, and explicit false) including many built in commands.

  17. added
    Resolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.
    and removed
    Needs-TriageThe issue is new and needs to be triaged by a work group.
    on Mar 10, 2025
  18. mklement0 commented on Mar 10, 2025

    @mklement0
    ContributorAuthor

    To be clear:

    we think special casing parameter set selection now would be potentially just as confusing

    If framed as follows, there would be no confusion going forward:

    • -switch:$false is the same as omitting switch, and therefore has no impact on parameter-set selection; the primary purpose of this syntax is to enable programmatic construction of parameter values (often indirectly, via hashtable-based splatting)

    • In edge cases, -switch:$false has different semantics, but this is best avoided in user code; a notable case is the common -Confirm parameter, where -Confirm:$false serves to override a preference variable (something that omitting -Confirm couldn't achieve).

      • Do not use this technique in your own code to implement tri-state logic, as it is both obscure and potentially confusing; the Strongly Encouraged Developer Guidelines suggest using a [Nullable[bool]] parameter instead ($true to set, $false to unset, $null to leave unchanged).

    the many commands that differentiate between the three states

    • The proposal allows them to continue to do so (although, as suggested, no new code should use this approach).

      • The only existing code that would break are cases where such conceptually tri-state [switch] parameters are mandatory members of a parameter set and the state of the switch is evaluated inside a parameter set-specific conditional. (By contrast, for non-mandatory [switch] parameters , existing code must already explicitly check $PSBoundParameters in order to distinguish the -switch:$false case from the omission case, and that will continue to work.)

      • Given that the tri-state logic is presumably rare to begin with, the additional constraints likely make the affected cases even rarer, and that should be weighed against what currently broken calls the proposal would implicitly fix (see next point).

    • Conversely, as surfingoldelephant's analysis shows, implementing the proposal would likely implicitly fix many commands, as they were clearly - and justifiably - designed under the assumption that -switch:$false is indeed the same as omitting switch.

  19. anonhostpi commented on Mar 10, 2025

    @anonhostpi

    The point that both myself and Patrick Meinecke (@SeeminglyScience) science are trying to make is that this may end up fixing many cmdlets at the cost of breaking many others.

    So far only an analysis of what would be fixed has been proposed. So far no analysis has been provided accounting for what would be broken.

  20. mklement0 commented on Mar 11, 2025

    @mklement0
    ContributorAuthor

    Blue Falcon (@anonhostpi), the following reasons have been laid out:

    • (a) why it is unlikely that many commands will be broken.
    • (b) why it is certain that many commands will be fixed.
    • (c) why the proposed change makes sense going forward.

    This sounds like a classic bucket 3 candidate to me, yet it is fair to ask for more analysis re (a).

    However, doing so is moot:

    • It is highly unlikely that the decision that was made will be re-negotiated.
    • Given the decision, it is highly unlikely that anyone will undertake said analysis.

    The reason for my posting another analysis after the decision was made is twofold:

    • (a) Getting clarity on an issue is always worthwhile, even if it has no effect on the issue at hand - it may inform future decisions.

    • (b) Secondarily, one can always hope that a decision will be revisited.

  21. anonhostpi commented on Mar 11, 2025

    @anonhostpi

    By analysis, I meant quantitative. No quantitative analysis has been provided proving that no cmdlets would be broken

    "The absence of evidence is not the evidence of absence" - Gin Rummy, The Boondocks

  22. mklement0 commented on Mar 11, 2025

    @mklement0
    ContributorAuthor

    Blue Falcon (@anonhostpi), it's unclear to me how your comment relates to my previous one (in the sense of not adding anything that hasn't already been said and acknowledged), but there's no need to delve into that: I'm opting out of this conversation.

  23. microsoft-github-policy-service commented on Mar 13, 2025

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as won't fix and has not had any activity for 1 day. It has been closed for housekeeping purposes.

  24. microsoft-github-policy-service commented on Mar 13, 2025

    @microsoft-github-policy-service
    Contributor

    📣 Hey Michael Klement (@mklement0), how did we do? We would love to hear your feedback with the link below! 🗣️

    🔗 https://aka.ms/PSRepoFeedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Enhancementthe issue is more of a feature request than a bugResolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.WG-Enginecore PowerShell engine, interpreter, and runtimeWG-Engine-ParameterBinder

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions