Powershell Countdown

Ich habe mehrere Skripte, die die gleichen Aufgaben immer wieder wiederholen. Das geht mit einer "while ($true){# befehl }"-Schleife sehr einfach. Aber ich möchte so eine Schleife zum einen auch sauber beenden und zudem gerne Anzeigen, wie lange das Skript wartet. Hierfür nutze ich den folgenden Code:

Die einfache Schleife

Ganz einfach ist das folgende Beispiel:

Write-host "Script gestartet"

write-host "Starte Transcript"
start-transcript

write-host "Starte Loop"
while ($true) {
   write-host "in der Loop und warte 5 Sekunden"
   Start-Sleep -seconds 5
}
write-host "Loop beendet"

write-host "Stoppe Transcript"
start-transcript
write-host "Script Beendet"

Das Script gibt jede 5 Sekunden einen Zeile aus und tut sonst nichts. Es kommt nur zum Ende, wenn Sie den Prozess abbrechen oder mit CTRL-C das Skript beenden. Speziell mit CTRL-C kann es aber passieren, dass einige Objekte und z.B. Dateisperren nicht gelöst werden. Insbesondere das gestartete Transcript wird bei so einem unsauberen Ende nicht beendet.

Abfangen von CTRL-C

Eine abgewandelte Form fängt dann CTRL-C ab, um die Objekte aufzuräumen. Zuerst verhindere ich, dass CTRL-C das Skript beendet aber dafür muss ich im Skript immer mal wieder sicherstellen, ob eine Taste gedrückt wurde. Das Script sieht dann wie folgt aus:

write-host "Script gestartet"

# ungeplanten Abbruch durch CTRL-C verhindern
[console]::TreatControlCAsInput = $true

write-host "Starte Transcript"
start-transcript

write-host "Starte Loop"
$abbruch = $false
while (!$abbruch) {
   write-host "in der Loop und warte 5 Sekunden"
   Start-Sleep -seconds 5
   if ([console]::KeyAvailable) {
      $key = [system.console]::readkey($true)
      if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
         Write-Host "CTRL-C Detected" 
         $abbruch=$true
      }
      else {
         write-host "Keypress detected $($key.keychar)"
      }
   }
}
write-host "Loop beendet"

write-host "Stoppe Transcript"
start-transcript
write-host "Script Beendet"

Es kann nicht mehr mit CTRL-C abgebrochen werden. Das bedeutet aber, dass Sie im schlimmsten Fall hier 5 Sekunden warten, ehe die Abfrage das Skript beendet.

Countdown anzeigen

Wenn ein Script wartet, z.B. mit Start-Sleep, dann ist es unhöflich eine lange Zeit ohne entsprechende Anzeige zu warten. Die einfache Form ist natürlich:

start-sleep -seconds 5

Schöner, wenngleich mit mehr Zeilen finde ich aber:

function start-countdown {
  param (
      $sleepintervalsec
   )

   foreach ($step in (1..$sleepintervalsec)) {
      write-progress -Activity "Waiting" -Status "Waiting - Press any key to stop" -SecondsRemaining ($sleepintervalsec-$step) -PercentComplete  ($step/$sleepintervalsec*100)
      start-sleep -seconds 1
   }
}

Damit sehe ich nun einen Fortschrittsbalken samt Zeitanzeige:

Wenn Sie bei einem Script die Anzeige der Fortschrittsbalken unterbinden wollen, dann hilft folgende Einstellung

$ProgressPreference = SilentlyContinue

Der Default ist einfach nur "Continue" Gerade bei Prozessen, die lange brauchen oder viele Ausgaben machen, z.B. "Invoke-Webrequest" werden damit deutlich schneller.

Countdown mit Tastaturabfrage

Nun muss ich nur noch diesen Countdown mit einer Abfrage der Tastatur verbinden und einen Tastendruck auszuwerten bzw. zurück zu geben.

function start-countdown {
  param (
      [long]$seconds ,
      [String]$message = "Waiting"
   )

   foreach ($step in (1..$seconds )) {
      write-progress -Activity $message -Status "Waiting - Press any key to stop" -SecondsRemaining ($seconds -$step) -PercentComplete  ($step/$seconds*100)
      start-sleep -seconds 1
      if ([console]::KeyAvailable) {
         [system.console]::readkey($true)
         break
      }
   }
}

Sobald nun der Countdown läuft, kann der Anwender durch den Druck einer beliebigen Taste die Funktion beenden. Das aufrufende Script ist nun natürlich in der Pflicht diese Taste abzufragen und zu bewerten. Bei Bedarf sollte das aufrufende Script natürlich auch CTRL-blockieren. Das macht die Funktion selbst nicht, um nicht in den Ablauf des eigentlichen Programs einzugreifen. Das könnte dann so aussehen

write-host "Script gestartet"

write-host "Blocking CTRL-C"
[console]::TreatControlCAsInput = $true

write-host "Starte Transcript"
start-transcript

write-host "Starte Loop"
$abbruch = $false
while (!$abbruch) {
   write-host "in der Loop und warte 5 Sekunden"
   $key = Start-countdown -seconds 5
   if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
      Write-Host "CTRL-C Detected" 
      $abbruch=$true
   }
   else {
      write-host "Keypress detected $($key.keychar)"
   }
}
write-host "Loop beendet"

write-host "Stoppe Transcript"
start-transcript

write-host "Release CTRL-C"
[console]::TreatControlCAsInput = $false

write-host "Script Beendet"

Das Skript läuft nun solange, bis mit CTRL-C der Abbruch angestoßen wird. das aufrufende Programm hat aber die komplette Kontrolle.

Sie können die Funktion "Start-Countdown" natürlich auch in eine PS1M-Datei auslagern und dynamisch einbinden oder als PS1-Datei

Weitere Links