2

I would like to write to console [host] as well as send the resolved value to the output channels under all circumstances

Desired Result:

==== FOOBAR ====
Input: <input>

<output>
==== END OF FOOBAR ====

The closest I've gotten to a solution is the following:

function Write-OutAndReturn {

    param(
        [Parameter(ValueFromPipeline = $true)]
        $Value
    )

    process {
        $ConsoleDevice = if ($IsWindows) { '\\.\CON' } else { '/dev/tty' }
        
        Write-Host "====== SECTION ======"

        Write-Host "Input: " -NoNewline
        Write-Host $MyInvocation.BoundParameters.Value

        $(if ($Value -is [scriptblock]) {
            $Value.Invoke()
        } else { $Value }) | Tee-Object -FilePath $ConsoleDevice

        Write-Host "====== END SECTION ======"
    }

}

Where the output of $Result = Write-OutAndReturn { 1 + 1 } equals:

====== SECTION ======
Input:  1 + 1
2
====== END SECTION ======

Which is desired; and $Result now contains 2

However when the function is only called (Write-OutAndReturn { 1 + 1 }) it generates the following:

====== SECTION ======
Input:  1 + 1
2
2
====== END SECTION ======

Where the second 2 is undesired

Is there a more semantic way to handle this behavior or to ensure brevity is honored?

soulshined
  • 9,612
  • 5
  • 44
  • 79
  • 1
    As an aside: Using the `.Invoke()` method to execute a script block in PowerShell code is best avoided, because it changes the semantics of the call in several respects. Use `& $scriptBlock [arg1 ...]` instead. See [this answer](https://stackoverflow.com/a/59220711/45375) for more information. – mklement0 Jun 04 '23 at 17:13

2 Answers2

1

In your example there are 3 possible outputs:

everything on the pipeline can be passed to another cmdlet or captured by assigning it to a variable (as: $Result = Write-OutAndReturn { 1 + 1 })

Write-Host ────────> Console <═╕<─┐
                ┌──> File      │  │
Tee-Object ═════╡    (or CON) ─┘  │ not processed
                │                 │
(Write-Output) ─┴──> Pipeline ────┘

The confusion comes from the Write-Output (or implicit PowerShell output) feature:

If Write-Output is the last command in the pipeline, the objects are displayed in the console.

Meaning, everything on the pipeline that is not captured in an variable or passed on to another cmdlet, will act similar as if you did a write-host. To capture everything that is currently written to the console, you will need to remove the write-host cmdlet (or replace them with Write-Output).

In your example, if $IsWindows is true, Tee-Object will output to the console twice:

  • explicit for the -Filepath parameter '\\.\CON'
  • and implicit by design (as in the drawing)
iRon
  • 20,463
  • 10
  • 53
  • 79
  • Thanks for the quick reply iRon. Maybe I'm misunderstanding, based on what I read in the docs, Tee-Object `stores the output in a file or variable and also sends it down the pipeline`; does this not make it explicit and negating the 'last statement' trigger? The example desired output is over simplified, there will eventually be other control flow for logging information beyond this. Swapping in Output only isn't desired. `$Result` doesn't need that information. Ideally, echoing will *always* happen, but using the resolved value may not – soulshined Jun 03 '23 at 09:36
  • 3
    @soulshined - any *uncaptured* output will be returned to the parent scope in the pipeline, and if *that* doesn’t capture it it will be passed up to *that* scope’s parent, etc. If it reaches the top-level scope and is not captured it will be piped to ```Out-Default``` which normally renders onto the console. If you don’t want the output stream to be cascaded upwards you need to capture it somehow. Some options are ```$null = …``` and ```… | Out-Null```. If you want to suppress pipeline output to the console from a top-level call to ```Write-OutAndReturn``` you’ll need to capture it *somehow*. – mclayton Jun 03 '23 at 09:44
  • Okay got ya! That makes sense thanks. I suppose theres no way to conditionally check for that though, within a function? At least nothing I can quickly find. It wouldn't be a unilateral `Out-Null`, just if it's uncaputered. And just to clarify i don't want to supress any console echos from within the function, I always want it to echo those statements, but I may not always want to use the Output variable – soulshined Jun 03 '23 at 09:49
  • 1
    @iron - as a test I just tried ```$null = 1..100 | write-output``` and got nothing written to the console, so the docs saying “If Write-Output is the last command in the pipeline, the objects are displayed in the console.” is maybe just badly worded or I’m reading it out of context, but it’s not true as an unqualified stand-alone statement… – mclayton Jun 03 '23 at 09:58
  • 1
    @soulshined - a function has no way of knowing whether its output is going to be captured or not by the caller. If the function is known to send pipeline output and the caller wants to prevent it bubbling up then that’s the caller’s own job to handle by capturing the output… – mclayton Jun 03 '23 at 10:02
  • 1
    @mclayton, you're right. I am just quoting the [`Write-Output`](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/write-output) document here: *`Writes the specified objects to the pipeline. If Write-Output is the last command in the pipeline, the objects are displayed in the console.`* (when I can find some time, I will feed this back) – iRon Jun 03 '23 at 10:02
  • I suppose what I'm asking to do is to effectively emulate `-PassThru` except always log the write-host statements right? But effectively this isn't possible in this context as I understand your feedback? There's just no way to `Write-Output $Value` (implicitly or not) just once based on if it's captured or not – soulshined Jun 03 '23 at 10:07
  • @soulshined - let’s assume you find a way to determine whether the output is captured or not at the call site. The problem *now* is, how do you know the callers intent *isnt* to deliberately leave it uncaptured because it *wants* to bubble the output up to the parent the pipeline? You might suppress your function’s output when the caller really wanted it… – mclayton Jun 03 '23 at 10:21
  • fair point @mclayton probably not a future proof solution. I think what I'll end up doing is just add a `-PassThru` switch and let the caller delegate. Not the end of the world just wanted it to be an 'all-in-one' request. Thanks so much for your guidance guys! – soulshined Jun 03 '23 at 10:24
1

Instead of

$(if ($Value -is [scriptblock]) {
            $Value.Invoke()
        } else { $Value }) | Tee-Object -FilePath $ConsoleDevice

You could try

$result = if ($Value -is [scriptblock]) { $Value.Invoke() } else { $Value }
Write-Host $result
$result

which is basically a poor-man’s Tee-Object that sends the value to the host and the output stream.

It means you might see the $result on the console a second time from the output stream if you don’t capture the return value from Write-OutAndReturn, but that’s pretty much what you’d expect from uncaptured values that reach the root (script) scope…

To address that you could potentially make the $result line optional (and “off” by default) and add a -PassThru switch for when the caller really does want the value sent to the output stream…

$result = if ($Value -is [scriptblock]) { $Value.Invoke() } else { $Value }
Write-Host $result
if( $PassThru ) { $result }
mclayton
  • 8,025
  • 2
  • 21
  • 26