I'm putting together a little demo where I want one console window to show a current status. Here's my first iteration:
clear-host; while (1 -eq 1) { git log --graph --oneline; start-sleep -s 1; clear}
Unfortunately, this results in the text "flashing" on the screen every second as the screen clears, then is replaced with the next result. No only is the aesthetically unpleasing, it is albeit rare, potentially dangerous for some unsuspecting viewers. So my next try was this:
clear-host; while (1 -eq 1) { git log --graph --oneline; start-sleep -s 1; [Console]::CursorTop=0;[Console]::CursorLeft=0;}
There is no flash now, but longer lines are not cleared just drag down the screen.
Can anyone think of a way to get the best of both worlds? Clear the screen when I have too, but no repeating blink?
You might be able to continue along the path of console cursor manipulation, but PowerShell is not specifically designed for this scenario.
A far better alternative is to use Write-Progress to show real progress information.
$activity = "Git 'er done"
for() {
$result = & git log --graph --oneline
Write-Progress -Activity $activity -Id 1 -Status ($result -join ', ')
Start-Sleep -Seconds 1
}
Write-Progress -Activity $activity -Id 1 -Completed
(season to taste)
The reason I'm -joining $result is that on a repo of mine, the git command itself returns 3 lines, and $result is an array. You may want to decide how you handle that for your specific case, but be aware that $result can be an array and Write-Progress won't accept one.
Other Considerations
Write-Progress shows a graphical progress bar in ISE, but shows a text-based progress bar in the console host (powershell.exe) and in PowerShell Web Access. To put that another way, the specific PowerShell host application determines how the information is displayed, but it's available in a supported, defined way.
That means that Write-Progress should work in any environment (it may result in nothing happening, or display nothing, but it shouldn't crash).
In contrast, direct console manipulation probably only works in ISE and console host (admittedly they are probably the only hosts you care about), but if your code were suddenly hosted in a runspace in some C# application, it could just error out (I actually have no idea).
Related
I have a bunch of PDF files that I would like to print in sequence on a windows 7 computer using Powershell.
get-childItem "*.pdf" | sort lastWriteTime | foreach-object {start-process $._Name -verb 'print'}
The printed files are sometimes out of order like 1) A.pdf, 2) C.pdf, 3) B.pdf 4) D.pdf.
Different trials printed out a different sequence of files, thus, I fear the error is related to the printing queue or the start-process command. My guess is that each printing process is fired without waiting for the previous printing process to be completed.
Is there a way to consistently print out PDF files in a sequence that I specify?
You are starting the processes in order, but by default Start-Process does not wait until the command completes before it starts the next one. Since the commands take different amounts of time to complete based on the .PDF file size they print in whatever order they finish in. Try adding the -wait switch to your Start-Process, which will force it to wait until the command completes before starting the next one.
EDIT: Found an article elsewhere on Stack which addresses this. Maybe it will help. https://superuser.com/questions/1277881/batch-printing-pdfs
Additionally, there are a number of PDF solutions out there which are not Adobe, and some of them are much better for automation than the standard Reader. Adobe has licensed .DLL files you can use, and the professional version of Acrobat has hooks into the back end .DLLs as well.
If you must use Acrobat Reader DC (closed system or some such) then I would try opening the file to print and getting a pointer to the process, then waiting some length of time, and forcing the process closed. This will work well if your PDF sizes are known and you can estimate how long it takes to finish printing so you're not killing the process before it finishes. Something like this:
ForEach ($PDF in (gci "*.pdf"))
{
$proc = Start-Process $PDF.FullName -PassThru
Start-Sleep -Seconds $NumberOfSeconds
$proc | Stop-Process
}
EDIT #2: One possible (but untested) optimization is that you might be able use the ProcessorTime counters $proc.PrivilegedProcessorTime and $proc.UserProcessorTime to see when the process goes idle. Of course, this assumes that the program goes completely idle after printing. I would try something like this:
$LastPrivTime = 0
$LastUserTime = 0
ForEach ($PDF in (gci "*.pdf"))
{
$proc = Start-Process $PDF.FullName -PassThru
Do
{
Start-Sleep -Seconds 1
$PrivTimeElapsed = $proc.PrivilegedProcessorTime - $LastPrivTime
$UserTimeElapsed = $proc.UserProcessorTime - $LastUserTime
$LastPrivTime = $proc.PrivilegedProcessorTime
$LastUserTime = $proc.UserProcessorTime
}
Until ($PrivTimeElapsed -eq 0 -and $UserTimeElapsed -eq 0)
$proc | Stop-Process
}
If the program still ends too soon, you might need to increase the # of seconds to sleep inside the inner Do loop.
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.
I have a script I'm using to loop through a bunch of domains and get dates from whois.exe. This works line-by-line, but when run as a script, it'll freeze. Here is where it gets stuck:
ForEach ($domain in $domains)
{
$domainname = $domain.Name
Write-Host "Processing $domainname..."
# WhoIsCL responds with different information depending on if it's a .org or something else.
if($domainname -like "*.org" -and $domainname)
{
$date = .\WhoIs.exe -v "$domainname" | Select-String -Pattern "Registry Expiry Date: " -AllMatches
Write-Host "Domain is a .org" -ForegroundColor "Yellow"
When I CTRL+C to cancel the command, I can verify that $domain is the correct variable. I can then write this:
if($domainname -like "*.org" -and $domainname)
{
"Test"
}
... and "Test" appears in the command line. I then run:
$date = .\WhoIs.exe -v "$domainname" | Select-String -Pattern "Registry Expiry Date: " -AllMatches
Upon checking the date, it comes out right and I get the appropriate date. Given it freezes right as it says "Processing $domainname..." and right before "Domain is a .org", I can only assume WhoIs.exe is freezing. So, why does this happen as the script is being run, but not directly from the Powershell window?
Lastly, I did a final test by simply copying and pasting the entire script into a Powershell window (which is just silly, but it appears to function) and get the same result. It freezes at whois.exe.
My best guess is that whois.exe needs to be run differently to be reliable in Powershell in my for-loop. However, I don't seem to have a way to test using it in a Start-Process and get string output.
Anyways, advise would be great. I've definitely hit a wall.
Thanks!
If your script is running through lots of domains, it could be that you're being throttled. Here is a quote from the Nominet AUP:
The maximum query rate is 5 queries per second with a maximum of 1,000
queries per rolling 24 hours. If you exceed the query limits a block
will be imposed. For further details regarding blocks please see the
detailed instructions for use page. These limits are not per IP
address, they are per user.
http://registrars.nominet.org.uk/registration-and-domain-management/acceptable-use-policy
Different registrars may behave differently, but I'd expect some sort of rate limit. This would explain why a script (with high volume) behaves differently to ad-hoc manual lookups.
Proposed solution from the comments below is to add Start-Sleep -Seconds 1 to the loop between each Whois lookup.
I am pretty new to powershell (About 1 week in) and I am trying to create a tool for our helpdesk to import and export printers. The tool is running great except for the form is freezing when the code is being run.
To mitigate the freezing, I found that running it as a job gets the task done, however I am having 2 issues with it.
I am not able to get the progress bar to increase 1 step as a result of the job completing.
I am not able to pass variables to it. (I am not as worried about this as there is a ton of information on it, I just need to figure out the syntax for it. If you could help with that as well, that would be great though.)
start-job -scriptblock {
C:\Windows\system32\spool\tools\PrintBrm.exe -b -f \\filestore\$EXPORTPRINTERS.printerExport
$progressbarexportprinters.PerformStep()
$progressbarexportprinters.TextOverlay = "Printer Export Complete"
}
I found a solution for this. the form is still freezing, but I can show movement on the progress bar. Which will be good enough.
C:\Windows\system32\spool\tools\PrintBrm.exe -r -f \\filestore\$EXPORTPRINTERS.printerExport | out-string -Stream | foreach-object {
$richTextBox1.lines = $richTextBox1.lines + $_
$richTextBox1.Select($richTextBox1.Text.Length, 0)
$richTextBox1.ScrollToCaret()
$progressbaraddprinters.PerformStep()
}
There are a number of different ways to output messages. What is the effective difference between outputting something via Write-Host, Write-Output, or [console]::WriteLine?
I also notice that if I use:
write-host "count=" + $count
The + gets included in the output. Why's that? Shouldn't the expression be evaluated to produce a single concatenated string before it gets written out?
Write-Output should be used when you want to send data on in the pipe line, but not necessarily want to display it on screen. The pipeline will eventually write it to out-default if nothing else uses it first.
Write-Host should be used when you want to do the opposite.
[console]::WriteLine is essentially what Write-Host is doing behind the scenes.
Run this demonstration code and examine the result.
function Test-Output {
Write-Output "Hello World"
}
function Test-Output2 {
Write-Host "Hello World" -foreground Green
}
function Receive-Output {
process { Write-Host $_ -foreground Yellow }
}
#Output piped to another function, not displayed in first.
Test-Output | Receive-Output
#Output not piped to 2nd function, only displayed in first.
Test-Output2 | Receive-Output
#Pipeline sends to Out-Default at the end.
Test-Output
You'll need to enclose the concatenation operation in parentheses, so that PowerShell processes the concatenation before tokenizing the parameter list for Write-Host, or use string interpolation
write-host ("count=" + $count)
# or
write-host "count=$count"
BTW - Watch this video of Jeffrey Snover explaining how the pipeline works. Back when I started learning PowerShell I found this to be the most useful explanation of how the pipeline works.
Apart from what Andy mentioned, there is another difference which could be important - write-host directly writes to the host and return nothing, meaning that you can't redirect the output, e.g., to a file.
---- script a.ps1 ----
write-host "hello"
Now run in PowerShell:
PS> .\a.ps1 > someFile.txt
hello
PS> type someFile.txt
PS>
As seen, you can't redirect them into a file. This maybe surprising for someone who are not careful.
But if switched to use write-output instead, you'll get redirection working as expected.
Here's another way to accomplish the equivalent of Write-Output. Just put your string in quotes:
"count=$count"
You can make sure this works the same as Write-Output by running this experiment:
"blah blah" > out.txt
Write-Output "blah blah" > out.txt
Write-Host "blah blah" > out.txt
The first two will output "blah blah" to out.txt, but the third one won't.
"help Write-Output" gives a hint of this behavior:
This cmdlet is typically used in scripts to display strings and other
objects on the console. However, because the default behavior is to
display the objects at the end of a pipeline, it is generally not
necessary to use the cmdlet.
In this case, the string itself "count=$count" is the object at the end of a pipeline, and is displayed.
For usages of Write-Host, PSScriptAnalyzer produces the following diagnostic:
Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.
See the documentation behind that rule for more information. Excerpts for posterity:
The use of Write-Host is greatly discouraged unless in the use of commands with the Show verb. The Show verb explicitly means "show on the screen, with no other possibilities".
Commands with the Show verb do not have this check applied.
Jeffrey Snover has a blog post Write-Host Considered Harmful in which he claims Write-Host is almost always the wrong thing to do because it interferes with automation and provides more explanation behind the diagnostic, however the above is a good summary.
From my testing Write-Output and [Console]::WriteLine() perform much better than Write-Host.
Depending on how much text you need to write out this may be important.
Below if the result of 5 tests each for Write-Host, Write-Output and [Console]::WriteLine().
In my limited experience, I've found when working with any sort of real world data I need to abandon the cmdlets and go straight for the lower level commands to get any decent performance out of my scripts.
measure-command {$count = 0; while ($count -lt 1000) { Write-Host "hello"; $count++ }}
1312ms
1651ms
1909ms
1685ms
1788ms
measure-command { $count = 0; while ($count -lt 1000) { Write-Output "hello"; $count++ }}
97ms
105ms
94ms
105ms
98ms
measure-command { $count = 0; while ($count -lt 1000) { [console]::WriteLine("hello"); $count++ }}
158ms
105ms
124ms
99ms
95ms
Regarding [Console]::WriteLine() - you should use it if you are going to use pipelines in CMD (not in powershell). Say you want your ps1 to stream a lot of data to stdout, and some other utility to consume/transform it. If you use Write-Host in the script it will be much slower.