Why can't I use $_ in write-host? - powershell

I am trying to pipe an array of strings to write-host and explicitly use $_ to write those strings:
'foo', 'bar', 'baz' | write-host $_
However, it fails with:
The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
This error message makes no sense to me because I am perfectly able to write
'foo', 'bar', 'baz' | write-host
I would have expected both pipelines to be equivalent. Apparently, they're not. So, what's the difference?

tl;dr
The automatic $_ variable and its alias, $PSItem, only ever have meaningful values inside script blocks ({ ... }), in specific contexts.
The bottom section lists all relevant contexts.
Update: A new conceptual help topic, about_PSItem, is now available, which covers most of what is in the bottom section.
I would have expected both pipelines to be equivalent.
They're not:
'foo', 'bar', 'baz' | write-host
It is the pipeline-based equivalent of the following (equivalent in ultimate effect, not technically):
foreach ($str in 'foo', 'bar', 'baz') { Write-Host -Object $str }
That is, in your command Write-Host receives input from the pipeline that implicitly binds to its -Object parameter for each input object, by virtue of parameter -Object being declared as accepting pipeline input via attribute [Parameter(ValueFromPipeline=$true)]
'foo', 'bar', 'baz' | write-host $_
Before pipeline processing begins, arguments - $_ in your case - are bound to parameters first:
Since $_ isn't preceded by a parameter name, it binds positionally to the - implied - -Object parameter.
Then, when pipeline processing begins, pipeline parameter binding finds no pipeline-binding Write-Host parameter to bind to anymore, given that the only such parameter, -Object has already been bound, namely by an argument $_.
In other words: your command mistakenly tries to bind the -Object parameter twice; unfortunately, the error message doesn't exactly make that clear.
The larger point is that using $_ only ever makes sense inside a script block ({ ... }) that is evaluated for each input object.
Outside that context, $_ (or its alias, $PSItem) typically has no value and shouldn't be used - see the bottom section for an overview of all contexts in which $_ / $PSItem inside a script block is meaningfully supported.
While $_ is most typically used in the script blocks passed to the ForEach-Object and Where-Object cmdlets, there are other useful applications, most typically seen with the Rename-Item cmdlet: a delay-bind script-block argument:
# Example: rename *.txt files to *.dat files using a delay-bind script block:
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
That is, instead of passing a static new name to Rename-Item, you pass a script block that is evaluated for each input object - with the input object bound to $_, as usual - which enables dynamic behavior.
As explained in the linked answer, however, this technique only works with parameters that are both (a) pipeline-binding and (b) not [object] or [scriptblock] typed; therefore, given that Write-Object's -Object parameter is [object] typed, the technique does not work:
# Try to enclose all inputs in [...] on output.
# !! DOES NOT WORK.
'foo', 'bar', 'baz' | write-host -Object { "[$_]" }
Therefore, a pipeline-based solution requires the use of ForEach-Object in this case:
# -Object is optional
PS> 'foo', 'bar', 'baz' | ForEach-Object { write-host -Object "[$_]" }
[foo]
[bar]
[baz]
Contexts in which $_ (and its alias, $PSItem) is meaningfully defined:
What these contexts have in common is that the $_ / $PSItem reference must be made inside a script block ({ ... }), namely one passed to / used in:
... the ForEach-Object and Where-Object cmdlets; e.g.:
1..3 | ForEach-Object { 1 + $_ } # -> 2, 3, 4
... the intrinsic .ForEach() and intrinsic .Where() methods; e.g.:
(1..3).ForEach({ 1 + $_ }) # -> 2, 3, 4
... a parameter, assuming that parameter allows a script block to act as a delay-bind script-block parameter; e.g.:
# Rename all *.txt files to *.dat files.
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
... conditionals and associated script blocks inside a switch statement; e.g.:
# -> 'is one or three: one', 'is one or three: three'
switch ('one', 'two', 'three') {
{ $_ -in 'one', 'three' } { 'is one or three: ' + $_ }
}
... simple function and filters; e.g.:
# -> 2, 3
function Add-One { process { 1 + $_ } }; 1..2 | Add-One
# -> 2, 3
filter Add-One { 1 + $_ }; 1..2 | Add-One
... direct subscriptions to an object's event (n/a to the script block that is passed to the -Action parameter of a Register-ObjectEvent call); e.g::Tip of the hat to Santiago Squarzon.
# In a direct event-subscription script block used in the
# context of WinForms; e.g:
$txtBox.Add_KeyPress({
param($sender, $eventArgs)
# The alternative to the explicitly defined parameters above is:
# $this ... implicitly the same as $sender, i.e. the event-originating object
# $_ / $PSItem ... implicitly the same as $eventArgs, i.e. the event-arguments object.
})
... the [ValidateScript()] attribute in parameter declarations; note that for array-valued parameters the script block is called for each element; e.g.,
function Get-Foo {
param(
[ValidateScript({ 0 -eq ($_ % 2) })]
[int[]] $Number
)
"All numbers are even: $Number"
}
PowerShell (Core) only: ... the substitution operand of the -replace operator; e.g.:
# -> 'a10, 'a20'
'a1', 'a2' -replace '\d+', { 10 * [int] $_.Value }
... in the context of <ScriptBlock> elements in formatting files (but not in the context of script block-based ETS members, where $this is used instead).
... in the context of using PowerShell SDK methods such as .InvokeWithContext()
# -> 43
{ 1 + $_ }.InvokeWithContext($null, [psvariable]::new('_', 42), $null)

You can use it as iRon has indicated in the comments. $_ or $PSItem is the current object in the pipeline that is being processed. Typically, you see this with commands that require a processing or script block. You would have to contain your Write-Host command within a similar processing block.
'foo', 'bar', 'baz' | ForEach-Object {write-host $_}
Here is an example using the process block of a function:
function write-stuff {
process { write-host $_ }
}
'foo', 'bar', 'baz' | write-stuff
bar
foo
hi

$_ will never work outside a scriptblock. How about write-output instead:
'hi' | Write-Output -InputObject { $_ + ' there' }
hi there

Related

How does $_ get set in a ScriptBlock?

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?
Note, this answer only covers how does $_ gets populated in the context of a process block of a script block. Other use cases can be found in the about_PSItem documentation.
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.
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).
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 $_

Redirect/Capture Write-Host output even with -NoNewLine

The function Select-WriteHost from an answer to another Stackoverflow question (see code below) will redirect/capture Write-Host output:
Example:
PS> $test = 'a','b','c' |%{ Write-Host $_ } | Select-WriteHost
a
b
c
PS> $test
a
b
c
However, if I add -NoNewLine to Write-Host, Select-WriteHost will ignore it:
PS> $test = 'a','b','c' |%{ Write-Host -NoNewLine $_ } | Select-WriteHost
abc
PS> $test
a
b
c
Can anyone figure out how to modify Select-WriteHost (code below) to also support -NoNewLine?
function Select-WriteHost
{
[CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
param(
[Parameter(ValueFromPipeline = $true, ParameterSetName = 'FromPipeline')]
[object] $InputObject,
[Parameter(Mandatory = $true, ParameterSetName = 'FromScriptblock', Position = 0)]
[ScriptBlock] $ScriptBlock,
[switch] $Quiet
)
begin
{
function Cleanup
{
# Clear out our proxy version of write-host
remove-item function:\write-host -ea 0
}
function ReplaceWriteHost([switch] $Quiet, [string] $Scope)
{
# Create a proxy for write-host
$metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Microsoft.PowerShell.Utility\Write-Host')
$proxy = [System.Management.Automation.ProxyCommand]::create($metaData)
# Change its behavior
$content = if($quiet)
{
# In quiet mode, whack the entire function body,
# simply pass input directly to the pipeline
$proxy -replace '(?s)\bbegin\b.+', '$Object'
}
else
{
# In noisy mode, pass input to the pipeline, but allow
# real Write-Host to process as well
$proxy -replace '(\$steppablePipeline\.Process)', '$Object; $1'
}
# Load our version into the specified scope
Invoke-Expression "function ${scope}:Write-Host { $content }"
}
Cleanup
# If we are running at the end of a pipeline, we need
# to immediately inject our version into global
# scope, so that everybody else in the pipeline
# uses it. This works great, but it is dangerous
# if we don't clean up properly.
if($pscmdlet.ParameterSetName -eq 'FromPipeline')
{
ReplaceWriteHost -Quiet:$quiet -Scope 'global'
}
}
process
{
# If a scriptblock was passed to us, then we can declare
# our version as local scope and let the runtime take
# it out of scope for us. It is much safer, but it
# won't work in the pipeline scenario.
#
# The scriptblock will inherit our version automatically
# as it's in a child scope.
if($pscmdlet.ParameterSetName -eq 'FromScriptBlock')
{
. ReplaceWriteHost -Quiet:$quiet -Scope 'local'
& $scriptblock
}
else
{
# In a pipeline scenario, just pass input along
$InputObject
}
}
end
{
Cleanup
}
}
PS: I tried inserting -NoNewLine to the line below (just to see how it would react) however, its producing the exception, "Missing function body in function declaration"
Invoke-Expression "function ${scope}:Write-Host { $content }"
to:
Invoke-Expression "function ${scope}:Write-Host -NoNewLine { $content }"
(Just to recap) Write-Host is meant for host, i.e. display / console output only, and originally couldn't be captured (in-session) at all. In PowerShell 5, the ability to capture Write-Host output was introduced via the information stream, whose number is 6, enabling techniques such as redirection 6>&1 in order to merge Write-Host output into the success (output) stream (whose number is 1), where it can be captured as usual.
However, due to your desire to use the -NoNewLine switch across several calls, 6>&1 by itself is not enough, because the concept of not emitting a newline only applies to display output, not to distinct objects in the pipeline.
E.g., in the following call -NoNewLine is effectively ignored, because there are multiple Write-Host calls producing multiple output objects (strings) that are captured separately:
'a','b','c' | % { Write-Host $_ -NoNewline } 6>&1
Your Select-WriteHost function - necessary in PowerShell 4 and below only - would have the same problem if you adapted it to support the -NoNewLine switch.
An aside re 6>&1: The strings that Write-Host invariably outputs are wrapped in [System.Management.Automation.InformationRecord] instances, due to being re-routed via the information stream. In display output you will not notice the difference, but to get the actual string you need to access the .MessageData.Message property or simply call .ToString().
There is no general solution I am aware of, but situationally the following may work:
If you know that the code of interest uses only Write-Host -NoNewLine calls:
Simply join the resulting strings after the fact without a separator to emulate -NoNewLine behavior:
# -> 'abc'
# Note: Whether or not you use -NoNewLine here makes no difference.
-join ('a','b','c' | % { Write-Host -NoNewLine $_ })
If you know that all instances of Write-Host -NoNewLine calls apply only to their respective pipeline input, you can write a simplified proxy function that collects all input up front and performs separator-less concatenation of the stringified objects:
# -> 'abc'
$test = & {
# Simplified proxy function
function Write-Host {
param([switch] $NoNewLine)
if ($MyInvocation.ExpectingInput) { $allInput = $Input }
else { $allInput = $args }
if ($NoNewLine) { -join $allInput.ForEach({ "$_" }) }
else { $allInput.ForEach({ "$_" }) }
}
# Important: pipe all input directly.
'a','b','c' | Write-Host -NoNewLine
}

Piping in PowerShell [duplicate]

I am trying to pipe an array of strings to write-host and explicitly use $_ to write those strings:
'foo', 'bar', 'baz' | write-host $_
However, it fails with:
The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
This error message makes no sense to me because I am perfectly able to write
'foo', 'bar', 'baz' | write-host
I would have expected both pipelines to be equivalent. Apparently, they're not. So, what's the difference?
tl;dr
The automatic $_ variable and its alias, $PSItem, only ever have meaningful values inside script blocks ({ ... }), in specific contexts.
The bottom section lists all relevant contexts.
Update: A new conceptual help topic, about_PSItem, is now available, which covers most of what is in the bottom section.
I would have expected both pipelines to be equivalent.
They're not:
'foo', 'bar', 'baz' | write-host
It is the pipeline-based equivalent of the following (equivalent in ultimate effect, not technically):
foreach ($str in 'foo', 'bar', 'baz') { Write-Host -Object $str }
That is, in your command Write-Host receives input from the pipeline that implicitly binds to its -Object parameter for each input object, by virtue of parameter -Object being declared as accepting pipeline input via attribute [Parameter(ValueFromPipeline=$true)]
'foo', 'bar', 'baz' | write-host $_
Before pipeline processing begins, arguments - $_ in your case - are bound to parameters first:
Since $_ isn't preceded by a parameter name, it binds positionally to the - implied - -Object parameter.
Then, when pipeline processing begins, pipeline parameter binding finds no pipeline-binding Write-Host parameter to bind to anymore, given that the only such parameter, -Object has already been bound, namely by an argument $_.
In other words: your command mistakenly tries to bind the -Object parameter twice; unfortunately, the error message doesn't exactly make that clear.
The larger point is that using $_ only ever makes sense inside a script block ({ ... }) that is evaluated for each input object.
Outside that context, $_ (or its alias, $PSItem) typically has no value and shouldn't be used - see the bottom section for an overview of all contexts in which $_ / $PSItem inside a script block is meaningfully supported.
While $_ is most typically used in the script blocks passed to the ForEach-Object and Where-Object cmdlets, there are other useful applications, most typically seen with the Rename-Item cmdlet: a delay-bind script-block argument:
# Example: rename *.txt files to *.dat files using a delay-bind script block:
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
That is, instead of passing a static new name to Rename-Item, you pass a script block that is evaluated for each input object - with the input object bound to $_, as usual - which enables dynamic behavior.
As explained in the linked answer, however, this technique only works with parameters that are both (a) pipeline-binding and (b) not [object] or [scriptblock] typed; therefore, given that Write-Object's -Object parameter is [object] typed, the technique does not work:
# Try to enclose all inputs in [...] on output.
# !! DOES NOT WORK.
'foo', 'bar', 'baz' | write-host -Object { "[$_]" }
Therefore, a pipeline-based solution requires the use of ForEach-Object in this case:
# -Object is optional
PS> 'foo', 'bar', 'baz' | ForEach-Object { write-host -Object "[$_]" }
[foo]
[bar]
[baz]
Contexts in which $_ (and its alias, $PSItem) is meaningfully defined:
What these contexts have in common is that the $_ / $PSItem reference must be made inside a script block ({ ... }), namely one passed to / used in:
... the ForEach-Object and Where-Object cmdlets; e.g.:
1..3 | ForEach-Object { 1 + $_ } # -> 2, 3, 4
... the intrinsic .ForEach() and intrinsic .Where() methods; e.g.:
(1..3).ForEach({ 1 + $_ }) # -> 2, 3, 4
... a parameter, assuming that parameter allows a script block to act as a delay-bind script-block parameter; e.g.:
# Rename all *.txt files to *.dat files.
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
... conditionals and associated script blocks inside a switch statement; e.g.:
# -> 'is one or three: one', 'is one or three: three'
switch ('one', 'two', 'three') {
{ $_ -in 'one', 'three' } { 'is one or three: ' + $_ }
}
... simple function and filters; e.g.:
# -> 2, 3
function Add-One { process { 1 + $_ } }; 1..2 | Add-One
# -> 2, 3
filter Add-One { 1 + $_ }; 1..2 | Add-One
... direct subscriptions to an object's event (n/a to the script block that is passed to the -Action parameter of a Register-ObjectEvent call); e.g::Tip of the hat to Santiago Squarzon.
# In a direct event-subscription script block used in the
# context of WinForms; e.g:
$txtBox.Add_KeyPress({
param($sender, $eventArgs)
# The alternative to the explicitly defined parameters above is:
# $this ... implicitly the same as $sender, i.e. the event-originating object
# $_ / $PSItem ... implicitly the same as $eventArgs, i.e. the event-arguments object.
})
... the [ValidateScript()] attribute in parameter declarations; note that for array-valued parameters the script block is called for each element; e.g.,
function Get-Foo {
param(
[ValidateScript({ 0 -eq ($_ % 2) })]
[int[]] $Number
)
"All numbers are even: $Number"
}
PowerShell (Core) only: ... the substitution operand of the -replace operator; e.g.:
# -> 'a10, 'a20'
'a1', 'a2' -replace '\d+', { 10 * [int] $_.Value }
... in the context of <ScriptBlock> elements in formatting files (but not in the context of script block-based ETS members, where $this is used instead).
... in the context of using PowerShell SDK methods such as .InvokeWithContext()
# -> 43
{ 1 + $_ }.InvokeWithContext($null, [psvariable]::new('_', 42), $null)
You can use it as iRon has indicated in the comments. $_ or $PSItem is the current object in the pipeline that is being processed. Typically, you see this with commands that require a processing or script block. You would have to contain your Write-Host command within a similar processing block.
'foo', 'bar', 'baz' | ForEach-Object {write-host $_}
Here is an example using the process block of a function:
function write-stuff {
process { write-host $_ }
}
'foo', 'bar', 'baz' | write-stuff
bar
foo
hi
$_ will never work outside a scriptblock. How about write-output instead:
'hi' | Write-Output -InputObject { $_ + ' there' }
hi there

Tab-complete a parameter value based on another parameter's already specified value

This question addresses the following scenario:
Can custom tab-completion for a given command dynamically determine completions based on the value previously passed to another parameter on the same command line, using either a parameter-level [ArgumentCompleter()] attribute or the Register-ArgumentCompleter cmdlet?
If so, what are the limitations of this approach?
Example scenario:
A hypothetical Get-Property command has an -Object parameter that accepts an object of any type, and a -Property parameter that accepts the name of a property whose value to extract from the object.
Now, in the course of typing a Get-Property call, if a value is already specified for -Object, tab-completing -Property should cycle through the names of the specified object's (public) properties.
$obj = [pscustomobject] #{ foo = 1; bar = 2; baz = 3 }
Get-Property -Object $obj -Property # <- pressing <tab> here should cycle
# through 'foo', 'bar', 'baz'
#mklement0, regarding first limitation stated in your answer
The custom-completion script block ({ ... }) invoked by PowerShell fundamentally only sees values specified via parameters, not via the pipeline.
I struggled with this, and after some stubbornness I got a working solution.
At least good enough for my tooling, and I hope it can make life easier for many others out there.
This solution has been verified to work with PowerShell versions 5.1 and 7.1.2.
Here I made use of $cmdAst (called $commandAst in the docs), which contains information about the pipeline. With this we can get to know the previous pipeline element and even differentiate between it containing only a variable or a command. Yes, A COMMAND, which with help of Get-Command and the command's OutputType() member method, we can get (suggested) property names for such as well!
Example usage
PS> $obj = [pscustomobject] #{ foo = 1; bar = 2; baz = 3 }
PS> $obj | Get-Property -Property # <tab>: bar, baz, foo
PS> "la", "na", "le" | Select-String "a" | Get-Property -Property # <tab>: Chars, Context, Filename, ...
PS> 2,5,2,2,6,3 | group | Get-Property -Property # <tab>: Count, Values, Group, ...
Function code
Note that apart from now using $cmdAst, I also added [Parameter(ValueFromPipeline=$true)] so we actually pick the object, and PROCESS {$Object.$Property} so that one can test and see the code actually working.
param(
[Parameter(ValueFromPipeline=$true)]
[object] $Object,
[ArgumentCompleter({
param($cmdName, $paramName, $wordToComplete, $cmdAst, $preBoundParameters)
# Find out if we have pipeline input.
$pipelineElements = $cmdAst.Parent.PipelineElements
$thisPipelineElementAsString = $cmdAst.Extent.Text
$thisPipelinePosition = [array]::IndexOf($pipelineElements.Extent.Text, $thisPipelineElementAsString)
$hasPipelineInput = $thisPipelinePosition -ne 0
$possibleArguments = #()
if ($hasPipelineInput) {
# If we are in a pipeline, find out if the previous pipeline element is a variable or a command.
$previousPipelineElement = $pipelineElements[$thisPipelinePosition - 1]
$pipelineInputVariable = $previousPipelineElement.Expression.VariablePath.UserPath
if (-not [string]::IsNullOrEmpty($pipelineInputVariable)) {
# If previous pipeline element is a variable, get the object.
# Note that it can be a non-existent variable. In such case we simply get nothing.
$detectedInputObject = Get-Variable |
Where-Object {$_.Name -eq $pipelineInputVariable} |
ForEach-Object Value
} else {
$pipelineInputCommand = $previousPipelineElement.CommandElements[0].Value
if (-not [string]::IsNullOrEmpty($pipelineInputCommand)) {
# If previous pipeline element is a command, check if it exists as a command.
$possibleArguments += Get-Command -CommandType All |
Where-Object Name -Match "^$pipelineInputCommand$" |
# Collect properties for each documented output type.
ForEach-Object {$_.OutputType.Type} | ForEach-Object GetProperties |
# Group properties by Name to get unique ones, and sort them by
# the most frequent Name first. The sorting is a perk.
# A command can have multiple output types. If so, we might now
# have multiple properties with identical Name.
Group-Object Name -NoElement | Sort-Object Count -Descending |
ForEach-Object Name
}
}
} elseif ($preBoundParameters.ContainsKey("Object")) {
# If not in pipeline, but object has been given, get the object.
$detectedInputObject = $preBoundParameters["Object"]
}
if ($null -ne $detectedInputObject) {
# The input object might be an array of objects, if so, select the first one.
# We (at least I) are not interested in array properties, but the object element's properties.
if ($detectedInputObject -is [array]) {
$sampleInputObject = $detectedInputObject[0]
} else {
$sampleInputObject = $detectedInputObject
}
# Collect property names.
$possibleArguments += $sampleInputObject | Get-Member -MemberType Properties | ForEach-Object Name
}
# Refering to about_Functions_Argument_Completion documentation.
# The ArgumentCompleter script block must unroll the values using the pipeline,
# such as ForEach-Object, Where-Object, or another suitable method.
# Returning an array of values causes PowerShell to treat the entire array as one tab completion value.
$possibleArguments | Where-Object {$_ -like "$wordToComplete*"}
})]
[string] $Property
)
PROCESS {$Object.$Property}
Update: See betoz's helpful answer for a more complete solution that also supports pipeline input.
The part of the answer below that clarifies the limitations of pre-execution detection of the input objects' data type still applies.
The following solution uses a parameter-specific [ArgumentCompleter()] attribute as part of the definition of the Get-Property function itself, but the solution analogously applies to separately defining custom-completion logic via the Register-CommandCompleter cmdlet.
Limitations:
[See betoz's answer for how to overcome this limitation] The custom-completion script block ({ ... }) invoked by PowerShell fundamentally only sees values specified via parameters, not via the pipeline.
That is, if you type Get-Property -Object $obj -Property <tab>, the script block can determine that the value of $obj is to be bound to the -Object parameter, but that wouldn't work with
$obj | Get-Property -Property <tab> (even if -Object is declared as pipeline-binding).
Fundamentally, only values that can be evaluated without side effects are actually accessible in the script block; in concrete terms, this means:
Literal values (e.g., -Object ([pscustomobject] #{ foo = 1; bar = 2; baz = 3 })
Simple variable references (e.g., -Object $obj) or property-access or index-access expressions (e.g., -Object $obj.Foo or -Object $obj[0])
Notably, the following values are not accessible:
Method-call results (e.g., -Object $object.Foo())
Command output (via (...), $(...), or #(...), e.g.
-Object (Invoke-RestMethod http://example.org))
The reason for this limitation is that evaluating such values before actually submitting the command could have undesirable side effects and / or could take a long time to complete.
function Get-Property {
param(
[object] $Object,
[ArgumentCompleter({
# A fixed list of parameters is passed to an argument-completer script block.
# Here, only two are of interest:
# * $wordToComplete:
# The part of the value that the user has typed so far, if any.
# * $preBoundParameters (called $fakeBoundParameters
# in the docs):
# A hashtable of those (future) parameter values specified so
# far that are side effect-free (see above).
param($cmdName, $paramName, $wordToComplete, $cmdAst, $preBoundParameters)
# Was a side effect-free value specified for -Object?
if ($obj = $preBoundParameters['Object']) {
# Get all property names of the objects and filter them
# by the partial value already typed, if any,
# interpreted as a name prefix.
#($obj.psobject.Properties.Name) -like "$wordToComplete*"
}
})]
[string] $Property
)
# ...
}

PowerShell: Invoking a script block that contains underscore variable

I normally do the following to invoke a script block containing $_:
$scriptBlock = { $_ <# do something with $_ here #> }
$theArg | ForEach-Object $scriptBlock
In effect, I am creating a pipeline which will give $_ its value (within the Foreach-Object function invocation).
However, when looking at the source code of the LINQ module, it defines and uses the following function to invoke the delegate:
# It is actually surprisingly difficult to write a function (in a module)
# that uses $_ in scriptblocks that it takes as parameters. This is a strange
# issue with scoping that seems to only matter when the function is a part
# of a module which has an isolated scope.
#
# In the case of this code:
# 1..10 | Add-Ten { $_ + 10 }
#
# ... the function Add-Ten must jump through hoops in order to invoke the
# supplied scriptblock in such a way that $_ represents the current item
# in the pipeline.
#
# Which brings me to Invoke-ScriptBlock.
# This function takes a ScriptBlock as a parameter, and an object that will
# be supplied to the $_ variable. Since the $_ may already be defined in
# this scope, we need to store the old value, and restore it when we are done.
# Unfortunately this can only be done (to my knowledge) by hitting the
# internal api's with reflection. Not only is this an issue for performance,
# it is also fragile. Fortunately this appears to still work in PowerShell
# version 2 through 3 beta.
function Invoke-ScriptBlock {
[CmdletBinding()]
param (
[Parameter(Position=1,Mandatory=$true)]
[ScriptBlock]$ScriptBlock,
[Parameter(ValueFromPipeline=$true)]
[Object]$InputObject
)
begin {
# equivalent to calling $ScriptBlock.SessionState property:
$SessionStateProperty = [ScriptBlock].GetProperty('SessionState',([System.Reflection.BindingFlags]'NonPublic,Instance'))
$SessionState = $SessionStateProperty.GetValue($ScriptBlock, $null)
}
}
process {
$NewUnderBar = $InputObject
$OldUnderBar = $SessionState.PSVariable.GetValue('_')
try {
$SessionState.PSVariable.Set('_', $NewUnderBar)
$SessionState.InvokeCommand.InvokeScript($SessionState, $ScriptBlock, #())
}
finally {
$SessionState.PSVariable.Set('_', $OldUnderBar)
}
}
}
This strikes me as a bit low-level. Is there a recommended, safe way of doing this?
You can invoke scriptblocks with the ampersand. No need to use Foreach-Object.
$scriptblock = {## whatever}
& $scriptblock
#(1,2,3) | % { & {write-host $_}}
To pass parameters:
$scriptblock = {write-host $args[0]}
& $scriptblock 'test'
$scriptBlock = {param($NamedParam) write-host $NamedParam}
& $scriptBlock -NamedParam 'test'
If you're going to be using this inside of Invoke-Command, you could also usin the $using construct.
$test = 'test'
$scriptblock = {write-host $using:test}