Home > other >  How does $_ get set in a ScriptBlock?
How does $_ get set in a ScriptBlock?

Time:01-28

I'm looking to have a function in script where I can use a ScriptBlock passed in as either a predicate or with Where-Object.

I can write

cat .\.gitignore | Where-Object { $_.contains('pp') }

and this works; as does:

$f =  { $_.contains('pp') }; cat .gitignore | Where-Object $f

however trying

$f.Invoke( 'apple' )

results in

MethodInvocationException: Exception calling "Invoke" with "1" argument(s): "You cannot call a method on a null-valued expression.

Whereas I expected True. So clearly $_ wasn't set.

Likewise

$ff = { echo "args: $args`nauto: $_" }; $ff.Invoke( 'apple' )

outputs

args: apple
auto:

So $_ is clearly not getting set.

'apple' | %{ $_.contains('pp') }

Works, but I want the scriptblock to be a variable and

$f = { $_.contains('pp') }; 'apple' | %$f

Is a compile error.


tl;dr: So how do I set/pass the value of $_ inside a scriptblock I am invoking?

CodePudding user response:

In the context of a process block of a Script Block, the $_ ($PSItem) variable is automatically populated and represents each element coming from the pipeline, i.e.:

$f = { process { $_.contains('pp') }}
'apple' | & $f # True

You can however achieve the same using InvokeWithContext method from the ScriptBlock Class:

$f = { $_.contains('pp') }
$f.InvokeWithContext($null, [psvariable]::new('_', 'apple')) # True

Do note, this method always returns Collection`1. Output is not enumerated.


Worth noting as zett42 points out, the scoping rules of script blocks invoked via it's methods or via the call operator & still apply.

Script Blocks are able to see parent scope variables (does not include Remoting):

$foo = 'hello'
{ $foo }.Invoke() # hello

But are not able to update them:

$foo = 'hello'
{ $foo = 'world' }.Invoke()
$foo # hello

Unless using a scope a modifier (applies only to Value Types):

$foo = 'hello'
{ $script:foo = 'world' }.Invoke()
$foo # world

Or via the dot sourcing operator .:

$foo = 'hello'
. { $foo = 'world' }
$foo # world

# still applies with pipelines!
$foo = 'hello'
'world' | . { process { $foo = $_ }}
$foo # world

See about Scopes for more details.

CodePudding user response:

Using the .Invoke() method (and its variants, .InvokeReturnAsIs() and .InvokeWithContext()) to execute a script block in PowerShell code is best avoided, because it changes the semantics of the call in several respects - see this answer for more information.

While the PowerShell-idiomatic equivalent is &, the call operator, it is not enough here, given that you want want the automatic $_ variable to be defined in your script block.

The easiest way to define $_ based on input is indeed ForEach-Object (one of whose built-in aliases is %):

$f = { $_.contains('pp') }
ForEach-Object -Process $f -InputObject 'apple'  # -> $true

Note, however, that -InputObject only works meaningfully for a single input object (though you may pass an array / collection in which case $_ then refers to it as a whole); to provide multiple ones, use the pipeline:

'apple', 'pear' | ForEach-Object $f  # $true, $false

# Equivalent, with alias
'apple', 'pear' | % $f

If, by contrast, your intent is simply for your script block to accept arguments, you don't need $_ at all and can simply make your script either formally declare parameter(s) or use the automatic $args variable which contains all (unbound) positional arguments:

# With $args: $args[0] is the first positional argument.
$f = { $args[0].contains('pp') }
& $f 'apple'


# With declared parameter.
$f = { param([string] $fruit) $fruit.contains('pp') }
& $f 'apple'

For more information about the parameter-declaration syntax, see the conceptual about_Functions help topic (script blocks are basically unnamed functions, and only the param(...) declaration style can be used in script blocks).

CodePudding user response:

I got it to work by wrapping $f in () like

$f = { $_.contains('pp') }; 'apple' | %($f)

...or (thanks to @zett42) by placing a space between the % and $ like

$f = { $_.contains('pp') }; 'apple' | % $f

Can even pass in the value from a variable

$f = { $_.contains('pp') }; $a = 'apple'; $a | %($f)

Or use it inside an If-statement

$f = { $_.contains('pp') }; $a = 'apple'; If ( $a | %($f) ){ echo 'yes' }    

So it appears that $_ is only set by having things 'piped' (aka \) into it? But why this is and how it works, and if this can be done through .invoke() is unknown to me. If anyone can explain this please do.

From What does $_ mean in PowerShell? and the related documentation, it seems like $PSItem is indeed a better name since it isn't like Perl's $_

  • Related