Pause a Powershell script and resume - powershell

I have a script which currently pulls the file location from a CSV and uploads the files to a database, using a ForEach-Object loop.
What I'd like it to do is upload 1000 files, then be able to pause the loop and resume it later from file 1001.
I don't want to use the Start-Sleep command, as I do not want the script to automatically resume after a set amount of time.
This is a one time deal, so I'd rather not convert it to a workflow.
What command or cmdlet can be used to accomplish this?
The Read-Host command would be perfect if there were a way to break the script and then resume from the same line later.

Here's how I'd do it:
$i = 0;
foreach ($file in (Get-ChildItem $path_to_directory)) {
# Code to upload the file referenced by $file
if (++$i -eq 1000) {
Write-Host -NoNewLine '1000 files have been uploaded. Press capital "C" to continue uploading the remaining files...'
do {
} until (($Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp').Character) -ceq 'C')
}
}
Using pause, as already suggested in Bluecakes' answer, is a perfectly good solution. The advantage of this method is that it gives you more control. pause always requires the Enter key and always gives you the same prompt, Press Enter to continue...:, whereas this way you can define both to your liking.
More significantly, and the reason I personally prefer to do it this way in my own scripts, is that you can protect against accidental key presses. In my example I made the required keystroke a capital C, so that it's very unlikely that you'd continue by accident.
However, if you don't care about any of that and just want a quick and simple say to do it, then pause is really all you need.

Use pause:
For ($i=1; $i -lt 2000; $i++) {
if ($i -eq 1001)
{
pause
}
Write-Host $i
}

Something along these lines could work for you...
For ($i=1; $i -lt 50; $i++) {
if ($i -eq 10)
{
$input = Read-Host "Should I continue Y/N"
if ($input -eq "N")
{
break
}
}
Write-Host $i
}

Related

Is there any way to colour text previously written using "Write-Host" in powershell?

I want to create the "Select-Multiple" function.
The function takes some parameters, but the most important one is the list of options.
Let's say
#("First Option", "Second Option")
Then the function will display something like:
a All
b First Option
c Second Option
d Exit
Choose your option: > ...
The "Choose your option: > ..." text, will be repeated as long as:
User choose "All" or "Exit" option
User will choose all possible options (other than "All" and "Exit")
At the end the function returns the List of options chosen by the user.
Simple. But... I'd like to highlight the options already chosen by the user.
So if the user chose "b", then "b First Option" gets green colour.
Is it possible to do something like that, without using Clear-Host, as I don't want to clear previous steps?
I attach you my "Select-Multiple" function in powershell, sorry if that's ugly written, but I don't use powershell that often.
function Select-Multiple {
Param(
[Parameter(Mandatory=$false)]
[string] $title,
[Parameter(Mandatory=$false)]
[string] $description,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
$options,
[Parameter(Mandatory=$true)]
[string] $question
)
if ($title) {
Write-Host -ForegroundColor Yellow $title
Write-Host -ForegroundColor Yellow ("-"*$title.Length)
}
if ($description) {
Write-Host $description
Write-Host
}
$chosen = #()
$values = #()
$offset = 0
$all = "All"
$values += #($all)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $all
$offset++
$options.GetEnumerator() | ForEach-Object {
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
$values += #($_)
Write-Host $_
$offset++
}
$exit = "Exit"
$values += #($exit)
Write-Host -ForegroundColor Yellow "$([char]($offset+97)) " -NoNewline
Write-Host $exit
$answer = -1
while($chosen.Count -ne $options.Count) {
Write-Host "$question " -NoNewline
$selection = (Read-Host).ToLowerInvariant()
if (($selection.Length -ne 1) -or (([int][char]($selection)) -lt 97 -or ([int][char]($selection)) -gt (97+$offset))) {
Write-Host -ForegroundColor Red "Illegal answer. " -NoNewline
}
else {
$answer = ([int][char]($selection))-97
$value = $($values)[$answer]
if ($value -eq $exit) {
return $chosen
}
if ($value -eq $all) {
return $options
}
else {
if ($chosen.Contains($value)) {
Write-Host -ForegroundColor Red "The value $value was already chosen."
}
else {
$chosen += ($value)
}
}
}
if ($answer -eq -1) {
Write-Host -ForegroundColor Red "Please answer one letter, from a to $([char]($offset+97))"
}
$answer = -1;
}
return $chosen
}
Because of how the console window works, you can't just recolor an existing line. Once you've written something to the console, the only way you can modify it is by overwriting it. This is no different when applying colors.
To understand why this is the case, let's go over how text is colored in PowerShell. Let's use the following command as an example:
Write-Host "Test" -ForegroundColor Green
Here is a (simplified) step by step overview of what this command will do:
Set the ForegroundColor property of the console to green
Write "Test" to the console
Set the ForegroundColor property of the console to whatever it was previously
This explains why we are unable to change the color of text that has already been written to the console. If you want to color a piece of text, you are required to set the console color before writing the text to the console.
However, there are a couple ways to create the same visual effect. In fact there are exactly two ways. One of them is clearing the console and re-writing everything which you mentioned you don't want to do, so let's talk about the other way first.
Overwriting Individual Lines
Let me preface this by saying that this does not work very well with the PowerShell ISE. If you decide to use this, you will have to debug it by either using the normal PowerShell console, or an IDE that supports this. This is also the more complicated option, so if you don't want to deal with the complexity of it, the second option would be the way to go.
The console window allows you to retrieve and set the cursor position by using Console.CursorLeft and Console.CursorTop. You can also use Console.SetCursorPosition() if you need to set both of them at the same time. There is also $Host.UI.RawUI.CursorPosition, but it's long and has some strange side effects when paired with Read-Host, so I would not recommend using it. When you write output to the console, it will write the output to wherever the cursor happens to be. We can use this to our advantage by setting the cursor's position to the beginning of the line we want to change the color of, then overwriting the normal text with colored text or vice versa.
In order to do this, all we need to do is keep track of which option is on which line. This is pretty simple, especially if you have an array of options that is in the same order that you printed them to the console in.
Here is a simple script I made that does exactly this:
$options = $("Option 1", "Option 2", "Option 3")
$initialCursorTop = [Console]::CursorTop
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
for($i = 0; $i -lt $options.Length; $i++)
{
Write-Host "$($i + 1). $($options[$i])"
}
Write-Host # Add an extra line break to make it look pretty
while($true)
{
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$oldCursorTop = [Console]::CursorTop
$oldCursorLeft = [Console]::CursorLeft
# Set the cursor to the beginning of the line corresponding to the selected option.
$index = $number - 1
[Console]::SetCursorPosition(0, $index + $initialCursorTop)
$choice = $options[$index]
$isSelected = $selectedOptionArr[$index]
$choiceText = "$($number). $($choice)"
if($isSelected)
{
Write-Host $choiceText -NoNewline
}
else
{
Write-Host $choiceText -ForegroundColor Green -NoNewline
}
$selectedOptionArr[$index] = !$isSelected
[Console]::SetCursorPosition($oldCursorLeft, $oldCursorTop)
}
# Subtract 1 from Y to compensate for the new line created when providing input.
[Console]::SetCursorPosition(0, [Console]::CursorTop - 1)
# Clear the input line.
Write-Host (' ' * $Host.UI.RawUI.WindowSize.Width) -NoNewline
[Console]::CursorLeft = 0
}
The main advantage of this approach is that it doesn't need to clear the entire console in order to update text. This means you can display whatever you want above it without worrying about it being cleared every time the user inputs something. Another advantage is that this performs a minimal number of operations in order to accomplish the task.
The main disadvantage is that this is relatively volatile. This requires you to use exact line numbers, so if something happens that offsets some of the lines (such as one option being multiple lines), it will more than likely cause some major issues.
However, these disadvantages can be overcome. Since you have access to $Host.UI.RawUI.WindowSize.Width which tells you how many characters you can put on a single line, we know that any string with a length greater than this will be wrapped onto multiple lines. Another option is just to keep track of which line the cursor starts on, then you can clear all text between the starting position and where the cursor currently is.
Clearing the Console
This approach is much simpler because you don't have to worry about what is on which line or where the cursor is at all. The idea is that you simply clear the entire console, then re-write everything with the changes you want to make. This is the nuclear approach, but it's also the most reliable.
Here is the same example as above using this approach instead:
$options = $("Option 1", "Option 2", "Option 3")
# An array to keep track of which options are selected.
# All entries are initially set to $false.
$selectedOptionArr = New-Object bool[] $options.Length
while($true)
{
Clear-Host
for($i = 0; $i -lt $options.Length; $i++)
{
if($selectedOptionArr[$i])
{
Write-Host "$($i + 1). $($options[$i])" -ForegroundColor Green
}
else
{
Write-Host "$($i + 1). $($options[$i])"
}
}
Write-Host # Add an extra line break to make it look pretty
Write-Host "Choose an option>" -NoNewline
$input = Read-Host
$number = $input -as [int]
if($number -ne $null -and
$number -le $options.Length -and
$number -gt 0)
{
# Input is a valid number that corresponds to an option.
$index = $number - 1
$choice = $options[$index]
$selectedOptionArr[$index] = !$selectedOptionArr[$index]
}
}
The main advantage of this approach is that it's super simple and easy to understand.
The main disadvantage is that this clears the entire console every time the user inputs something. In most cases this isn't a huge problem, but it can be with large data sets.
I agree with Trevor Winge, I it is most likely not possible to change earlier console outputs appearance, but it is for certain not worth your while. There are certain limitations of the console, and that is ok since that is what GUIs are for. I hope you feel encouraged by this situation to look into Windows.Froms or WPF. Make use of the controls! checkboxes would be interesting for your scenario. I know that is not exactly what you asked for, but it is garanteed a journey that is worth your time. When i started my first GUI with powershell, i was astonished how much i could accomplish with little afford. Stackoverflow is full of examples.

Interrupting a loop

I just do my first steps with powershell and have a questions about a loop I'm using in a script:
for ($i=1; $i -le 100; $i++){
$res = Test-Connection $IP -count 1 -Quiet
...do something more
start-sleep -seconds 30
}
This script does not allow to close the windows form (it's started from a GUI) or interrupt the loop. Is there a way to do so? Sometimes I want to stop the loop manually.
Thanks a lot for your help.
I think you might want to use powershell flow control.
With powershell flow control you are able to manually control loops in powershell.
Let me give you an example:
for ($i=1; $i -le 100; $i++){
$res = Test-Connection $IP -count 1 -Quiet
...do something more
if ($res -eq $anyresultyouwouldexpect) {
break ##with **break** you interupt the loop completely- You script
## would continue after the loop.
}
}
There are also flow control statements like continue to jump into the next iteration loop. It is depending on what you need in your case.

How to get script to start over

I have this section of code that I need to loop through if the file is locked.
What I want to do is if the file is locked the script goes to sleep for 10 seconds then goes back to the if (test-Path) and runs through again until the file(s) are no longer locked.
I'm just not understanding how to do this, any help is appreciated.
if (Test-Path -Path "$path\*")
{
# Code for directory not empty
# enumerate the items array
foreach ($item in $items)
{
# if the item is NOT a directory, then process it.
if ($item.Attributes -ne "Directory")
{
$filePath = $path+$item.name
}
else
{
}
function isFileLocked([string]$LockedPath)
{
$oFile = New-Object System.IO.FileInfo $LockedPath
# Make sure the path is good
if ((Test-Path -LiteralPath $LockedPath) -eq $false)
{
#If file is locked go to sleep for 2 minutes and check again, loop until the file is no longer locked.
Start-Sleep -s 10
# Go back and check directory again to see if more files have come in
#return $false
}
You need to use a while or do loop if you want it to go back to the if statement. If you want it to skip to the next item in the foreach loop you can use continue. help about_continue
Edit: example:
Do {
Start-Sleep -s 10
}
Until (Test-Path $LockedPath)
or
#Script block executes as long as condition value = True
While (! (Test-Path $Lockedpath) )
{Sleep 10}
The do until is easier logically to use, but the benefit of the second option is it tests the condition before executing the script block which in some cases (such as this one), it will be more efficient.

PowerShell -- Looking for equivalent of Perl's redo

I have the following PowerShell loop:
Set-Variable -Name YELLOW -Option ReadOnly -Value 0
Set-Variable -Name ORANGE -Option ReadOnly -Value 1
Set-Variable -Name RED -Option ReadOnly -Value 2
While ($true) {
for ($color = $YELLOW; $color -le $RED; $color++) {
$LastWriteTime = ((Get-Item $FilePath).LastWriteTime)
$waitTime = (Get-Date).AddMinutes(-($WarningTimeArray[$color]))
$SleepTime = ($LastWriteTime - $waitTime).TotalSeconds
If ($SleepTime -gt 0) {
$WarningFlagArray[$color] = $false;
SendMail ("Start-Sleep -Seconds" + $SleepTime)
Start-Sleep -Seconds $SleepTime
Continue #Actually want a "redo"
}
ElseIf (-not $WarningFlagArray[$color]) {
SendMail $WarningMessageArray[$color]
$WarningFlagArray[$color] = $true
}
}
}
When PowerShell hits the "Continue", it continues back to the top of the for loop, and goes to the next color.
In Perl, you have last (equivalent to PowerShell's break) and you have next (equivalent to PowerShell's continue). You also have the redo which goes back to the top of the loop, but doesn't increment the for loop counter.
Is there an equivalent Powershell command to Perl's redo function for the for loop?
I can easily convert the statement into a while loop to get around this particular issue, but I was wondering if there is something like redo anyway.
I'm pretty sure PowerShell doesn't support what you're looking for. You can have labelled break and continue statements but neither will take you to the top of the inside of the associated loop/switch construct. If PowerShell had a goto statment you'd be set but I'm glad it doesn't have that keyword. :-) For more info on labelled break/continue see the about_break help topic.
An equivalent of Perl's redo would be nice, and I hope they introduce it in a future version, but mainly because it would be useful in Foreach-Object, foreach, and while loops.
However, in a for loop, at least a typical one, you can easily reproduce the functionality by simply reversing the incrementation immediately before the continue statement.
In this case, since the increment statement (aka <repeat> in the PowerShell documentation) is $color++, just stick $color-- immediately before continue. For clarity/elegance, I'd suggest putting them on one line to make it clearer that it's essentially a single loop control contsruct:
$color--; continue
The only reasons I can think of why that would ever not work:
You're otherwise manipulating the loop variable inside the loop. But I think that's terrible programming style and anyone who's doing that deserves to have their script break. ;)
You have a condition that depends on some other variable that can change within the loop, e.g. for ($i = 0; $i -lt $j; $i++) where $j can change within the loop.

How to process a file in PowerShell line-by-line as a stream

I'm working with some multi-gigabyte text files and want to do some stream processing on them using PowerShell. It's simple stuff, just parsing each line and pulling out some data, then storing it in a database.
Unfortunately, get-content | %{ whatever($_) } appears to keep the entire set of lines at this stage of the pipe in memory. It's also surprisingly slow, taking a very long time to actually read it all in.
So my question is two parts:
How can I make it process the stream line by line and not keep the entire thing buffered in memory? I would like to avoid using up several gigs of RAM for this purpose.
How can I make it run faster? PowerShell iterating over a get-content appears to be 100x slower than a C# script.
I'm hoping there's something dumb I'm doing here, like missing a -LineBufferSize parameter or something...
If you are really about to work on multi-gigabyte text files then do not use PowerShell. Even if you find a way to read it faster processing of huge amount of lines will be slow in PowerShell anyway and you cannot avoid this. Even simple loops are expensive, say for 10 million iterations (quite real in your case) we have:
# "empty" loop: takes 10 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) {} }
# "simple" job, just output: takes 20 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i } }
# "more real job": 107 seconds
measure-command { for($i=0; $i -lt 10000000; ++$i) { $i.ToString() -match '1' } }
UPDATE: If you are still not scared then try to use the .NET reader:
$reader = [System.IO.File]::OpenText("my.log")
try {
for() {
$line = $reader.ReadLine()
if ($line -eq $null) { break }
# process the line
$line
}
}
finally {
$reader.Close()
}
UPDATE 2
There are comments about possibly better / shorter code. There is nothing wrong with the original code with for and it is not pseudo-code. But the shorter (shortest?) variant of the reading loop is
$reader = [System.IO.File]::OpenText("my.log")
while($null -ne ($line = $reader.ReadLine())) {
$line
}
System.IO.File.ReadLines() is perfect for this scenario. It returns all the lines of a file, but lets you begin iterating over the lines immediately which means it does not have to store the entire contents in memory.
Requires .NET 4.0 or higher.
foreach ($line in [System.IO.File]::ReadLines($filename)) {
# do something with $line
}
http://msdn.microsoft.com/en-us/library/dd383503.aspx
If you want to use straight PowerShell check out the below code.
$content = Get-Content C:\Users\You\Documents\test.txt
foreach ($line in $content)
{
Write-Host $line
}