Saturday, August 2, 2008

Returning values from Fuctions in PowerShell... Tricky :(

First let me explain something: I LOVE POWERSHELL...

No, I really do. After 3 years working every day with batches, vbscript & .NET I can see huge huge potential in PowerShell and more and more I use it I think it is genius shell.

However I am always trying to be skeptical about everything I do - and about every product I love.

I don't want to end up like some people that think their favorite product is PERFECT - and when there is new version, they suddenly realized that something that was PERFECT is more PERFECT ;)

 

I worked with Monad\PowerShell a lot 3 years ago, then I stopped and returned to batches (because of my work). It was quite fun and it turned out that with good imagination you can still achieve a lot with batches (script blocks, complex scopes, private variables, subroutines, functions etc ;)), however it was always just about building some workarounds. 1 month ago I returned to PowerShell and now I am working with it on daily basis - and of course I already encountered some things I don't really like. 

Working with function output

If you are familiar with programming, following code should be easy to understand for you:

Function bar {
$MyVariable = "Foo"
Return $MyVariable
}



Function bar returns MyVariable. So using $X = bar should be equivalent to $X = $MyVariable in fact.



Not in PowerShell. $X will get output from WHOLE function. That means that following function is equivalent to one above:



Function bar {
$MyVariable = "Foo"
$MyVariable
Return
}



I really, really don't like this behavior. Creating complex scripts is getting much more complicated. Consider example where you run into some problems in bar function. Most primitive debugging is always to use Echo (Write-Host) and just see value itself.



If I would have enough time, I would change it as follows:



Function bar {
$MyVariable = "Foo"
Write-Host "DEBUG: $MyVariable"
Return $MyVariable
}



Result would be as expected - if I will use $X = bar, $X will be Foo. However what if I try following:



Function bar {
$MyVariable = "Foo"
"DEBUG: $MyVariable"
Return $MyVariable
}


Looks fine? But doesn't work - $X in this case is Foo Foo and you will never see "DEBUG: Foo" output in console. I really think this is against object-oriented principle of PowerShell and it reminds me of 'For /f' behavior in batches.



You probably won't run into this problem - however you will run into it if you will start creating really complex scripts with XML handling, adding new elements etc. Usually (in .NET), if you for example add something to array, index is returned.



Look at following function. Variable $MyVariable is created, two numbers are added (1 and 2) and then it is returned:



Function bar {
[System.Collections.ArrayList]$MyVariable = @()
$MyVariable.Add("a")
$MyVariable.Add("b")
Return $MyVariable
}



Normally you would expect to get array that contains 'a' and 'b'. But because of PowerShell way of returning object, instead you will get array with 4 (!) entries: 0 1 a b.



0 and 1 in this case is index of added array elements. 



To make this function work as expected, you will need to redirect output to null:



Function bar {
[System.Collections.ArrayList]$MyVariable = @()
$MyVariable.Add("a") | Out-Null
$MyVariable.Add("b") | Out-Null
Return $MyVariable
}



Usually I try to write all my functions in following format:



Function <Name> {
<initial checks>
...
<final checks before return>
Return
}



With PowerShell this is usually not possible, because even if you check if $MyVariable is correct type with correct values etc, it is quite hard to know if something "leaked" before. 



For me ideal solution would be that PowerShell would support BOTH methods for returning:



Function bar {
$MyVariable = "Foo"
Return $MyVariable
}



Would return object $MyVariable



Function bar {
$MyVariable = "Foo"
$MyVariable
Return
}


Would return whole output from function



13 comments:

Anonymous said...

Yes, Powershell is different. But I think the positive effects of a function returning "everything" is visible when you start using pipelines. I don't think it's bad, it's just different.

BTW, wour sample function bar with the "debug" string in it, does not return "foo" only, but two strings, "foo" and your debug output.

Your ArrayList sample is somewhat weird. Why should I use an ArrayList instead of Powershell's builtin arrays?

Martin Zugec said...

Heya,

well, I already run into few situations where it was nice to have this behavior - however I also run into many others where it was driving me crazy :(

That is why I said best would be if there could be difference between

Return $Object

and

$Object
Return

Then PS could support both methods easily.


Regarding debug, that's what I wanted to point out... Using "Hello world" and Write-Host "Hello world" have completely different behavior here (and you don't expect this).

Well, obviously builtin array is function-proof, that is why I tried next available collection type that came to my mind ;)
However you will run into similar issues once you start to use .NET instead of built-in PS.

For example code that is using .NET method .Add() is returning reference to index, BUT following code doesn't return it:

Function bar {
[System.Collections.ArrayList]$MyVariable = @();
$MyVariable += "a";
$MyVariable += "b";
Return $MyVariable;
}

Anonymous said...

Obviously I'm stupid, but for me it sounds like "Name redefined". Where did I miss the point?

Anonymous said...

In my opinion, as someone familiar with many languages, but new to Powershell, is that the Return keyword should have been omitted from the language, as it works so differently from every other language I've ever worked with.

Return "foo" should just return "foo", not everything that has been 'output' from the function.

Anonymous said...

What is really missing in this port is what everyone is looking for. How to get more than one thing out of a function. Well I am going to share what everyone wants to know who has searched and found this hoping it will answer the question.

function My-Function([string]$IfYouWant)
{
[hashtable]$Return = @{}

$Return.Success = $False
$Return.date = get-date
$Return.Computer = Get-Host

Return $Return
}
#End Function

$GetItOut = My-Function
Write-host “The Process was $($GetItOut.Success) on the date $($GetItOut.date) on the host $($GetItOut.Computer)”

#You could then do
$var1 = $GetItOut.Success
$Var2 =$GetItOut.date
$Var3 = $GetItOut.Computer

If ($var1 –like “True”){write-host “Its True, Its True”}

Enjoy
Peter S
Australia

Martin Zugec said...

Hi Peter,

http://martinzugec.blogspot.sk/2009/08/how-to-return-multiple-values-from.html

Obviously the same way of thinking :)

Martin

Anonymous said...

Wow. This just solved 3 hrs of painful investigation. I have a function to open a MySQL connection and it worked in a very simple test script. As I was assembling a 'real' version which was part of a much bigger script, I had calls to a DBUG function that would put messages to standard out (in a nice, tabbed, timestamped format). This made the db connection function NOT function until the RETURN variable was used in another manner (in my case, used in another call to the DBUG function).

Once I changed my DBUG function to use write-host instead of standard out, it worked fine. But lots of head scratching/beating-against-wall.

Anonymous said...

Hey,
Generally when I want a predictable return code from a function I do this:

function test {
Write-Output "Status Message"
return $False
}

$exitCode = $(test)[-1]
$exitCode

This seems to work well under Powershell v3 and v5. Not sure what backwards compatibility is like.

Anonymous said...

Great blog!
Run also into this problem and your blog helped me fix it. Thanks!

Greets, Jeroen Bleeker

Anonymous said...

This is genius, just saved me a lot of head scratching with an array that was not returning from a function as expected. Thanks very much!

Anonymous said...

Function bar {
[System.Collections.ArrayList]$MyVariable = @()
$MyVariable.Add("a") | Out-Null
$MyVariable.Add("b") | Out-Null
Return $MyVariable
}
$Z = bar
$z.GetType()
Name BaseType
---- --------
Object[] System.Array

Stop! We specify [System.Collections.ArrayList] class!!!
Let`s try "," before variable return:

Function bar {
[System.Collections.ArrayList]$MyVariable = @()
$MyVariable.Add("a") | Out-Null
$MyVariable.Add("b") | Out-Null
Return ,$MyVariable
}
$Z = bar
$z.GetType()
Name BaseType
---- --------
ArrayList System.Object

Martin Zugec said...

Yes, coma before returned object is exactly what I'm doing now. 10 years ago (when this blog was written), I didn't know that trick. Guess I should update the blog post.

aenagy said...

I just tried the comma trick:

return ,$myVar

and it returns the boolean and not $myVar.

For reference:
PS > $PSVersionTable

Name Value
---- -----
PSVersion 5.1.14393.3866
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.14393.3866
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1