1

Can someone please explain how/why this code:

function DoIt($one, $two) 
{
    "`$one is $one"
    "`$two is $two"
    return $one * $two
}
$sum = DoIt 5 4
$sum

works exactly as expected/intended - i.e., it produces the following output:

$one is 5
$two is 4
20

However, this (seemingly almost identical) code:

function DoIt($one, $two) 
{
    "`$one is $one"
    "`$two is $two"
    return $one * $two
}
$sum = DoIt 5 4
"`$sum is $sum"

bends my brain and breaks my understanding of reality by producing the following output:

$sum is $one is 5 $two is 4 20
Rich Bayless
  • 143
  • 1
  • 7

4 Answers4

2

The reason for the odd behavior is that you are polluting the output stream. PowerShell doesn't explicitly have a pure "return" value like a compiled program, and relies on streams to pass data between functions. See about_Return for more information, as well as an example that illustrates this very behavior.

Basically the $sum variable gets all the output of the function, and as @TheMadTechnician says, it outputs differently depending on how it is being used.

The "correct" way in PowerShell to return values is in general not to use return but to use Write-Output to explicitly define which stream you want to output to. The second thing is that you have to use Write-Host when writing messages to the Host console, otherwise it gets returned on the Output stream as well.

function DoIt($one, $two) 
{
    Write-Host "`$one is $one"
    Write-Host "`$two is $two"
    Write-Output ($one * $two)
}
$sum = DoIt 5 4
"`$sum is $sum"

$one is 5
$two is 4
$sum is 20
HAL9256
  • 12,384
  • 1
  • 34
  • 46
  • Thanks @HAL9256 - that helped! However, exactly as you wrote it, I got back "$sum is 5 * 4". It seems the mathematical computation didn't actually take place within a Write-Output command (as I too would have expected to happen). But, this could be easily fixed by splitting that one line of code into two lines: `$retval = $one * $two` and `Write-Output $retval` – Rich Bayless Apr 04 '19 at 19:55
  • Argh, @RichBayless yeah you're right. Simply wrapping some brackets around the `($one * $two)` causes it to evaluate the expression rather than output the raw string output. I have edited the Answer to correct it. Funny thing is is that I had copied the expression off an earlier test I had, where instead of `Write-Output` I had `return $one * $two` without brackets, and it worked ;-) Ah, all those testing edge cases! – HAL9256 Apr 04 '19 at 20:29
  • All output, unless otherwise specified, is sent to StdOut, so `return` and `write-output` are redundant. Rather than using `Write-Host`, I would recommend using `[cmdletbinding()]` and `Write-Verbose`. – TheMadTechnician Apr 04 '19 at 21:25
1

This is due to how you are outputting an array, and has nothing to do with the function. You can replicate the same behavior with the following:

$sum='$one is 5','$two is 4',20
"`$sum is $sum"

Putting the variable in double quotes performs string expansion, which performs the .ToString() method on each item in the array, and then joins them with a space to convert the array into a string.

TheMadTechnician
  • 34,906
  • 3
  • 42
  • 56
  • 1
    Nitpicking ;-) `and then joins them with a space` as default or the content of the variable `$OFS`. Cite from `Get-Help about_automatic_Variables`: `$OFS is a special variable that stores a string that you want to use as an output field separator.` (+1) –  Apr 04 '19 at 20:14
1

There's good information in the existing answers, but let me try to bring it all together:

function DoIt($one, $two) 
{
    "`$one is $one"
    "`$two is $two"
    return $one * $two
}

produces three outputs ("return values") to PowerShell's success [output] stream (see about_Redirection), which are the evaluation results of:

  • expandable string "`$one is $one"
  • expandable string "`$two is $two"
  • expression $one * $two

The expandable strings are implicitly output - due to producing a result that is neither captured, sent to another command, nor redirected .

Similarly, the return in return $one * $two is just syntactic sugar for:

$one * $two # implicit output
return      # control flow: exit the scope

Note:

  • return is unnecessary here and never required.

  • While Write-Output could be used in lieu of implicit output:

    • it is only ever helpful if you want output a collection as a single object, via its -NoEnumerate switch.
    • it is otherwise not only needlessly verbose, it slows things down.
  • If you want to print status information from a function without polluting the output stream, you have several choices:

    • Write-Host prints to the host's display (the console, if you're running in a console window); in PSv4-, such output could neither be captured nor suppressed; in PSv5+, Write-Host now writes to the information stream (number 6), which means that 6> can be used to redirect/suppress the output.
    • Write-Verbose is for opt-in verbose output, which you can activate by setting preference variable $VerbosePreference to 'Continue' or by using the -Verbose switch.
    • Write-Debug is for opt-in debugging output via $DebugPreference or -Debug, though note that in the latter case a prompt is displayed whenever a Write-Debug statement is encountered.

Because these outputs are captured in a variable - $sum = DoIt 5 4 - and there is more than 1 output, they are implicitly collected in an array (of type [object[]]).

Using implicit output to print this variable's value to the display - $sum - enumerates the array elements, which prints them each on their own line:

$one is 5
$two is 4
20

By contrast, using implicit output with expandable string "`$sum is $sum" results in a single output string in which the reference to variable $sum, which contains an array, is expanded as follows:

  • each element of the array is stringified

    • loosely speaking, this is like calling .ToString() on each element, but it's important to note that PowerShell opts for a culture-invariant string representation, if available - see this answer.
  • the results are joined with the value of (the rarely used automatic variable) $OFS as the separator to form a single string; $OFS defaults to a single space.

Thus, if you join array elements $one is 5, $two is 4, and 20 with a single space between them, you get:

$one is 5 $two is 4 20

To put it differently: the above is the equivalent of executing '$one is 5', '$two is 4', '20' -join ' '

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

After playing around more (using the info I learned from @HAL9256), I discovered the problem isn't actually with using return vs Write-Output but rather with just how command-less strings are handled in functions. For example, this too works perfectly:

function DoIt($one, $two) 
{
    Write-Host "`$one is $one"
    Write-Host "`$two is $two"
    return ($one * $two)
}
$sum = DoIt 5 4
Write-Host "`$sum is $sum"

that is, it produces the following expected output:

$one is 5
$two is 4
$sum is 20

(And, apparently, the mathematical computation without explicit parentheses works just fine within a return command - unlike within a Write-Output command.)

In other words, I guess a static string in a function is treated as if you're building an array to be returned (as @TheMadTechnician was referring) as opposed to shorthand for a Write-Host command. Live and learn - "explicit is better than implicit". :-)

Granted, I still don't understand why the final output wasn't exactly the same (right or wrong) between the two code blocks in my original question ... but, hey, my brain is tired enough for one day. :-P

thanks again guys!!!

Rich Bayless
  • 143
  • 1
  • 7
  • @HAL9256 pointed out my other mistake ... that I'd forgotten to put parentheses around my mathematical computation ... oddly, `return` doesn't care but `Write-Output` did ... I'll update my sample code in this "answer" above accordingly ... to make it better. – Rich Bayless Apr 04 '19 at 20:35
  • I believe the difference is because `$sum` on it's own at the prompt will evaluate like a array object, where the prompt will iterate through the array and output the `ToString()` each on a new line, vs., when wrapped inside quotes, it iterates through the array and adds the `ToString()` to the existing string (e.g. substitution). Since there are no newlines inside the strings, the resulting string output is all on the same line. – HAL9256 Apr 04 '19 at 20:40
  • Good call @HAL9256 - yep, I believe you're correct. FYI: I get the same wacky results (in my original question's code) if I were to use `Write-Host $sum` (instead of just `$sum`) ... since the implicit functionality of just specifying the variable is indeed different than an explicit `Write-Host` command. – Rich Bayless Apr 04 '19 at 20:50