5

I have the powershell function below

Function Test
{
    Param
    (               
        [Parameter()]
        [string]$Text = "default text"
    )

    Write-Host "Text : $($Text)"
}

And I would like to be able to call this function like below :

Test -Text : should display the default text on the host

Test -Text "another text" : should display the provided text on the host

My issue is that the first syntax is not allowed in powershell ..

Any ideas of how I can achieve this goal ? I would like a kind of 'switch' parameter that can take values other than boolean.

Thanks

Riana
  • 689
  • 6
  • 22
  • Simply add another (optional) parameter `$text2` and add `Position = 0` to the first parameter. – Theo Nov 13 '19 at 14:47
  • 1
    @Theo That isn't answering the question. They want to be able to use `$Text` as a `string` **_and_** a `switch`. – Maximilian Burszley Nov 13 '19 at 14:48
  • What you're asking is not how PowerShell parameters work. Maybe you could do what you want via dynamic parameters, but I fail to see the problem with just calling the function without parameters for having it display the default text. – Ansgar Wiechers Nov 13 '19 at 14:51
  • @TheIncorrigible1 Are you sure? I'm confused now.. With a second parameter you can test if that has any value and if so ignore the first default parameter and display the second one. I thought that was the question. – Theo Nov 13 '19 at 14:52
  • @Theo From what I understand, they want a default value when the `switch` `-Text` is passed, and a different behavior when they pass a `string` to `-Text` but powershell's rules around parametersets won't allow this use-case. – Maximilian Burszley Nov 13 '19 at 14:56
  • @Theo I can use two parameters as you said and it will work but I'm looking for a solution where I can use only one parameter. Using only one parameter makes the final syntax more elegant and and concise IMO. – Riana Nov 13 '19 at 21:47
  • @Ans: The desired behavior - which _is_ a useful idiom, even though currently unsupported by PowerShell - is to have an optional-argument parameter, i.e., a parameter that you may pass by itself - to fundamentally opt into a behavior - but which you _may, optionally_ also _qualify_ - with the optional argument. An example from the Unix world is GNU `sed`'s `-i` (create a backup) option: if you pass just `-i`, you opt into creating a backup with the default extension of `.bak`; alternatively, you can pass the specific backup-file extension desired: `-i.sav`. – mklement0 Nov 14 '19 at 13:49
  • (Cont'd) Syntax-wise, PowerShell _could_ support this idiom, by using `:` as the separator between parameter name and (optional) value, to unequivocally identify the next token as belonging to the parameter. In fact, that's how it already works for `[switch]` parameters, but currently _only_ for them. – mklement0 Nov 14 '19 at 13:51
  • 1
    @mklement0 Thanks for explaining. I do use the colon syntax for switches, but never thought of using similar for other types of parameters. – Theo Nov 14 '19 at 13:53
  • @mklement0 Nothing either of us said contradicts what the other said, no? – Ansgar Wiechers Nov 14 '19 at 14:22
  • @AnsgarWiechers. The primary purpose of my comment was to frame the issue properly for future readers, while secondarily also notifying previous commenters (I first notified Theo, then realized you might find it interesting too). As for contradictions: _I fail to see the problem with just calling the function without parameters for having it display the default text_ contradicts the idea of an optional-argument parameter. – mklement0 Nov 14 '19 at 14:26
  • @mklement0 Not really, as the intention of my comment was simply dealing with the status quo. I have no problem should Microsoft decide to change parameter handling in the way you suggested. – Ansgar Wiechers Nov 14 '19 at 14:36
  • @AnsgarWiechers: Yes; I would have no problem either and would even welcome the change, though the question is whether it is worth the effort. As for the status quo: Your recommendation of just calling the function without parameters does not address the OP's requirements - it addresses a different scenario. – mklement0 Nov 14 '19 at 14:40
  • @mklement0 I was questioning that requirement, though. ;) – Ansgar Wiechers Nov 14 '19 at 14:40

3 Answers3

5

The problem you're running into is with parameter binding. PowerShell is seeing [string] $Text and expecting a value. You can work around this like so:

function Test {
    param(
        [switch]
        $Text,

        [Parameter(
            DontShow = $true,
            ValueFromRemainingArguments = $true
        )]
        [string]
        $value
    )

    if ($Text.IsPresent -and [string]::IsNullOrWhiteSpace($value)) {
        Write-Host 'Text : <default text here>'
    }
    elseif ($Text.IsPresent) {
        Write-Host "Text : $value"
    }
}

Note: this is a hacky solution and you should just have a default when parameters aren't passed.

Maximilian Burszley
  • 18,243
  • 4
  • 34
  • 63
  • 1
    Great solution ! Thank you :) ! One last question though. What if I have a second parameter $Text1 that should work exactly as the $Text parameter ? I would expect to copy past the code of $Text and $Value and create $Text1 and $Value1 but there is a gotcha because if I call Test -Text -Text1 "Hehe" , The text "Hehe" will be assigned to $value instead of $value1 .. – Riana Nov 13 '19 at 22:09
  • Nicely done - that's indeed the best you can do in PowerShell. @Riana, this workaround only works for a _single_ optional-argument parameter. TheIncorrigible1, using a regular parameter with a default value cannot implement the desired behavior, because the fundamental opt-in aspect (was the parameter passed at all?) is lost. – mklement0 Nov 14 '19 at 13:45
4

tl;dr

  • PowerShell does not support parameters with optional values.

  • A workaround is possible, but only for a single parameter.


Maximilian Burszley's helpful answer provides a workaround for a single parameter, via a catch-all parameter that collects all positionally passed arguments via the ValueFromRemainingArguments parameter property.

Fundamentally, though, what you're asking for is unsupported in PowerShell:

PowerShell has no support for parameters with optional values as of 7.2 - except for [switch] parameters, which are limited to [bool] values.

That is:

  • Any parameter you declare with a type other than [switch] invariably requires a value (argument).

  • The only other option is to indiscriminately collect any unbound positional arguments in a ValueFromRemainingArguments-tagged parameter, but you won't be able to associate these with any particular other bound parameter.

In other words:

  • If you happen to need just one optional-argument parameter, the ValueFromRemainingArguments can work for you (except that you should manually handle the case of mistakenly receiving multiple values), as shown in Maximilian Burszley's answer.

  • If you have two or more such parameters, the approach becomes impractical: you'd have to know in which order the parameters were passed (which PowerShell doesn't tell you) in order to associate the remaining positional arguments with the right parameters.


With [switch] parameters (using an imagined -Quiet switch as an example):

  • The default value - if you just pass -Quiet -is $true.
  • $false is typically indicated by simply not specifying the switch at all (that is, omitting -Quiet)

However, you may specify a value explicitly by following the switch name with :, followed by the Boolean value:

  • -Quiet:$true is the same as just -Quiet

  • -Quiet:$false is typically the same as omitting -Quiet; in rare cases, though, commands distinguish between an omitted switch and one with an explicit $false value; notably, the common -Confirm parameter allows use of -Confirm:$false - as opposed to omission of -Confirm - to override the value of the $ConfirmPreference preference variable.

While : as the separator between the parameter name and its argument (as opposed to the usual space char.) is supported with all parameters, with [switch] parameters it is a must so as to unequivocally signal that what follows is an argument for the switch parameter (which by default needs no argument) rather than an independent, positional argument.


The above tells us that PowerShell already has the syntax for general support of optional-argument parameters, so at some point in the future it could support them with any data type, as suggested in GitHub issue #12104.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Actually, powershell does give you a way to view the order of the arguments passed to the function with $MyInvocation.Line. If you really want to do this with a function supporting multiple parameters, then you can simply parse that string to match parameters to their values. – JKony Sep 29 '21 at 00:38
  • @JKony, yes, but at that point you may better off not using a `param()` block at all and inspecting the automatic `$args` variable instead - either way, it is a nontrivial effort. As an aside: Avoid `$MyInvocation.Line`, because (a) it can include _other_ statements placed on the same line and (b) conversely, it only reports the _first_ line if the call happens to have been spread across multiple lines (which is arguably a bug). The robust alternative is `(Get-PSCallStack)[1].Position.Text`. Either way, an additional challenge is that the _raw_ command is reported, with _unexpanded_ arguments. – mklement0 Sep 29 '21 at 13:08
0

I like @Maximilian Burszley's answer (and his name!) for String, I tweaked it for Ints:

function Optional-SwitchValue {
  [CmdletBinding()]
  param (
    [Switch]
    $Bump,

    [Int]
    $BumpAmount
  )

  Begin {
    # nifty pattern lifted from https://stackoverflow.com/questions/58838941/powershell-special-switch-parameter
    # default Bump to 1
    if ($Bump.IsPresent -and -not $BumpAmount) {
      $BumpAmount = 1
    }
  }
  
  Process {
    if($Bump) {
        #use $BumpAmount in some way
    }
  }
}
Max Cascone
  • 648
  • 9
  • 25