1

The function Select-WriteHost from an answer to another Stackoverflow question (see code below) will redirect/capture Write-Host output:

Example:

PS> $test = 'a','b','c' |%{ Write-Host $_ } | Select-WriteHost
a
b
c

PS> $test
a
b
c

However, if I add -NoNewLine to Write-Host, Select-WriteHost will ignore it:

PS> $test = 'a','b','c' |%{ Write-Host -NoNewLine $_ } | Select-WriteHost
abc

PS> $test
a
b
c

Can anyone figure out how to modify Select-WriteHost (code below) to also support -NoNewLine?

function Select-WriteHost
{
   [CmdletBinding(DefaultParameterSetName = 'FromPipeline')]
   param(
     [Parameter(ValueFromPipeline = $true, ParameterSetName = 'FromPipeline')]
     [object] $InputObject,

     [Parameter(Mandatory = $true, ParameterSetName = 'FromScriptblock', Position = 0)]
     [ScriptBlock] $ScriptBlock,

     [switch] $Quiet
   )

   begin
   {
     function Cleanup
     {
       # Clear out our proxy version of write-host
       remove-item function:\write-host -ea 0
     }

     function ReplaceWriteHost([switch] $Quiet, [string] $Scope)
     {
         # Create a proxy for write-host
         $metaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Microsoft.PowerShell.Utility\Write-Host')
         $proxy = [System.Management.Automation.ProxyCommand]::create($metaData)

         # Change its behavior
         $content = if($quiet)
                    {
                       # In quiet mode, whack the entire function body,
                       # simply pass input directly to the pipeline
                       $proxy -replace '(?s)\bbegin\b.+', '$Object'
                    }
                    else
                    {
                       # In noisy mode, pass input to the pipeline, but allow
                       # real Write-Host to process as well
                       $proxy -replace '(\$steppablePipeline\.Process)', '$Object; $1'
                    }

         # Load our version into the specified scope
         Invoke-Expression "function ${scope}:Write-Host { $content }"
     }

     Cleanup

     # If we are running at the end of a pipeline, we need
     #    to immediately inject our version into global
     #    scope, so that everybody else in the pipeline
     #    uses it. This works great, but it is dangerous
     #    if we don't clean up properly.
     if($pscmdlet.ParameterSetName -eq 'FromPipeline')
     {
        ReplaceWriteHost -Quiet:$quiet -Scope 'global'
     }
   }

   process
   {
      # If a scriptblock was passed to us, then we can declare
      #   our version as local scope and let the runtime take
      #   it out of scope for us. It is much safer, but it
      #   won't work in the pipeline scenario.
      #
      #   The scriptblock will inherit our version automatically
      #   as it's in a child scope.
      if($pscmdlet.ParameterSetName -eq 'FromScriptBlock')
      {
        . ReplaceWriteHost -Quiet:$quiet -Scope 'local'
        & $scriptblock
      }
      else
      {
         # In a pipeline scenario, just pass input along
         $InputObject
      }
   }

   end
   {
      Cleanup
   }
}

PS: I tried inserting -NoNewLine to the line below (just to see how it would react) however, its producing the exception, "Missing function body in function declaration"

Invoke-Expression "function ${scope}:Write-Host { $content }"

to:

Invoke-Expression "function ${scope}:Write-Host -NoNewLine { $content }"
MKANET
  • 573
  • 6
  • 27
  • 51
  • Thanks.. I don't need to use PowerShell <5. However I am looking for solution via Select-WriteHost....since that's the function I need to use for redirecting all other write-host cases. – MKANET Mar 02 '22 at 02:42
  • No it doesn't. `$test = 'a','b','c' |%{ Write-Host $_ -NoNewline} 6>&1` will not make $test = "abc". The whole point is I was looking for a way to capture the text correctly when NoNewLine is used. – MKANET Mar 02 '22 at 07:17

1 Answers1

1
  • (Just to recap) Write-Host is meant for host, i.e. display / console output only, and originally couldn't be captured (in-session) at all. In PowerShell 5, the ability to capture Write-Host output was introduced via the information stream, whose number is 6, enabling techniques such as redirection 6>&1 in order to merge Write-Host output into the success (output) stream (whose number is 1), where it can be captured as usual.

  • However, due to your desire to use the -NoNewLine switch across several calls, 6>&1 by itself is not enough, because the concept of not emitting a newline only applies to display output, not to distinct objects in the pipeline.

    • E.g., in the following call -NoNewLine is effectively ignored, because there are multiple Write-Host calls producing multiple output objects (strings) that are captured separately:

      • 'a','b','c' | % { Write-Host $_ -NoNewline } 6>&1
    • Your Select-WriteHost function - necessary in PowerShell 4 and below only - would have the same problem if you adapted it to support the -NoNewLine switch.

  • An aside re 6>&1: The strings that Write-Host invariably outputs are wrapped in [System.Management.Automation.InformationRecord] instances, due to being re-routed via the information stream. In display output you will not notice the difference, but to get the actual string you need to access the .MessageData.Message property or simply call .ToString().


There is no general solution I am aware of, but situationally the following may work:

  • If you know that the code of interest uses only Write-Host -NoNewLine calls:

Simply join the resulting strings after the fact without a separator to emulate -NoNewLine behavior:

# -> 'abc'
# Note: Whether or not you use -NoNewLine here makes no difference.
-join ('a','b','c' | % { Write-Host -NoNewLine $_ })
  • If you know that all instances of Write-Host -NoNewLine calls apply only to their respective pipeline input, you can write a simplified proxy function that collects all input up front and performs separator-less concatenation of the stringified objects:
# -> 'abc'
$test = & {

  # Simplified proxy function
  function Write-Host {
    param([switch] $NoNewLine)
    if ($MyInvocation.ExpectingInput) { $allInput = $Input } 
    else                              { $allInput = $args }
    if ($NoNewLine) { -join $allInput.ForEach({ "$_" }) }
    else            { $allInput.ForEach({ "$_" })  }
  }

  # Important: pipe all input directly.
  'a','b','c' | Write-Host -NoNewLine

}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks for the answer. I am just looking for a replacement for `Select-WriteHost`; except with the ability to also handle `-nonewline`. When I tried to use your Write-Host (renamed to Select-WriteHost2), $test doesn't have a value. `$test = 'a','b','c' |%{ Write-Host -NoNewLine $_ } | Select-WriteHost2` – MKANET Mar 02 '22 at 18:26
  • PS: I tried to integrate your proxy code into the original Select-WriteHost code I posted.. however, can't seem to get it to work. – MKANET Mar 02 '22 at 19:08
  • @MKANET, my solution is designed to _override_ `Write-Host`, so renaming it won't work. As stated, I don't think there's a solution for _multiple_ `Write-Host -NoNewLine` calls - only for a _single_ one to which all inputs are piped _directly_. If you can ensure that, you can rewrite `Select-WriteHost` to do what the `Write-Host` function in this answer does, i.e. you need to _collect all input objects first_ and act on the resulting collection. – mklement0 Mar 02 '22 at 21:25
  • 1
    thank you for taking the time to provide an answer. It really isn't a solution to my question. However, at least you confirmed that it's not possible to do what I want. – MKANET Mar 04 '22 at 22:04