2

Basically I'm trying to get the below "inline if-statement" function working (credit here)

Function IIf($If, $Then, $Else) {
    If ($If -IsNot "Boolean") {$_ = $If}
    If ($If) {If ($Then -is "ScriptBlock") {&$Then} Else {$Then}}
    Else {If ($Else -is "ScriptBlock") {&$Else} Else {$Else}}
}

Using PowerShell v5 it doesn't seem to work for me and calling it like

IIf "some string" {$_.Substring(0, 4)} "no string found :("

gives the following error:

You cannot call a method on a null-valued expression.
At line:1 char:20
+ IIf "some string" {$_.Substring(0, 4)} "no string found :("
+                    ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

So, as a more general question, how do you make $_ available to the scriptblock passed into a function?

I kind of tried following this answer, but it seems it's meant for passing it to a separate process, which is not what I'm looking for.

Update: It seems the issue is that I have the function in a module rather than directly in a script/PS session. A workaround would be to avoid putting it in the module, but I feel a module is more portable, so I'd like to figure out a solution for that.

Xerillio
  • 4,855
  • 1
  • 17
  • 28
  • 2
    If have tested, PowerShell 5, Core and ISE and they all give the expected results: `Some`. I can't explain why you get this error. Have you tried a new PowerShell session? – iRon Feb 14 '19 at 19:15
  • I too get *some* in powershell.exe as host and 5.1 as version – vrdse Feb 14 '19 at 19:40
  • What happens when use the `$If` variable rather then the automatic variable `$_` in your `Else` scriptblock? Thus: `{$If.Substring(0, 4)}` – iRon Feb 15 '19 at 07:41
  • @iRon thanks for testing it out. If I add the function directly to the session it works now. I had it in a module before which seems to cause the difference. I'll update the question with that info... I wasn't aware that made a difference. So, I'm still looking for a solution. – Xerillio Feb 15 '19 at 08:21
  • @mklement0 thanks, it's never too late to learn more! That addition perfectly explains why my original attempt in a module didn't work. I unfortunately can't upvote more than once :) – Xerillio Jun 13 '22 at 19:53
  • @Xerillio :) Glad to hear it. – mklement0 Jun 13 '22 at 19:54

1 Answers1

4

There are two changes worth making, which make your problem go away:

  • Do not try to assign to $_ directly; it is an automatic variable under PowerShell's control, not meant to be set by user code (even though it may work situationally, it shouldn't be relied upon).

    • Instead, use the ForEach-Object cmdlet to implicitly set $_ via its -InputObject parameter.
    • Note that use of ForEach-Object with -InputObject rather than with input from the pipeline is unusual, because it results in atypical behavior: even collections passed to -InputObject are passed as a single object to the -Process block; that is, the usual enumeration does not take place; however, in the context at hand, this is precisely what is desired here: whatever $If represents should be passed as-is to the -Process script block, even if it happens to be a collection.
  • Use the -is operator with type literals such as [Boolean], not type names such as "Boolean".

Function IIf($If, $Then, $Else) {
  If ($If) { 
    If ($Then -is [scriptblock]) { ForEach-Object -InputObject $If -Process $Then } 
    Else { $Then } 
  } Else {
    If ($Else -is [scriptblock]) { ForEach-Object -InputObject $If -Process $Else }
    Else { $Else }
  }
}

As for what you tried:

In a later update you state that your IIf function is defined in a module, which explains why your attempt to set $_ by direct assignment ($_ = $If, which, as stated, is to be avoided in general), was ineffective:

It created a function-local $_ instance, which the $Then script block, due to being bound to the scope of the (module-external) caller, does not see.

The reason is that each module has its own scope domain (hierarchy of scopes aka session state), which only shares the global scope with non-module callers - see the bottom section of this answer for more information about scopes in PowerShell.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks for the comment, in fact the automatic variable is a little overdone, because you could reuse the input variable (`$If`) as well (just one character more). – iRon Feb 15 '19 at 07:48
  • Thanks for the suggestions, however `ForEach-Object` will apply `$Then` to every object in an array, which is not the intention. Also interesting you mention using type literals, which I think looks much neater in code, but I stumbled upon [this blog post](https://www.adamtheautomator.com/wary-powershells-outputtype-keyword/) that suggests using strings as a general rule of thumb. Of course that's only really necessary for non-standard .NET types – Xerillio Feb 15 '19 at 08:27
  • @iRon: Relying on `$If` means relying on an _implementation detail_, namely the name of the parameter variable. While you could make that part of the "contract" of your function, it is far from obvious and runs counter to the well-established semantics of using `$_` to refer to the input object at hand. – mklement0 Feb 15 '19 at 11:37
  • @Xerillio: No, due to use of `-InputObject` rather than the pipeline, an array is bound _as a whole_ to `$_` - try `iif (1,2) { "[$_]" } 'unused'`. – mklement0 Feb 15 '19 at 11:42
  • 1
    @Xerillio: As for using type literals: The blog post you link to discusses a _parse-time_ problem related to the `[OutputType]` attribute used in the definition of advanced functions, with respect to types that aren't loaded into the session until _runtime_. This is _not_ a problem here, and use of type literals is generally preferable. – mklement0 Feb 15 '19 at 11:47
  • 1
    @mklement0 ah, you're absolutely right. Thanks, that seems to work even when `IIf` is part of a module. And yeah regarding type literals, I guess I was thinking more about making it a habit to avoid making that mistake without thinking. But type literals is a better option in the general sense like you say. – Xerillio Feb 15 '19 at 11:54
  • Good point re definition in a module, Xerillio; @iRon, that's another reason not to use `$If`: if the `IIf` function is defined in a module and you call it from outside that module, script blocks you pass to it won't see the now module-scoped `$If` variable. – mklement0 Feb 15 '19 at 12:09
  • Another way to pass a variable as `$_` to a scriptblock is the `.ForEach` _intrinsic_ method: `$var.ForEach($scriptBlock)`. This runs faster as it avoids the parameter-binding overhead of `ForEach-Object`. Do you see any disadvantage of `.ForEach` over `ForEach-Object` for this use case? – zett42 Jun 13 '22 at 11:07
  • 1
    Good point in general, @zett42, but here use of `ForEach-Object -Input` is actually necessary, so as to ensure that if `$If` happens to be an enumerable, it is still passed _as a single object_. I've updated the answer to clarify. Leaving that aside, if you were to compare the performance of the two approaches with a _scalar_, the `.ForEach()` call would actually be slower on the _first_ invocation in a session, where you incur overhead for behind-the-scenes compilation of access to a .NET method. – mklement0 Jun 13 '22 at 14:05