PowerShell how to use $MyInvocation for recursion - powershell

I am trying to implement some recursive functions with PowerShell. Here is the basic function:
function MyRecursiveFunction {
param(
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
$input
)
if ($input -is [System.Array] -And $input.Length -eq 1) {
$input = $input[0]
}
if ($input -is [System.Array]) {
ForEach ($i in $input) {
$i | ##### HOW DO I USE $MyInvocation HERE TO CALL MyRecursiveFunction??? #####
}
return
}
# Do something with the single object...
}
I have looked at Invoke-Expression and Invoke-Item but have not been able to get the syntax right. For instance I tried
$i | Invoke-Expression $MyInvocation.MyCommand.Name
I'm guessing there is an easy way to do this if you know the right syntax :-)

A bit of an old question, but since there's no satisfactory answer and I've had the same question, here's my experience.
Calling the function by name will break if the name is changed, or if the function is out of scope. It's also not very nice to manage as changing the function name requires editing all the recursive calls, and it'll likely break in modules imported using the prefix option.
# A simple recursive countdown function
function countdown {
param([int]$count)
$count
if ($count -gt 0) { countdown ($count - 1) }
}
# And a way to break it
$foo = ${function:countdown}
function countdown { 'failed' }
& $foo 5
$MyInvocation.InvocationName is slightly nicer to work with, but will still break in the above example (although for different reasons).
The best way would seem to be calling the scriptblock of the function, $MyInvocation.MyCommand.ScriptBlock. That way it'll still work regardless of the function name/scope.
function countdown {
param([int]$count)
$count
if ($count -gt 0) { & $MyInvocation.MyCommand.ScriptBlock ($count - 1) }
}

Just Call the function:
$i | MyRecursiveFunction
To call it without knowing the name of the function you should be able to call it with $myInvocation.InvocationName:
Invoke-Expression "$i | $($myInvocation.InvocationName)"

Related

Use powershell tabexpansion function

When I run the statement man tabexpansion it shows that I simply need to provide -line and -lastword.
However, when I run a statement like this:
tabexpansion -line "c:/win" -lastword "c:/win"
It returns nothing.
Shouldn't it show at least C:\Windows? What am I doing wrong?
tabexpansion is a function involved in doing tab handling during manual command entry. It performs completion of various types of objects, like path names, variable names, function/cmdlet names, etc. The interface to this function is discussed at this SO post. The booked referenced describes overriding TabExpansion2.
I read elsewhere that TabExpansion is used in PS 2.0, whereas TabExpansion2 is used in 3.0.
As to why TabExpansion doesn't return anything, I can only answer for my system (which has PS 3.0). On my system cat function:tabexpansion gives:
[CmdletBinding()]
param(
[String] $line,
[String] $lastWord
)
process {
if ($line -eq "Install-Module $lastword" -or $line -eq "inmo $lastword"
-or $line -eq "ismo $lastword" -or $line -eq "upmo $lastword"
-or $line -eq "Update-Module $lastword") {
Get-PsGetModuleInfo -ModuleName "$lastword*" | % { $_.Id } | sort -Unique
}
elseif ( Test-Path -Path Function:\$tabExpansionBackup ) {
& $tabExpansionBackup $line $lastWord
}
}
Unless $line begins with a few specific tokens it goes to the elseif statement. There if the variable $tabexpansionBackup is not defined, the function is exited with no output. With the input in the OP, it gives the output you're seeing - none.

Powershell: idiomatic way of mutating items passing through a stream?

When processing an input stream it's sometimes necessary to effect changes on the objects passing through the stream. It's also useful to allow those objects to pass out the other end so that they may be piped into other processes. Is there a more idiomatic/concise way than this to mutate and pass along?
$input | % {
if ($_.Office.length -ne 11) { $_.Errors += "Bad office" }
$_ #needed to allow additional piping
}
Some other operator? Something like... %!
$input | %! {
if ($_.Office.length -ne 11) { $_.Errors += "Bad office" }
} #whatever goes in must come out
I don't see anything wrong with the first example there, but you could write your own function and then use that:
function Do-Modification {
[CmdletBinding()]
param(
[Parameter(
ValueFromPipeline=$true
)]
$myObject
)
Process {
if ($myObject.Office.length -ne 11) {
$myObject.Errors += "Bad Office"
}
$myObject
}
}
$inputs | Do-Modification
# or to use it in a larger ForEach-Object (even though it's redundant here)
$inputs | ForEach-Object {
$_ | Do-Modification
}
Edit to add:
This is a contrived example; it's probably not worth it to reinvent the wheel (writing your own pipeline function) except for code re-use (where it is very much worth it in my opinion). It's up to you to decide where one approach or the other makes sense.

Exiting the function while in loops like Foreach-Object in PowerShell

I have a function like this in Powershell:
function F()
{
$something | Foreach-Object {
if ($_ -eq "foo"){
# Exit from F here
}
}
# do other stuff
}
if I use Exit in the if statement, it exits powershell, I don't want this behavior. If I use return in the if statement, foreach keeps executing and the rest of the function is also executed. I came up with this:
function F()
{
$failed = $false
$something | Foreach-Object {
if ($_ -eq "foo"){
$failed = $true
break
}
}
if ($failed){
return
}
# do other stuff
}
I basically introduced a sentinel variable holding if I broke out of the loop or not. Is there a cleaner solution?
Any help?
function F()
{
Trap { Return }
$something |
Foreach-Object {
if ($_ -eq "foo"){ Throw }
else {$_}
}
}
$something = "a","b","c","foo","d","e"
F
'Do other stuff'
a
b
c
Do other stuff
I'm not entirely sure of your specific requirements, but I think you can simplify this by looking at it a different way. It looks like you just want to know if any $something == "foo" in which case this would make things a lot easier:
if($something ? {$_ -eq 'foo')) { return }
? is an alias for Where-Object. The downside to this is that it will iterate over every item in the array even after finding a match, so...
If you're indeed searching a string array, things can get even simpler:
if($something -Contains 'foo') { return }
If the array is more costly to iterate over, you might consider implementing an equivalent of the LINQ "Any" extension method in Powershell which would allow you to do:
if($something | Test-Any {$_ -eq 'foo'}) { return }
As an aside, while exceptions in the CLR aren't that costly, using them to direct procedural flow is an anti-pattern as it can lead to code that's hard to follow, or, put formally, it violates the principal of least surprise.

Is there a short circuit 'or' that returns the first 'true' value?

Scheme has a short-circuiting or that will return the first non-false value:
> (or 10 20 30)
10
> (or #f 20 30)
20
> (or #f)
#f
It does not evaluate its arguments until needed.
Is there something like this already in PowerShell?
Here's an approximation of it:
function or ()
{
foreach ($arg in $args) {
$val = & $arg; if ($val) { $val; break }
}
}
Example:
PS C:\> or { 10 } { 20 } { 30 }
10
Example:
PS C:\> $abc = $null
PS C:\> or { $abc } { 123 }
123
PS C:\> $abc = 456
PS C:\> or { $abc } { 123 }
456
You could do something like this:
10, $false, 20 | ? { $_ -ne $false } | select -First 1
The result is either the first value from the input list that isn't $false, or $null. Since $null is among the values that PowerShell treats as $false in comparisons, the above should do what you want.
As far as I know, there isn't anything like this built in. I think your function looks pretty good.
It might be more idiomatic to make it take pipelined input:
function or
{
foreach ($x in $input) {
$val = & $x; if ($val) { $val; break }
}
}
Example:
PS > $abc = $null
PS > { $abc },{ 123 } | or
123
PS > $abc = 456
PS > { $abc },{ 123 } | or
456
You're trying to make PowerShell use a Scheme-like syntax by way of your function. Don't do this. Write idiomatic PowerShell. Trying to coerce one language into looking like another language just makes things harder on yourself, introduces lots of room for bugs, and will confuse the %$^%&^*( out of whoever has to maintain your code after you're gone.
PowerShell does appear to short-circuit. Put this code in the ISE and set a breakpoint on the write-output lines in each function, then start the debugger (F5):
function first () {
write-output "first"
}
function second() {
write-output "second"
}
$true -or $(first) -or $(second);
$false -or $(first) -or $(second);
$false -or $(second) -or $(first);
$true evaluates to true (obviously), so it doesn't attempt to process the expression beyond that point. When the next to last line processes, only the breakpoint in first processes. When the last line processes, only the breakpoint in second() is hit.
As long as you give the function a good name, I think creating such a function is perfectly idiomatic Powershell. One tweak I would make to OP's implementation is to make passing in a script block optional:
function Select-FirstValue {
$args |
foreach { if ($_ -is [scriptblock]) { & $_ } else { $_ } } |
where { $_ } |
select -First 1
}
Then the caller only has to add { } brackets around arguments that could have side-effects or be performance costly.
I'm using such a function as a simple way to provide default values for params from a config file. It looks something like:
function Do-Something {
param([string]$Arg1)
$Arg1 = Select-FirstValue $Arg1 { Get-ConfigValue 'arg1' } 'default arg1 val'
[...]
}
This way, the config file only is only attempted to be read if the user does not pass in the argument.

How to enable OGV in a powershell function

How to enable Out-GridView in a function.
I mean,
"Hello" | Out-GridView
Works.
But if I have a simple function like this
function Count ([int]$times)
{
for ($i=1; $i -le $times;$i++)
{
Write-Host $i
}
}
Why calling Count 5 doest not support a pipe to Out-GridView?
The problem you are having is that Write-Host does not output to the pipeline at all. It writes directly to the screen. Replace Write-Host with Write-Output and it should work fine.
BTW, Write-Output is the default so you could just use:
function Count ([int]$times)
{
for ($i=1; $i -le $times;$i++)
{
$i
}
}
or even more simply:
function Count([int]$times)
{
1..$times
}