PoshCode / PowerShellPracticeAndStyle

The Unofficial PowerShell Best Practices and Style Guide
https://poshcode.gitbooks.io/powershell-practice-and-style
Other
2.23k stars 288 forks source link

Is there guidance for using the 3 states of a switch parameter? #69

Open imfrancisd opened 8 years ago

imfrancisd commented 8 years ago

I'm tempted to create a function that does different things when the switch parameter is missing, true, and false.

Using the 3 states cuts down on the number of switch parameters, and sometimes makes the function easier to understand, but I don't know how widely this practice is accepted by the community.

Example

For an example of when a 2-state switch parameter is misleading, look at Get-ChildItem -File -Directory.

    #list all - ok
    Get-ChildItem

    #list directories - ok
    Get-ChildItem -Directory

    #list files - ok
    Get-ChildItem -File

    #list directories - ok
    Get-ChildItem -File:$false -Directory

    #list files - ok
    Get-ChildItem -File -Directory:$false

So far so good, but look at this.

    #list all - not intuitive
    Get-ChildItem -Directory:$false

    #list all - not intuitive
    Get-ChildItem -File:$false

    #list nothing! - very surprising
    Get-ChildItem -File -Directory

Using 3 states for this is much simpler for the script writer. It may be easier for the user too.

    #list all
    Get-ChildItem

    #list directories
    Get-ChildItem -Attributes D

    #list files
    Get-ChildItem -Attributes !D

    #there is no attribute for files

Question

So, would it be okay to use 3 states for switch parameters like the -Directory switch for Get-ChildItem?

mattmcnabb commented 8 years ago

Not really sure what you mean by "3 states" - switch parameters by their nature have only 2 states. They are either on or off. Passing an explicit boolean is only used in special cases, like for -Confirm switches to override the default ConfirmPreference:

Set-Something -Confirm:$false

Of course, you can create your own parameters with unlimited numbers of arguments. Depending on your use case this may or not be more useful or intuitive than a switch parameter. I definitely would not choose to use a string parameter where there are only two choices - this seems un-intuitive and runs counter to the behavior of most commands.

EDIT: It's also worth noting that the syntax above using ":" to bind arguments to parameters is not unique to switch parameters. You can use this to pass a value to any parameter.

imfrancisd commented 8 years ago

Usually, switch parameters are viewed as having 2 states (true or false), but you can also look at them as having 3 states (missing, present/true, explicitly false).

For example, Get-ChildItem treats the -Directory switch as having only 2 states (true or false), which is how most switch parameters are treated in many functions.

    #list directories
    Get-ChildItem -Directory

    #list all
    Get-ChildItem
    Get-ChildItem -Directory:$false

But Get-ChildItem could have also treated the -Directory switch as having 3 states (missing, present/true, explicitly false).

It could have done something like this:

    #list all
    Get-ChildItem

    #list directories
    Get-ChildItem -Directory

    #could have made this list files
    Get-ChildItem -Directory:$false

If Get-ChildItem treated the -Directory switch parameter as having 3 states (missing, present/true, explicitly false), then the -File switch would not be needed, and weird cases like this would not be possible:

    #list nothing
    Get-ChildItem -Directory -File

But I don't know if designing a function to treat a switch parameter as having three states is an acceptable practice in PowerShell.

mattmcnabb commented 8 years ago

So, given your example with the -Directory switch, what would be different about the explicitly false scenario. This is essentially the same as missing/false, so I don't think I fully understand how you would treat this differently in a function. Also, why is there not a 4th state for explicitly true?

As far as the Get-ChildItem example with both the -File and -Directory parameters, I think this is probably a bug and I wouldn't be surprised if someone on this project is aware of it. I'm not really sure how these are implemented, but from the looks of it, they are dynamic parameters since they aren't returned by the Get-Help.

imfrancisd commented 8 years ago

It would only return files.

    #this
    Get-ChildItem -Directory:$false

    #could have been similar to this
    Get-ChildItem -Attributes !D

    #and this
    cmd /c dir /a-d
imfrancisd commented 8 years ago

I'm not sure if you can tell from inside the function if a switch parameter is present or explicitly true to get a fourth state.

At least I don't think you can without looking at the command that called the function.

mattmcnabb commented 8 years ago

But this is much more explicit, and you immediately know what it does:

Get-ChildItem -File

And that's the whole point of PowerShell. Once you start down the path described above, you're losing a lot of what we gain with PowerShell - readability and consistency of behavior. One of the biggest problems with existing shells is that each application is a world unto itself with its' own behavior. This increases the learning curve dramatically. Clearly with any managed technology there is always a bit of "domain-specific" problems and jargon, but it's not a good idea to design our user experience around them.

And yes, you can check if a switch parameter was explicitly bound:

function Test-SwitchParam
{
    [CmdletBinding()]
    param ([switch]$Switch)

    $PSBoundParameters.ContainsKey("Switch")
}
imfrancisd commented 8 years ago

Yes, Get-ChildItem -File is easier than Get-ChildItem -Directory:$false.

You can test if the parameter is bound, but you cannot tell the difference between -Switch and -Switch:$true.

function Test-SwitchParam
{
    [CmdletBinding()]
    param ([switch]$Switch)

    $PSBoundParameters.ContainsKey("Switch")
    $Switch.ToBool()
}
Test-SwitchParam -Switch
Test-SwitchParam -Switch:$true
dlwyatt commented 8 years ago

Switches should be treated as booleans, period. Trying to make a third "state" is a mistake that will just confuse users at some point. (Splatting comes to mind as a common way to screw with that logic.)

imfrancisd commented 8 years ago

Then I think that should be placed in the Best Practices guide (if it is I couldn't find it).

The reason I ask is that if you have a function like Get-ChildItem that already has a default behavior (show files and directories) and you want to add the ability to show only files, or show only directories, using a switch with 3 states becomes very tempting.

To put this another way, how would you have added the ability to show only files or to show only directories to Get-ChildItem. Would you have used switch parameters like they do now, or use something else?

indented-automation commented 8 years ago

Perhaps ItemType or PathType as New-Item and Test-Path do, that is a parameter backed by ValidateSet or an Enumeration.

On 23 August 2016 at 17:29, Francis de la Cerna notifications@github.com wrote:

Then I think that should be placed in the Best Practices guide (if it is I couldn't find it).

The reason I ask is that if you have a function like Get-ChildItem that already has a default behavior (show files and directories) and you want to add the ability to show only files, or show only directories, using a switch with 3 states becomes very tempting.

To put this another way, how would you have added the ability to show only files or to show only directories to Get-ChildItem. Would you have used switch parameters like they do now, or use something else?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/69#issuecomment-241792039, or mute the thread https://github.com/notifications/unsubscribe-auth/AMO6Dl3bD0zJf52oXOi7Q9G2-opSbmYaks5qix_XgaJpZM4JqqEF .

dlwyatt commented 8 years ago

I would have probably gone with an enumerated type with a default value. (Or a ValidateSet, which amounts to the same thing). Something like this:

function Get-ChildItem
{
    param (
        [ValidateSet("All", "File", "Directory")]
        [string] $ItemType = "All"
    )
}

I suspect that their decision to have -File and -Directory switches was driven by wanting to make dir / ls feel more familiar to users of different shells.

imfrancisd commented 8 years ago

Ok, thanks. :)

Jaykul commented 8 years ago

Yeah, if it's not just a boolean True/False, you'd want an enum. If we don't have a best practices about switches, I suppose it wouldn't hurt to add. We ought to have best practices on a few parameter types: switches, credentials, paths ...

Jaykul commented 4 years ago

As an additional point, we've had some problems with functions that behaved differently than expected when people tried to pass through a switch statement:

function Test-One {
    [CmdletBinding()]
    param(
        [switch]$Force
    )

    Test-Two -Force:$Force
}

In this situation, simply not passing -Force to Test-One will result in calling Test-Two -force:$false

Treating it as trinary makes it substantially harder to reuse

tapika commented 3 years ago

There exists one approach to mimic 3 state behavior - I needed this kind of system, because despite of default parameters , I need to re-define default parameters based on environment variable.

$PSBoundParameters.ContainsKey("Switch") can be used to query whether user provided specific flag from command line. If not provided - then can figure out it's default values. (If you're using alias - then ContainsKey need to have actual parameter name, not it's alias)

(Referred information: https://stackoverflow.com/a/56809664/2338477)

But after that use switch normally, compare to $True / $False as necessary.

There is only one catch to this - if you have batch wrapper over .ps1 script - then that batch file must not use -File switch - provide whole .ps1 file path instead.

For example:

test.bat:

@echo off
set args=%*
set args=%args:"='"%
powershell -executionpolicy bypass "%~dpn0.ps1" %args%

(Referred information: https://stackoverflow.com/a/40799651/2338477)

Extra char replacement " => '" is done so powershell script can accept string with quotes (e.g. -Param "")

Jaykul commented 3 years ago

To be clear. The guidance here is simple:

Do not treat SwitchParameter as a trinary

As implied by the design, and documented in the official about_ docs