Powershell Forms Texbox Multine Output Issue - powershell

Racking my brains here, and i'm sure it's something simple.
I've built this simple form app that runs an exe in cmd.
The problem is when i click the build iso button the function runs but only outputs the first line of the .exe and the rest of the output is passed to the terminal and not in the textbox?.
It's almost as if the textbox won't accept more than one line, even though i have multiline set to $true?
The yellow text in terminal should be in the textbox as well:
Code:
Function xiso_build {
Set-Location -Path $PSScriptRoot # change to root folder of this script wherever it's run from
$outputbox.text = & .\extract-xiso.exe -r $selected_file | out-string
}
# Output of xtract-iso textbox
$global:outputBox = New-Object System.Windows.Forms.TextBox #creating the text box
$outputBox.Location = '10,150' #location of the text box (px) in relation to the primary window's edges (length, height)
$outputBox.Size = New-Object System.Drawing.Size(565,200) #the size in px of the text box (length, height)
$outputBox.MultiLine = $True #declaring the text box as multi-line
$outputBox.ScrollBars = "Vertical" #adding scroll bars if required
$form.Controls.Add($outputBox) #activating the text box inside the primary window
# Build Iso Button
$build_button = New-Object System.Windows.Forms.button
$build_button.Text = 'Build ISO'
$build_button.Size = '200,50'
$build_button.Location = '10,360'
# $button.Anchor = 'Bottom,left' # uncomment to move button down to bottom left of app window
$form.Controls.Add($build_button)
$build_button.Add_Click({xiso_build }) # run 'xiso_build' func from above
I'm at a loss here as to have all output in textbox. Thanks all

Theo has provided the crucial pointer: In order to capture all output from an external program, you must also capture its stderr output - by default, only stdout output is captured by PowerShell.
To that end:
use redirection 2>&1 to merge stderr output into the success output stream.
convert all input lines to strings with ForEach-Object ToString (which calls the .ToString() method on each line).
This is necessary, because PowerShell wraps stderr lines in System.Management.Automation.ErrorRecord instances, which Out-String renders as if they were PowerShell errors. Strictly speaking, you only need this in Windows PowerShell; in PowerShell (Core) 7+, even though the wrapping still happens, rendering is by the wrapped line only.
then pipe to Out-String to form a single, multi-line string from all input lines.
$outputbox.text = & .\extract-xiso.exe -r $selected_file 2>&1 |
ForEach-Object ToString | # no longer needed in PS 7+
Out-String
A more concise alternative with a [string[]] cast and the -join operator:
# [string[]] is no longer needed in PS 7+
$outputbox.text =
[string[]] (& .\extract-xiso.exe -r $selected_file 2>&1) -join "`n"
Again, the [string[]] cast isn't needed in PowerShell (Core) 7+ anymore (analogous to how ForEach-Object ToString isn't needed anymore above).
The above doesn't add a trailing newline to the result (though you could achieve that with (... -join "`n") + "`n"), whereas Out-String invariably does.[1]
[1] Arguably, Out-String should not do that; see GitHub issue #14444 for a discussion.

Related

PowerShell Extract text between two strings with -Tail and -Wait

I have a text file with a large number of log messages.
I want to extract the messages between two string patterns. I want the extracted message to appear as it is in the text file.
I tried the following methods. It works, but doesn't support Get-Content's -Wait and -Tail options. Also, the extracted results are displayed in one line, but not like the text file. Inputs are welcome :-)
Sample Code
function GetTextBetweenTwoStrings($startPattern, $endPattern, $filePath){
# Get content from the input file
$fileContent = Get-Content $filePath
# Regular expression (Regex) of the given start and end patterns
$pattern = "$startPattern(.*?)$endPattern"
# Perform the Regex opperation
$result = [regex]::Match($fileContent,$pattern).Value
# Finally return the result to the caller
return $result
}
# Clear the screen
Clear-Host
$input = "THE-LOG-FILE.log"
$startPattern = 'START-OF-PATTERN'
$endPattern = 'END-OF-PATTERN'
# Call the function
GetTextBetweenTwoStrings -startPattern $startPattern -endPattern $endPattern -filePath $input
Improved script based on Theo's answer.
The following points need to be improved:
The beginning and end of the output is somehow trimmed despite I adjusted the buffer size in the script.
How to wrap each matched result into START and END string?
Still I could not figure out how to use the -Wait and -Tail options
Updated Script
# Clear the screen
Clear-Host
# Adjust the buffer size of the window
$bw = 10000
$bh = 300000
if ($host.name -eq 'ConsoleHost') # or -notmatch 'ISE'
{
[console]::bufferwidth = $bw
[console]::bufferheight = $bh
}
else
{
$pshost = get-host
$pswindow = $pshost.ui.rawui
$newsize = $pswindow.buffersize
$newsize.height = $bh
$newsize.width = $bw
$pswindow.buffersize = $newsize
}
function Get-TextBetweenTwoStrings ([string]$startPattern, [string]$endPattern, [string]$filePath){
# Get content from the input file
$fileContent = Get-Content -Path $filePath -Raw
# Regular expression (Regex) of the given start and end patterns
$pattern = '(?is){0}(.*?){1}' -f [regex]::Escape($startPattern), [regex]::Escape($endPattern)
# Perform the Regex operation and output
[regex]::Match($fileContent,$pattern).Groups[1].Value
}
# Input file path
$inputFile = "THE-LOG-FILE.log"
# The patterns
$startPattern = 'START-OF-PATTERN'
$endPattern = 'END-OF-PATTERN'
Get-TextBetweenTwoStrings -startPattern $startPattern -endPattern $endPattern -filePath $inputFile
You need to perform streaming processing of your Get-Content call, in a pipeline, such as with ForEach-Object, if you want to process lines as they're being read.
This is a must if you're using Get-Content -Wait, because such a call doesn't terminate by itself (it keeps waiting for new lines to be added to the file, indefinitely), but inside a pipeline its output can be processed as it is being received, even before the command terminates.
You're trying to match across multiple lines, which with Get-Content output would only work if you used the -Raw switch - by default, Get-Content reads its input file(s) line by line.
However, -Raw is incompatible with -Wait.
Therefore, you must stick with line-by-line processing, which requires that you match the start and end patterns separately, and keep track of when you're processing lines between those two patterns.
Here's a proof of concept, but note the following:
-Tail 100 is hard-coded - adjust as needed or make it another parameter.
The use of -Wait means that the function will run indefinitely - waiting for new lines to be added to $filePath - so you'll need to use Ctrl-C to stop it.
While you can use a Get-TextBetweenTwoStrings call itself in a pipeline for object-by-object processing, assigning its result to a variable ($result = ...) won't work when terminating with Ctrl-C, because this method of termination also aborts the assignment operation.
To work around this limitation, the function below is defined as an advanced function, which automatically enables support for the common -OutVariable parameter, which is populated even in the event of termination with Ctrl-C; your sample call would then look as follows (as Theo notes, don't use the automatic $input variable as a custom variable):
# Look for blocks of interest in the input file, indefinitely,
# and output them as they're being found.
# After termination with Ctrl-C, $result will also contain the blocks
# found, if any.
Get-TextBetweenTwoStrings -OutVariable result -startPattern $startPattern -endPattern $endPattern -filePath $inputFile
Per your feedback you want the block of lines to encompass the full lines on which the start and end patterns match, so the regexes below are enclosed in .*
The word pattern in your $startPattern and $endPattern parameters is a bit ambiguous in that it suggests that they themselves are regexes that can therefore be used as-is or embedded as-is in a larger regex on the RHS of the -match operator.
However, in the solution below I am assuming that they are be treated as literal strings, which is why they are escaped with [regex]::Escape(); simply omit these calls if these parameters are indeed regexes themselves; i.e.:
$startRegex = '.*' + $startPattern + '.*'
$endRegex = '.*' + $endPattern + '.*'
The solution assumes there is no overlap between blocks and that, in a given block, the start and end patterns are on separate lines.
Each block found is output as a single, multi-line string, using LF ("`n") as the newline character; if you want a CRLF newline sequences instead, use "`r`n"; for the platform-native newline format (CRLF on Windows, LF on Unix-like platforms), use [Environment]::NewLine.
# Note the use of "-" after "Get", to adhere to PowerShell's
# "<Verb>-<Noun>" naming convention.
function Get-TextBetweenTwoStrings {
# Make the function an advanced one, so that it supports the
# -OutVariable common parameter.
[CmdletBinding()]
param(
$startPattern,
$endPattern,
$filePath
)
# Note: If $startPattern and $endPattern are themselves
# regexes, omit the [regex]::Escape() calls.
$startRegex = '.*' + [regex]::Escape($startPattern) + '.*'
$endRegex = '.*' + [regex]::Escape($endPattern) + '.*'
$inBlock = $false
$block = [System.Collections.Generic.List[string]]::new()
Get-Content -Tail 100 -Wait $filePath | ForEach-Object {
if ($inBlock) {
if ($_ -match $endRegex) {
$block.Add($Matches[0])
# Output the block of lines as a single, multi-line string
$block -join "`n"
$inBlock = $false; $block.Clear()
}
else {
$block.Add($_)
}
}
elseif ($_ -match $startRegex) {
$inBlock = $true
$block.Add($Matches[0])
}
}
}
First of all, you should not use $input as self-defined variable name, because this is an Automatic variable.
Then, you are reading the file as a string array, where you would rather read is as a single, multiline string. For that append switch -Raw to the Get-Content call.
The regex you are creating does not allow fgor regex special characters in the start- and end patterns you give, so it I would suggest using [regex]::Escape() on these patterns when creating the regex string.
While your regex does use a group capturing sequence inside the brackets, you are not using that when it comes to getting the value you seek.
Finally, I would recommend using PowerShell naming convention (Verb-Noun) for the function name
Try
function Get-TextBetweenTwoStrings ([string]$startPattern, [string]$endPattern, [string]$filePath){
# Get content from the input file
$fileContent = Get-Content -Path $filePath -Raw
# Regular expression (Regex) of the given start and end patterns
$pattern = '(?is){0}(.*?){1}' -f [regex]::Escape($startPattern), [regex]::Escape($endPattern)
# Perform the Regex operation and output
[regex]::Match($fileContent,$pattern).Groups[1].Value
}
$inputFile = "D:\Test\THE-LOG-FILE.log"
$startPattern = 'START-OF-PATTERN'
$endPattern = 'END-OF-PATTERN'
Get-TextBetweenTwoStrings -startPattern $startPattern -endPattern $endPattern -filePath $inputFile
Would result in something like:
blahblah
more lines here
The (?is) makes the regex case-insensitive and have the dot match linebreaks as well
Nice to see you're using my version of the Get-TextBetweenTwoStrings function, however I believe you are mistaking the output in the console to output as in a dedicated text editor. In the console, too long lines will be truncated, whereas in a text editor like notepad, you can choose to wrap long lines or have a horizontal scrollbar.
If you simply append
| Set-Content -Path 'X:\wherever\theoutput.txt'
to the Get-TextBetweenTwoStrings .. call, you will find the lines are NOT truncated when you open it in Word or notepad for instance.
In fact, you can have that line folowed by
notepad 'X:\wherever\theoutput.txt'
to have notepad open that file straight away.

clip and display result in same time

How can I use clip and display output at the same time?
Every time if I put | clip at the of the end line, the output is copied to clipboard, but not displayed in the console window.
Get-Date | clip
Use common parameter -ov (-OutVariable) to also capture Get-Date's output in a variable, then output that variable:
Get-Date -ov output | clip; $output
If the command you're invoking is not a cmdlet or advanced function/script and therefore doesn't support -OutVariable, you can use this technique instead:
($output = Get-Date) | clip; $output
This relies on the fact that enclosing a variable assignment in (...) passes the assigned value through.
You can package this functionality with the help of a custom function:
Function Write-OutputAndClip { $Input | Write-Output -ov output | clip; $output }
If you also define an alias for it, say clipecho ...
Set-Alias clipecho Write-OutputAndClip
... you can invoke it succinctly as:
Get-Date | clipecho # copies output to the clipboard *and* echoes it.
After looking into this a bit I think the only way to do what you're asking is to do it in two separate lines.
$value = Get-Date
Write-host $value
$value | clip
One line would look like this $value = Get-Date;$value;$value|clip
Powershell really wants to redirect anything from write-host to the console. And clip doesn't want to pass anything further down the pipeline...

add double quotes to variable that came from array

I'm creating a function that does some things to Excel files. The list of Excel files are passed into the function as an array called $excelFiles. The code shown below is in progress (it does not yet do all the things it's intended to do). This code, as written so far, appears to be failing because there are no quotes around the string held in $excelFile that sets the $wb variable (right before the nested foreach):
Function CovertExcelFileToTextFiles ($excelFiles)
{
# create an Excel application object (fire off Excel in the background)
$excelApp = New-Object -ComObject Excel.Application
$excelApp.Visible = $false
$excelApp.DisplayAlerts = $false
# get first 3 letters of each file's name
foreach ($excelFile in $excelFiles)
{
$name = Split-Path $excelFile.FullName -Leaf #get filename only from full path
$prefix = $name.Substring(0,3) #get first 3 letters of filename
#look at contents of this variable
$excelFile
$wb = $excelApp.Workbooks.Open($excelFile)
foreach ($ws in $wb.Worksheets)
{
$n = $prefix + "_"+ $ws.Name
$n
}
}
$excelApp.Quit()
}
Here is the error that appears in the console:
The reason I suspect the problem is lack of quotes is because the code works if $wb is set to a hardcoded file path.
I'm having difficulty figuring out how to get double quotes around the variable to feed into the line that sets $wb. I have tried "$excelFile" and the editor puts a red squiggly line under it so apparently that's not allowed. I have also tried creating a new variable and populating that with "$excelFile", then plugging that into the parenthesis in the $wb line. That causes an error in the console as well. How can double quotes be put around $excelFile?
Lack of quotes is not your issue. When you hardcode a file and use quotes, the quotes tell the parser that it's a string; they aren't part of the string value.
You need to know what data type $excelFile is (that is, $excelFiles is an array of what?).
If your use of $excelFile.FullName is correct, then it seems you're dealing with an object.
In that case, the .Open() method is likely expecting a [String] and won't understand the object you're passing it, so try:
$wb = $excelApp.Workbooks.Open($excelFile.FullName)
A good way to troubleshoot this kind of thing is to use PowerShell ISE, then set a breakpoint.
In your case, set a breakpoint on the $wb = ... line, execute, and when the breakpoint is hit, execution will stop at that line (before executing it).
At that point, you can use the console to execute statements that will be run in the context of your running code. So for example you could run:
$excelFile.GetType()
or
$excelFile | Get-Member
and learn some things about the object you're dealing with. You could look at its properties, etc.:
$excelFile.FullName
You can look at the overloads of the method you're calling:
$excelApp.Workbooks.Open # <-- note no parentheses

Get-Help format is different when calling it in a script

I am wondering why my PowerShell get-help outputs like the following image when using a script I've written. The script's purpose is to display get-help information when selecting a function from an array.
#Run this file in the same directory as the Functions file.
#this function validates user input
function getInput
{
do
{
$input = Read-Host "`n>Enter function # to see its description"
}until(([int]$input -gt 0) -and ([int]$input -le $flist.count))
$input
}
#include the script we want
. "$PSScriptRoot\functions.ps1"
#This operates on a loop. After viewing your help info, press key and you will be prompted to choose another function.
$quit = 0
while(!$quit){
#get all functions
$f = #(get-content functions.ps1 | where-object { $_.StartsWith("function", "CurrentCultureIgnoreCase") -and (-not $_.Contains("#")); $c++} | sort-object)
"There are " + $f.count + " functions!"
#split on ' ', get second word (function name), add to array
$flist = #{}
$i = 0
foreach($line in $f){
$temp = $line.split(' ')
$temp[1]
$i++
$flist.add($i, $temp[1])
}
#print, order ascending
$flist.GetEnumerator() | sort -Property name
#accept user input
$input = getInput
#get-help about the chosen function
"Get-Help " + $flist[[int]$input]
Get-Help Add-ADGrouptoLocalGroup | format-list
#Get-Help $flist[[int]$input] -full
Get-Command $flist[[int]$input] -syntax
Pause
}
The target script $PSScriptRoot\Functions.ps1 has a bunch of functions in it. What my script is doing is this:
List all functions found within a target file.
Put their name in an indexed array
Prompt user for which function to get-help on, at a given index
Print get-help and get-syntax on the selected function
Each function has the <#.SYNOPSIS .DESCRIPTION ... etc #> comment block in it (You can see the function's details--from the function's comment help-block--in the provided image). If I run get-help on the function within the target script, it appears to be formatted normally--but that's not the case when using script I've written.
What is really bothering me is the #{Text = 'stuff'} formatting, etc. Thanks ahead of time!
You're piping the output of get-help through format-list. This "overrides" the default formatting PS does on the PSCustomObject (in PS 3.0 at least) that get-help creates. You should be able to just invoke get-help by itself and not pipe it. If that doesn't work, then pipe it through out-default.
See help about_format for more details.

Copying to the clipboard in PowerShell without a new line

Is there a way to remove a new line from out-clipboard or clip in PowerShell?
I'm using this code to copy current path to clipboard:
function cl() {
(Get-Location).ToString() | clip
}
And every time I use this, a new line is added to the copied text. It's frustrating, because then I can't paste it in the CLI, like I would with text that is copied from elsewhere. Because a new line makes a command on the CLI automatically executed.
Example: I'm in C:\Users and type cl, and then I use Alt + SPACE + E + P to pass the text, the command is executed, and I can't type any more. But when text is passed without a new line nothing is executed, and I can continue to type.
Use the Set-Clipboard function:
(get-location).ToString()|Set-Clipboard
Add-Type -Assembly PresentationCore
$clipText = (get-location).ToString() | Out-String -Stream
[Windows.Clipboard]::SetText($clipText)
As pointed out by #PetSerAl in the comments, the newline is added by PowerShell when the string object is sent through the pipeline. The stringified output of Get-Location does not have that trailing newline:
PS C:\> $v = (Get-Location).ToString()
PS C:\> "-$v-"
-C:\-
You could try something like this:
Add-Type -AssemblyName System.Windows.Forms
$tb = New-Object Windows.Forms.TextBox
$tb.MultiLine = $true
$tb.Text = (Get-Location).ToString()
$tb.SelectAll()
$tb.Copy()
Ending the string with a null byte will take care of it. Useful for powershell core, which doesn't contain Set-Clipboard
function set-clipboard{
param(
[parameter(position=0,mandatory=$true,ValueFromPipeline=$true)]$Text
)
begin{
$data = [system.text.stringbuilder]::new()
}
process{
if ($text){
[void]$data.appendline($text)
}
}
end{
if ($data){
$data.tostring().trimend([environment]::newline) + [convert]::tochar(0) | clip.exe
}
}
}
"asdf" | set-clipboard