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 }"
2
  • 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. Commented Mar 2, 2022 at 2: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. Commented Mar 2, 2022 at 7:17

1 Answer 1

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

}
Sign up to request clarification or add additional context in comments.

4 Comments

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
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, 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.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.