I have a script that runs several loops of code and relies on specific input at various phases in order to advance. That functionality is working. My current issue revolves around extraneous input being supplied by the user displaying on screen in the console window wherever I have the cursor position currently aligned.
I have considered ignoring this issue since the functionality of the script is intact, however, I am striving for high standards with the console display of this script, and I would like to know a way to disable all user input period, unless prompted for. I imagine the answer has something to do with being able to command the Input Buffer to store 0 entries, or somehow disabling and then re-enabling the keyboard as needed.
I have tried using $HOST.UI.RawUI.Flushinputbuffer() at strategic locations in order to prevent characters from displaying, but I don't think there's anywhere I could put that in my loop that will perfectly block all input from displaying during code execution (it works great for making sure nothing gets passed when input is required, though). I've tried looking up the solution, but the only command I could find for manipulating the Input Buffer is the one above. I've also tried strategic implementation of the $host.UI.RawUI.KeyAvailable variable to detect keystrokes during execution, then $host.UI.RawUI.ReadKey() to determine if these keystrokes are unwanted and do nothing if they are, but the keystrokes still display in the console no matter what.
I am aware that this code is fairly broken as far as reading the key to escape the loop goes, but bear with me. I hashed up this example just so that you could see the issue I need help eliminating. If you hold down any letter key during this code's execution, you'll see unwanted input displaying.
$blinkPhase = 1
# Set Coordinates for cursor
$x = 106
$y = 16
$blinkTime = New-Object System.Diagnostics.Stopwatch
$blinkTime.Start()
$HOST.UI.RawUI.Flushinputbuffer()
do {
# A fancy blinking ellipses I use to indicate when Enter should be pressed to advance.
$HOST.UI.RawUI.Flushinputbuffer()
while ($host.UI.RawUI.KeyAvailable -eq $false) {
if ($blinkTime.Elapsed.Milliseconds -gt 400) {
if ($blinkPhase -eq 1) {
[console]::SetCursorPosition($x,$y)
write-host ". . ." -ForegroundColor gray
$blinkPhase = 2
$blinkTime.Restart()
} elseif ($blinkPhase -eq 2) {
[console]::SetCursorPosition($x,$y)
write-host " "
$blinkPhase = 1
$blinkTime.Restart()
}
}
start-sleep -m 10
}
# Reading for actual key to break the loop and advance the script.
$key = $host.UI.RawUI.ReadKey()
} while ($key.key -ne "Enter")
The expected result is that holding down any character key will NOT display the input in the console window while the ellipses is blinking. The actual result, sans error message, is that a limited amount of unwanted/unnecessary input IS displaying in the console window, making the script look messy and also interfering with the blinking process.
What you're looking for is to not echo (print) the keys being pressed, and that can be done with:
$key = $host.UI.RawUI.ReadKey('IncludeKeyDown, NoEcho')
Also, your test for when Enter was pressed is flawed[1]; use the following instead:
# ...
} while ($key.Character -ne "`r")
Caveat: As of at least PSReadLine version 2.0.0-beta4, a bug causes $host.UI.RawUI.KeyAvailable to report false positives, so your code may not work as intended - see this GitHub issue.
Workaround: Use [console]::KeyAvailable instead, which is arguably the better choice anyway, given that you're explicitly targeting a console (terminal) environment with your cursor-positioning command.
As an aside: You can simplify and improve the efficiency of your solution by using a thread job to perform the UI updates in a background thread, while only polling for keystrokes in the foreground:
Note: Requires the ThreadJob module, which comes standard with PowerShell Core, and on Windows PowerShell can be installed with Install-Module ThreadJob -Scope CurrentUser, for instance.
Write-Host 'Press Enter to stop waiting...'
# Start the background thread job that updates the UI every 400 msecs.
# NOTE: for simplicity, I'm using a simple "spinner" here.
$jb = Start-ThreadJob {
$i=0
while ($true) {
[Console]::Write("`r{0}" -f '/-\|'[($i++ % 4)])
Start-Sleep -ms 400
}
}
# Start another thread job to do work in the background.
# ...
# In the foreground, poll for keystrokes in shorter intervals, so as
# to be more responsive.
While (-not [console]::KeyAvailable -or ([Console]::ReadKey($true)).KeyChar -ne "`r" ) {
Start-Sleep -Milliseconds 50
}
$jb | Remove-Job -Force # Stop and remove the background UI thread.
Note the use of [Console]::Write() in the thread job, because Write-Host output wouldn't actually be passed straight through to the console.
[1] You tried to access a .Key property, which only the [SystemConsoleKeyInfo] type returned by [console]::ReadKey() has; the approximate equivalent in the $host.UI.rawUI.ReadKey() return type, [System.Management.Automation.Host.KeyInfo], is .VirtualKeyCode, but its specific type differs, so you can't (directly) compare it to "Enter"; The latter type's .Character returns the actual [char] instance pressed, which is the CR character ("`r") in the case of Enter.
Related
I have a script that inventories the installed software on a machine, based on certain criteria provided in arguments. It also needs to support being buried in a very deep folder structure (Architects just think like that and I need to support it, not control it). And since it now also supports AppX, which requires running elevated, which in turn requires the full path to the PS1 in the -file argument, I am running into issues with the overall length of the Target value in the shortcut.
So, I was looking at the usual run with the -verb argument approach, like this
If (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$psArguments = #('-nologo', '-noExit', '-executionpolicy', 'bypass', '-verb', 'Runas', '-file',$PSCommandPath)
$pxArguments = #($myInvocation.BoundParameters.GetEnumerator().ForEach({"-$($_.Key)", "$($_.Value)"}))
$arguments = $psArguments + $pxArguments
Start-Process 'powershell.exe' -ArgumentList:$arguments -Verb:Runas
Stop-Process $PID
} else {
Write-Host 'Elevated'
# actual code to process
}
But I am having two issues there, one is critical, one less so.
On the critical side, I see a second blue PowerShell console open for a split second, but then it closes with no error or anything. I would have expected the -noExit argument in $psArguments to at least leave that window open for me, so I could read any errors. But I can't see any red, so that makes me think there is no error, and this usually happens when the -file argument doesn't point to an actual script. However, if I validate the arguments like so
foreach ($item in $arguments) {
Write-Host "$item"
}
Write-Host "$PSCommandPath $(Test-Path $PSCommandPath)"
The arguments all look good and Test-Path returns true.
So, what in this am I getting wrong, such that I don't get a new PowerShell console that actually works?
The second issue is that I have defined fonts and colors and console size in the shortcut, and I wonder if there is a single variable, perhaps $host, that I could then send to the second session populated with the values of the first. I suspect not, because I think the only types that can be used as arguments are strings and numbers.
DOH: I was on the right track, with that comment about it looking like the script wasn't found. There are SPACES in that path, so I need
$psArguments = #('-nologo', '-noExit', '-executionpolicy', 'bypass', '-file',"`"$PSCommandPath`"")
Now I just need to deal with resizing the console. I can live without fonts and colors, but the width and text wrapping I need to address.
Disclaimer : I am the epitome of a scipting/Powershell rookie, so please bear with me.
I've written a script to return the Active Directory username of any user currently logged into a given workstation.
$input = Read-Host "Workstation Name"
$domain = ".*****.***.com"
$computer = $input + $domain
$list = gwmi win32_computersystem -comp $computer | select Username,Caption
Write-Output $list
However, if I run this from a pinned script in the taskbar, the Powershell window closes before I have a chance to view the results.
I have tried method 2 and 3 from this post, but to no avail. Method 2 prompts for user input before the results are displayed instead of after, even when the code for the prompt is added at the end of the script.
Any help would be greatly appreciated.
Method 2 from the linked post - i.e., waiting for the user to press a key before exiting the script - can be used, but it requires additional effort:
End your script as follows in order to see the value of $list before the pause command prompts:
$list | Out-Host # Force *synchronous* to-display output.
pause # Wait for the user to press Enter before exiting.
Note: pause in PowerShell is simply a function wrapper around Read-Host as follows: $null = Read-Host 'Press Enter to continue...' Therefore, if you want to customize the prompt string, call Read-Host directly.
This answer explains why the use of Out-Host (or Format-Table) is necessary in this case; in short:
In PSv5+, an implicitly applied Format-Table command asynchronously waits for up to 300 msecs. for additional pipeline input, in an effort to derive suitable column widths from the input data.
Because you use Write-Output output objects without predefined formatting data that have 2 properties (4 or fewer ), tabular output is implicitly chosen, and Format-Table is used behind the scenes, asynchronously.
Note: The asynchronous behavior applies only to output objects for whose types formatting instructions aren't predefined (as would be reported with Get-FormatData <fullOutputTypeName>); for instance, the output format for the System.Management.Automation.AliasInfo instances output by Get-Alias is predefined, so Get-Alias; pause does produce output in the expected sequence.
The pause command executes before that waiting period has elapsed, and only after you've answered the prompt does the table print, after which point the window closes right away.
The use of an explicit formatting command (Out-Host in the most generic case, but any Format-* cmdlet will do too) avoids that problem by producing display output synchronously, so that the output will be visible by the time pause displays its prompt.
I had the same problem for scripts that I'm executing "on demand". I tend to simply add a Read-Host at the end of the script like so
$str = "This text is hardly readable because the console closes instantly"
Write-Output $str
Read-Host "Script paused - press [ENTER] to exit"
Disclaimer : I am the epitome of a scipting/Powershell rookie, so please bear with me.
I've written a script to return the Active Directory username of any user currently logged into a given workstation.
$input = Read-Host "Workstation Name"
$domain = ".*****.***.com"
$computer = $input + $domain
$list = gwmi win32_computersystem -comp $computer | select Username,Caption
Write-Output $list
However, if I run this from a pinned script in the taskbar, the Powershell window closes before I have a chance to view the results.
I have tried method 2 and 3 from this post, but to no avail. Method 2 prompts for user input before the results are displayed instead of after, even when the code for the prompt is added at the end of the script.
Any help would be greatly appreciated.
Method 2 from the linked post - i.e., waiting for the user to press a key before exiting the script - can be used, but it requires additional effort:
End your script as follows in order to see the value of $list before the pause command prompts:
$list | Out-Host # Force *synchronous* to-display output.
pause # Wait for the user to press Enter before exiting.
Note: pause in PowerShell is simply a function wrapper around Read-Host as follows: $null = Read-Host 'Press Enter to continue...' Therefore, if you want to customize the prompt string, call Read-Host directly.
This answer explains why the use of Out-Host (or Format-Table) is necessary in this case; in short:
In PSv5+, an implicitly applied Format-Table command asynchronously waits for up to 300 msecs. for additional pipeline input, in an effort to derive suitable column widths from the input data.
Because you use Write-Output output objects without predefined formatting data that have 2 properties (4 or fewer ), tabular output is implicitly chosen, and Format-Table is used behind the scenes, asynchronously.
Note: The asynchronous behavior applies only to output objects for whose types formatting instructions aren't predefined (as would be reported with Get-FormatData <fullOutputTypeName>); for instance, the output format for the System.Management.Automation.AliasInfo instances output by Get-Alias is predefined, so Get-Alias; pause does produce output in the expected sequence.
The pause command executes before that waiting period has elapsed, and only after you've answered the prompt does the table print, after which point the window closes right away.
The use of an explicit formatting command (Out-Host in the most generic case, but any Format-* cmdlet will do too) avoids that problem by producing display output synchronously, so that the output will be visible by the time pause displays its prompt.
I had the same problem for scripts that I'm executing "on demand". I tend to simply add a Read-Host at the end of the script like so
$str = "This text is hardly readable because the console closes instantly"
Write-Output $str
Read-Host "Script paused - press [ENTER] to exit"
I want to monitor a log file which is constantly being added to (every few seconds) over the course of a 2 hour period. I am currently using Get-Content file.txt –Wait which displays the content to the screen and allows me to see what’s being added to the file but I need to take this a step further and actually watch for specific messages and if something I’m looking for in the log file appears, then do something. Initially I used a .net file reader with a for loop as shown below
try {
for(;;) {
$line = $log_reader.ReadLine()
if ($line -match "something")
{
Write-Host "we have a match"
Break
}
The issue with this however is that it was causing the process that is generating the log file to fall over - it throws an error (because another process is using the log file it’s creating – I thought this was odd because I assumed the .net stream reader would just be ‘reading’ the file). I don’t have any control over the process which is generating the log file so I don’t know what it’s doing exactly (I’m guessing it has the file in read/write mode with some kind of lock which gets upset when I try to read the file using a .net stream reader). Doing a Get-Content on the file doesn’t seem to cause this issue however.
The question is, how can I use something like Get-Content (or another process) to monitor the log file but move onto another part of the script if a message I’m looking for in the log appears?
If you constantly want to monitor the log file and catch the desired pattern as soon as it appears:
while(!(Select-String -path 'c:\log.txt' -pattern 'something' -quiet)){};
## when finds the pattern, comes out of the loop and proceed to next part of the script
Write-host 'we have a match'
You may want to change the path of the file to yours.
Though this would work, there will be a lot of processing that your computer have to do, since the while loop is a constant loop. If you can afford to introduce some delay, like if it is OK to find the error 30 sec or whatever is your threshold, after it appeared, then you can consider introducing sleep:
while(!(Select-String -path 'c:\log.txt' -pattern 'something' -quiet)){start-sleep -seconds 30};
## when finds the pattern, comes out of the loop and proceed to next part of the script
Write-host 'we have a match'
You can write a small logic to terminate the script after two hours, otherwise it would become an infinite loop, if 'something' doesn't gets written at all in the log file.
Edit 1:
If you want to print the new lines at the console, you can try manipulating a bit like:
while(!(Select-String -path 'c:\log.txt' -pattern 'something' -quiet))
{
$a = get-content 'c:\log.txt'
if(($a.count) -gt $b )
{
$a[$b..($a.count)]
}
$b = ($a.count)
start-sleep -Seconds 30
}
## To print the line containing the pattern when while loop exited
(Get-content 'c:\log.txt')[-1]
Is this possible? I've finally decided to start setting up my personal .NET development environment to closer mimic how I'd set up a *NIX dev environment, which means learning Powershell in earnest.
I'm currently writing a function that recurses through the file system, setting the working directory as it goes in order to build things. One little thing that bothers me is that if I Ctrl+C out of the function, it leaves me wherever the script last was. I've tried setting a trap block that changes the dir to the starting point when run, but this seems to only be intended (and fire) on Exception.
If this were in a language that had root in Unix, I'd set up a signal handler for SIGINT, but I can't find anything similar searching in Powershell. Putting on my .NET cap, I'm imagining there's some sort of event that I can attach a handler to, and if I had to guess, it'd be an event of $host, but I can't find any canonical documentation for System.Management.Automation.Internal.Host.InternalHostUserInterface, and nothing anecdotal that I've been able to search for has been helpful.
Perhaps I'm missing something completely obvious?
Do you mean something like this?
try
{
Push-Location
Set-Location "blah"
# Do some stuff here
}
finally
{
Pop-Location
}
See documentation here. Particularly that paragraph: "The Finally block statements run regardless of whether the Try block encounters a terminating error. Windows PowerShell runs the Finally block before the script terminates or before the current block goes out of scope. A Finally block runs even if you use CTRL+C to stop the script. A Finally block also runs if an Exit keyword stops the script from within a Catch block."
This handles console kepboard input. If control C is pressed during the loop you'll have a chance to handle the event however you want. In the example code a warning is printed and the loop is exited.
[console]::TreatControlCAsInput = $true
dir -Recurse -Path C:\ | % {
# Process file system object here...
Write-Host $_.FullName
# Check if ctrl+C was pressed and quit if so.
if ([console]::KeyAvailable) {
$key = [system.console]::readkey($true)
if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
Write-Warning "Quitting, user pressed control C..."
break
}
}