2

I have the following method declared in a module I've called Common.psm1:

function Test-Any ([ScriptBlock]$FilterScript = $null)
{    
    begin {
        $done = $false
    }
    process { 
        if (!$done)
        {
            if (!$FilterScript -or ($FilterScript | Invoke-Expression)){
                $done = $true
            }
        }
    }
    end {
        $done
    }
}
Set-Alias any Test-Any -Scope Global

Now in another module, I have the following validation:

    $id = 1
    if($notifications | any { $_.Id -eq $id })
    {
        # do stuff here
        return
    }

I receive the following error:

Invoke-Expression : The variable '$id' cannot be retrieved because it has not been set.

The interesting thing is that if I move the Test-Any definition to the calling module, it works like a charm.

How can I make this work without copying Test-Any to my other modules and without changing this syntax:

if($notifications | any { $_.Id -eq $id })

EDIT 1: There seems to be some debate about whether or not my code should work. Feel free to try this on your own machine:

function Test-Any ([ScriptBlock]$FilterScript = $null)
{    
    begin {
        $done = $false
    }
    process { 
        if (!$done)
        {
            if (!$FilterScript -or ($FilterScript | Invoke-Expression)){
                $done = $true
            }
        }
    }
    end {
        $done
    }
}
Set-Alias any Test-Any -Scope Global

$id = 3

$myArray = @(
    @{Id = 1},
    @{Id = 2},
    @{Id = 3},
    @{Id = 4},
    @{Id = 5},
    @{Id = 6},
    @{Id = 7},
    @{Id = 8}
)
$myEmptyArray = @()

$myArray | any #returns true
$myArray | any {$_.Id -eq $id} #returns true
$myEmptyArray | any #returns false
$myEmptyArray | any {$_.Id -eq $id} #returns false

EDIT 2: I just discovered that you only encounter this issue, when Test-Any resides in one loaded module and the calling code resides in a second module using Set-StrictMode -Version Latest. If you turn off StrictMode, you don't get the error, but it also doesn't work.

EDIT 3: Needless to say this works perfectly fine:

    $sb = [Scriptblock]::Create("{ `$_.Id -eq $id }")
    if($notifications | any $sb)

But seriously takes away from the simplicity and intuitiveness I am trying to obtain

saml
  • 463
  • 3
  • 14
  • Have you looked into `Export-ModuleMember`? Given the scenario, you can specify the scope level of a variable as well, if I'm not misunderstanding something. – Abraham Zinala Jan 10 '22 at 23:21
  • @SantiagoSquarzon This is a PowerShell Advanced Function. You can read more about them [here](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced?view=powershell-7.2). Each iteration is processed in the `process` block – saml Jan 10 '22 at 23:59
  • @AbrahamZinala I am using `Export-ModuleMember` to export the method `Test-Any` but I don't want to use it to export `$id` since that will quickly become messy if I have to export a variable each time I use `any` – saml Jan 11 '22 at 00:01
  • 1
    @SantiagoSquarzon the point is when you use Advanced Functions, you don't need to declare `ValueFromPipeLine` because it already processes each iteration in the `process` block. Give it a try, copy my code in the same module and debug it. You will see that each item is properly piped in. While the code in `begin` and `end` only execute once, the code in `process` executes for each item piped in. – saml Jan 11 '22 at 00:24
  • 1
    @saml you're right, my bad. I see the point of what you're trying to do now. I have tried saving the function in a module and after restarting session calling `import-module` on the script with the `if` condition and it works fine. I'm not seeing that error you get. – Santiago Squarzon Jan 11 '22 at 00:52
  • @SantiagoSquarzon no worries, I just learnt about Advanced Functions today myself. So I was able to confirm what you mentioned. If `Test-Any` is in a module, and you import the module, then it works. But if the calling code resides in its own module as well, and then you call the function that calls `Test-Any` that is when it fails. – saml Jan 11 '22 at 01:15
  • Ok, so, what Abraham said was right. On the second script, the results from `if($notifications | any { $_.Id -eq $id }) ...` would need to be stored in a variable and exported using `Export-ModuleMember` – Santiago Squarzon Jan 11 '22 at 01:47
  • The problem is that $id is a local function variable. That means I'd have to export this variable each time the function is called. I'm hoping for another solution. – saml Jan 11 '22 at 02:13

1 Answers1

2

Invoke-Expression (which, when possible, should be avoided) implicitly recreates the script block passed from the caller's scope, via its string representation, in the context of the module, which invalidates any references to the caller's state in the script-block code (because modules generally don't see an outside caller's state, except for the global scope).

The solution is to execute the script block as-is, but provide it pipeline input as passed to the module function:

# Note: New-Module creates a *dynamic* (in-memory only) module,
#       but the behavior applies equally to regular, persisted modules.
$null = New-Module {
  function Test-Any ([ScriptBlock] $FilterScript)
  {    
      begin {
          $done = $false
      }
      process { 
          if (!$done)
          {
              # Note the use of $_ | ... to provide pipeline input
              # and the use of ForEach-Object to evaluate the script block.
              if (!$FilterScript -or ($_ | ForEach-Object $FilterScript)) {
                  $done = $true
              }
          }
      }
      end {
          $done
      }
  }
  
}

# Sample call. Should yield $true
$id = 1
@{ Id = 2 }, @{ Id = 1 } | Test-Any { $_.Id -eq $id }

Note: The Test-Any function in this answer uses a similar approach, but tries to optimize processing by stopping further pipeline processing - which, however, comes at the expense of incurring an on-demand compilation penalty the first time the function is called in the session, because - as of PowerShell 7.2 - you cannot (directly) stop a pipeline on demand from user code - see GitHub issue #3821.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • This is it! I've been trying to break it and aside from `$null | any` returning `True` (which is a whole other issue) it works exactly as I hoped! You are a gentleman and a scholar. And the explanation to boot! – saml Jan 11 '22 at 03:02
  • 1
    Glad to hear it helped, @saml; my pleasure. Please also see my update re on-demand stopping of the pipeline. As for `$null` being considered "something" in the pipeline: it may be surprising, but note that PowerShell has its own definition of "nothing", which is "AutomationNull" (`[System.Management.Automation.Internal.AutomationNull]::Value`) - see [this answer](https://stackoverflow.com/a/66410615/45375). – mklement0 Jan 11 '22 at 03:13