Nico Nekoru
Nico Nekoru

Reputation: 3112

Why are (return) and return different?

I was testing a function and was playing around with what I could do with return and stumbled accross a strange issue, in PowerShell 5.1 and PwSh 7.1, the return cmdlet seemd to not work in a group:

PS C:\Users\Neko> return "test"
test
PS C:\Users\Neko> (return "test")
return : The term 'return' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct
and try again.
At line:1 char:2
+ (return "test")
+  ~~~~~~
    + CategoryInfo          : ObjectNotFound: (return:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

If I put return in a group, it seemed to become unrecognized by PowerShell.

I understand that return prints to the console and does not have "output" per say like Write-Host,

PS C:\Users\Neko> $test = return "test"
test
PS C:\Users\Neko> $test
PS C:\Users\Neko> $test = Write-Host "test"
test
PS C:\Users\Neko> $test
PS C:\Users\Neko>

but I can't fathom why having return in a grouping expression is causing such an issue.


Why is using return in groups causing a strange error and how can I remedy that?

This doesn't occur in subexpressions,

PS C:\Users\Neko> $(return "test")
test

Upvotes: 2

Views: 231

Answers (3)

mklement0
mklement0

Reputation: 438153

return is a statement, whereas only commands and expressions are allowed inside (), the grouping operator.

  • All keyword-based constructs are statements; to name a few: other flow-control statements (exit, throw, ...) and loops and conditionals (if, while, switch, ...) - see about_Language_Keywords for the list of all keywords in PowerShell.

In order to group (embed in another statement) a statement (potentially even multiple ones, separated with ;, as usual) - you need $(), the subexpression operator.

  • Note that the operator's name is somewhat unfortunate; "sub-statement operator" would have been more descriptive.

  • The primary use of $(...) is inside expandable strings ("..."), and it is typically not needed elsewhere; instead, (...) should be used when possible, so as to avoid potential side effects (and prevent visual clutter) - see this answer.

That said, in this specific case, as the about_Return help topic states, return's purpose is to exit the enclosing scope (function, script, or script block), so using it as the RHS of a variable assignment never makes sense.


The reason for the can't-use-a-statement-as-an-expression restriction is rooted in the fundamentals of PowerShell's grammar, as summarized by a member of the PowerShell team in this GitHub issue (emphasis added):

PowerShell has pipelines contained by statements, not statements contained by pipelines. That's always been the case, and the way to put a statement within a pipeline is to use the $(...) syntax — that's its only purpose to exist outside of strings in the language.

The two most important consequences to end users are:

  • You cannot use a statement in a pipeline:

    • That is, something like foreach ($i in 1..3) { $i } | % { $_ + 1 } does not work.[1]
    • While you could use $(...) as a workaround($(foreach ($i in 1..3) { $i }) | % { $_ + 1 }, doing so forgoes the streaming benefits of the pipeline; that is, the output from the foreach loop is collected in memory first, in full before the results are sent through the pipeline.
    • In order to get the usual streaming behavior (one-by-one passing of output through the pipeline), enclose the loop in a script block ({ ... }) and invoke it with &, the call operator[2]:
      • & { foreach ($i in 1..3) { $i } } | % { $_ + 1 }
    • (Using expressions in a pipeline works, just fine, however: 1..3 | % { $_ + 1 })
  • You cannot use the flow-control statements return, exit, and throw with && and ||, the pipeline-chain operators:

    • That is, something like ls nosuchfile || exit 1 does not work.
    • Instead, you must use $(...): ls nosuchfile || $(exit 1)

Unfortunately, these restrictions cannot be lifted if backward compatibility must be maintained; quoting again from the previously linked GitHub issue:

[Lifting these restrictions amounts to] a huge change in PowerShell's grammar (effectively creating a fork in the language)

  • While PowerShell to date (as of v7.0) has had an ironclad commitment to backward compatibility which prevented serious breaking changes, a debate about whether to permit such changed and how to (carefully) manage them has now begun: see this GitHub issue.

  • If such changes are eventually permitted, many longstanding annoyances could finally be tackled - see this GitHub issue.


[1] Note that foreach is also a built-in alias for the ForEach-Object cmdlet, but the command shown uses the foreach loop statement - see about_ForEach.

[2] If the statement has side effects that you want the caller to see, use ., the dot-sourcing operator instead of &.

Upvotes: 2

mklement0
mklement0

Reputation: 438153

Note: This answer complements my (already lengthy) other one in order to address some of your analysis in the question:

I understand that return prints to the console and does not have "output" per say like Write-Host

It is Write-Host that has no output in the data sense: that is, it does not write to the success output stream (stream number 1 - see about_Redirection)

Instead, it writes to the host (the display; in PSv5+, it does so via stream 6, the information stream), bypassing the success output stream, preventing capturing or redirection of the output - see this answer for background information, including when use of Write-Host (rather than Write-Output / implicit output) is appropriate.

By contrast, return does send the output from its (optional) operand[1] to the success output stream.

As for the surprising behavior of:

PS> $test = return "test"
test

You've hit an edge case (discussed in this since-closed GitHub issue): something that is syntactically allowed, but doesn't make sense to do: the return statement effectively short-circuits the assignment: that is, "test" is sent directly to the success output stream (1), and the assignment is ignored, because the flow-control statement return exits the enclosing scope - not just the statement! - before the assignment occurs.

You can verify that return indeed still targets the success output stream as follows:

PS> $testOuter = & { $test = return "test" }; "[$testOuter]"
[test]   # proof that $testOuter was able to capture the `return` value.

However, it's easy to avoid this problem: any expression or command (pipeline) can directly be used as the RHS.

# Do NOT use `return` in the RHS of an assignment.
PS> $test = "test"; "[$test]"
[test]

[1] While using return with an expression (such as return $foo) is probably most common, note that it supports an entire pipeline as its operand (e.g., return Get-Date | % Year).

Upvotes: 1

postanote
postanote

Reputation: 16096

@Neko Musume -- as per your request.

Because as per the MS Docs on the topic ...

About_Return | MS Docs

... it can only be used as defined. Meaning 'Exit the current scope, which can be a function, script, or script block.', thus using parens, as you are trying to do, does not meet that criteria.

Upvotes: 2

Related Questions