4

I'm trying to put this function:

function Test-Any {
    [CmdletBinding()]
    param($EvaluateCondition,
        [Parameter(ValueFromPipeline = $true)] $ObjectToTest)
    begin {
        $any = $false
    }
    process {
        if (-not $any -and (& $EvaluateCondition $ObjectToTest)) {
            $any = $true
        }
    }
    end {
        $any
    }
}

into a module. I just created a new module, the my-scripts.psm1 file, which contains just the above function and import it with Import-Module <absolute path>.

The problem is that if I use the function from the module 1..4 | Test-Any { $_ -gt 3 } returns false, because $_ is not set to the value from the pipe.

If I define the function normally in a script and use it from there it works as expected (with $_ getting assigned the integer values).

This happens with PowerShell v4.0 under Windows 7.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
Voo
  • 29,040
  • 11
  • 82
  • 156
  • Thanks to @PetSerAl's help, there's now a robust and optimizing (exits pipeline as soon as the test succeeds) version of this function in my answer [here](http://stackoverflow.com/a/34800670/45375). – mklement0 Jan 16 '16 at 22:14

2 Answers2

5

That command: & $EvaluateCondition $ObjectToTest — does not bind anything to $_. In absence of a param() block in ScriptBlock, the value of $ObjectToTest will be bound to $args[0].

$SB = {"`$_: '$_'; `$args[0]:'$($args[0])'"}
1..3 | ForEach-Object {& $SB ($_+3)}

Output:

$_: '1'; $args[0]:'4'
$_: '2'; $args[0]:'5'
$_: '3'; $args[0]:'6'

Why does referencing $_ work: you simply reference the $_ variable from the parent scope.

The value of $_ that you see, is a current pipeline input object, passed to the Test-Any function.

function Test-Any {
    param($EvaluateCondition)
    process {
        "Test-Any `$_: '$_'"
        & $EvaluateCondition
    }
}
1..2 | %{3..4 | Test-Any {"EvaluateCondition `$_:'$_'"}}

Output:

Test-Any $_: '3'
EvaluateCondition $_:'3'
Test-Any $_: '4'
EvaluateCondition $_:'4'
Test-Any $_: '3'
EvaluateCondition $_:'3'
Test-Any $_: '4'
EvaluateCondition $_:'4'

When you define Test-Any in module scope, then variable $_ with pipeline input to Test-Any also got defined in that module scope and was not available outside of it.

New-Module {
    function Test-Any {
        param($EvaluateCondition)
        process {
            "Test-Any `$_: '$_'"
            & $EvaluateCondition
        }
    }
} | Out-Null
1..2 | %{3..4 | Test-Any {"EvaluateCondition `$_:'$_'"}}

Output:

Test-Any $_: '3'
EvaluateCondition $_:'1'
Test-Any $_: '4'
EvaluateCondition $_:'1'
Test-Any $_: '3'
EvaluateCondition $_:'2'
Test-Any $_: '4'
EvaluateCondition $_:'2'

If you want to invoke a script block with some value bound to $_, then one way to do this would be:

ForEach-Object $EvaluateCondition -InputObject $ObjectToTest
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user4003407
  • 21,204
  • 4
  • 50
  • 60
1

The answer of user4003407 did solve my problem, but I don't find the examples very intuitive. So I'm writing this solution mainly to add an additional example.

I had the following function defined in my MyHelpers.psm1 file which was causing issues:

function ConvertTo-HashTable {
  param (
    [Parameter(Mandatory, ValueFromPipeline)]
    $InputObject,
    [scriptblock] $Key = { $InputObject },
    [scriptblock] $Value = { $InputObject }
  )

  begin { $HashTable = @{} }
  process { $HashTable.Add((& $Key), (& $Value)) }
  end { $HashTable }
}

On usage:

Import-Module 'ExchangeOnlineManagement'
Import-Module 'MyHelpers'

$UserLookup = Get-User | ConvertTo-HashTable -Key { $_.Sid }

I got the error:

EXCEPTION: Exception calling "Add" with "2" argument(s): "Key cannot be null. (Parameter 'key')"

Because $_ was not bound correctly, thus returning $null from the scriptblock.

To fix this I used ForEach-Object to correctly bind $_, as suggested by the answer of user4003407. So I changed the definition to:

function ConvertTo-HashTable {
  param (
    [Parameter(Mandatory, ValueFromPipeline)]
    $InputObject,
    [scriptblock] $Key = { $InputObject },
    [scriptblock] $Value = { $InputObject }
  )

  begin { $HashTable = @{} }
  process {
    $HashTable.Add(
      ($InputObject | ForEach-Object $Key),
      ($InputObject | ForEach-Object $Value)
    )
  }
  end { $HashTable }
}

I went for $InputObject | ForEach-Object $Key instead of ForEach-Object $Key -InputObject $InputObject, because I prefer the way it reads, but both should do the job.

If you're looking for a shorter solution you could also use its alias %, ($InputObject | % $Key), but the linter I'm using doesn't recommend it (AvoidUsingCmdletAliases) so I went with the full cmdlet name.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • I've done a whole lot of PowerShell since asking the question and I still don't understand why exactly ForEach-Object helps here and what happens behind the scenes. – Voo Jul 13 '23 at 10:12