Damit der User bei länger dauernden Aktionen nicht die Geduld verliert, zeigen Desktop-Applikationen, Webseiten und sogar Kommandozeilen-Tools kleine Fortschrittsbalken an. Mike Schilli zeigt mehrere Programmieransätze für handgeschriebene Tools.
Nicht nur hibbelige Millennials, auch altgediente Internetnutzer verlieren die Geduld, wenn es länger dauert, bis eine Webseite im Browser erscheint. Es ist auch wirklich nervig, dass nicht klar ist, was los ist. Also erfand ein kluger Kopf vor 40 Jahren den Progress-Bar [2], der quasi beruhigend auf den User einredet: “10 Prozent sind durch, und die restlichen 90 schaffen wir auch noch, und zwar in folgendem Tempo.”
Auch Thriller aus Hollywood lieben Fortschrittsbalken (Abbildung 1). Wenn der Spion die Daten auf den USB-Stick abzieht, scheint der Progress-Bar nur endlos langsam voranzuschreiten, während die Bösewichte nahen.
Einige Unix-Tools bringen bereits Fortschrittsbalken mit. So lernt »curl« normalerweise beim Einholen einer Webseite ganz zu Anfang, wie viele Bytes diese enthält und zeigt mit der Option »-#« (oder »–progress-bar«) das anschließende Eintrudeln der Daten an:
$ curl -# -o data http://... ########### 50.6%
Auch das oft zum Kopieren von Diskdaten genutzte Tool »dd« zeigt neuerdings (ab Version GNU Coreutils 8.24) den Fortschritt an, wenn der User die Option »status=progress« setzt (Abbildung 2).
Abbildung 2: Seit Coreutils 8.24 zeigt das Tool »dd« mit der Option »status=progress« den Fortschritt an.
Bash & Co.
Freunde der Shellprogrammierung greifen zum Linux-Werkzeug »pv«, das auch Utilities ohne eingebauten Fortschrittsbalken auf die Sprünge hilft. Als Zwischenstück zwischen zwei Abschnitte einer Pipe geklemmt, zeigt es mittels eines Ascii-Balkens den Fortschritt der Daten durch die Pipe an, indem es die durchfließenden Bytes zählt. Damit es erfährt, welcher Bruchteil der Daten geflossen ist und was noch ansteht, um regelmäßig den Balken aufzufrischen, muss es vorab die Gesamtdatenmenge wissen. Dann ergibt sich der angezeigte Prozentwert aus der Division von mitgezählten Bytes zur Gesamtmenge.
Einfach zwischen zwei Pipe-Abschnitte eingebaut, weiß »pv« allerdings nichts über die noch zu erwartende Menge und kann demnach nur bereits geflossene Bytes zählen (Abbildung 3 oben). Wer »pv« in einem Pipe-Zwischenstück unter die Arme greifen möchte, darf die (eventuell vorab bekannte) Gesamtdatenmenge mit der Option »-s bytes« angeben, dann malt »pv« ebenfalls einen Fortschrittsbalken.
Übergibt der User »pv« allerdings den Namen einer Datei, kann es feststellen, wie groß diese ist, bevor es die Daten stückweise weiterreicht und ohne Hilfestellung den Fortschritt anzeigt wie beim letzten Backup-Kommando in Abbildung 2.
Selbst ist der User
Wer sich gerne selbst seine Tools zusammenklopft, findet Progress-Bars zu seiner verwendeten Programmiersprache oft auf Github. Für Go bietet sich dafür etwa der simple Ascii-Balken »progressbar« an, den
$ go get github.com/schollz/progressbar
direkt von Github auf den Rechner holt. Listing 1 zeigt einen einfachen Webclient »webpgb«, der eine ihm auf der Kommandozeile übergebene URL vom Netz holt und nebenher in einem Progress-Bar anzeigt, wie weit der Download schon fortgeschritten ist:
Listing 1
webpgb.go
01 package main
02
03 import (
04 "os"
05 "net/http"
06 "io"
07 pb "github.com/schollz/progressbar"
08 )
09
10 func main() {
11 resp, err := http.Get(os.Args[1])
12 buffer := make([]byte, 4096)
13
14 if err != nil {
15 panic(err)
16 }
17
18 if resp.StatusCode != 200 {
19 panic(resp.StatusCode)
20 return
21 }
22
23 bar := pb.NewOptions(
24 int(resp.ContentLength),
25 pb.OptionSetTheme(
26 pb.Theme{Saucer: "#",
27 SaucerPadding: "-",
28 BarStart: "[",
29 BarEnd: "]"}),
30 pb.OptionSetWidth(30))
31
32 bar.RenderBlank()
33
34 defer resp.Body.Close()
35
36 for {
37 n, err := resp.Body.Read(buffer)
38 if err == nil {
39 bar.Add(n)
40 } else if err == io.EOF {
41 return
42 } else {
43 panic(err)
44 }
45 }
46 }
$ ./webpgb http://... 13%[##----------][16s:1m49s]
Dazu importiert Zeile 7 die Progress-Bar-Library als »pb« ins Hauptprogramm, das mit »os.Args[1]« den ersten ihm überreichten Kommandozeilen-Parameter als URL interpretiert und mit der Get-Funktion aus dem Standard-Paket »net/http« vom Netz holt.
Zum stückweisen Einsammeln der Daten definiert Zeile 12 einen Puffer als Go-Slice von 4096 Bytes Länge, den die Endlosschleife ab Zeile 36 so lange mittels »Read()« aus der HTTP-Antwort mit bis zu 4096 Zeichen befüllt, bis der Webserver den Rollladen schließt und EOF schickt, was Zeile 40 abfängt und »main« beendet. Zwischenzeitlich frischt Zeile 39 mittels »Add()« die Anzeige des Progress-Bar auf, in dem sie ihm die jeweils im Puffer liegende Anzahl von erhaltenen Bytes schickt.
Anfangs hat Zeile 23 eine neue Balkenstruktur in der Variablen »bar« definiert und ihre Maximallänge auf die Gesamtzahl der aus dem Web-Request erwarteten Bytes initialisiert. Dabei definieren die Zeilen 24 bis 30 auch noch kosmetische Einstellungen wie das ASCII-Zeichen für die so genannte Saucer (Untertasse), das fliegende Objekt zur Illustration des Fortschritts als »#« sowie den Balkenrahmen als »[]« und das Füllzeichen des leeren Balkens als »-«.
Abbildung 4: Einer der futuristischsten Fortschrittsbalken in der Software-Industrie: »npm install«.
Da die Daten aus der Webanfrage sowieso stückweise aus dem Netz eintreffen, fügen sich der Balken und die Logik, um ihn aufzufrischen, organisch in den Code ein. Falls lange laufende Systemcalls die Laufzeit des Programms bestimmen, müssen diese umgeschrieben werden, damit ein Balken schrittweise voranschreiten kann und nicht etwa bis kurz vor Schluss stillsteht und dann ruckartig bis ans Ende schnalzt.
Retrolook
Wem nach etwas mehr Eye Candy als nur Terminal-Buchstaben gelüstet, der kann als nächste Stufe mit einem Terminal-UI, etwa dem letztens vorgestellten Termui-Projekt [3], wie in Abbildung 5 illustriert punkten. Listing 2 zeigt auch, wie Systemfunktionen beim Kopieren von Dateien und grafischer Elemente eines GUI sich verzahnen lassen.
Listing 2
cpgui.go
01 package main
02
03 import (
04 ui "github.com/gizak/termui"
05 "io/ioutil"
06 "os"
07 "fmt"
08 "log"
09 )
10
11 func main() {
12 file := os.Args[1];
13 err := ui.Init()
14 if err != nil {
15 panic(err)
16 }
17 defer ui.Close()
18
19 g := ui.NewGauge()
20 g.Percent = 0
21 g.Width = 50
22 g.Height = 7
23 g.BorderLabel = "Copying"
24 g.BarColor = ui.ColorRed
25 g.BorderFg = ui.ColorWhite
26 g.BorderLabelFg = ui.ColorCyan
27 ui.Render(g)
28
29 update := make(chan int)
30 done := make(chan bool)
31
32 // wait for completion
33 go func() {
34 <-done
35 ui.StopLoop()
36 }()
37
38 // process updates
39 go func() {
40 for {
41 g.Percent = <-update
42 ui.Render(g)
43 }
44 }()
45
46 go backup(file, fmt.Sprintf("%s.bak", file),
47 update, done)
48
49 ui.Handle("/sys/kbd/q", func(ui.Event) {
50 ui.StopLoop()
51 })
52
53 ui.Loop()
54 }
55
56 func backup(src string, dst string,
57 update chan int, done chan bool) error {
58
59 input, err := ioutil.ReadFile(src)
60 if err != nil {
61 log.Println(err)
62 done <- true
63 }
64 total := len(input)
65 total_written := 0
66
67 out, err := os.Create(dst)
68 if err != nil {
69 log.Println(err)
70 done <- true
71 }
72
73 lim := 4096
74 var chunk []byte
75
76 for len(input) >= lim {
77 chunk, input = input[:lim], input[lim:]
78 out.Write(chunk)
79 total_written += len(chunk)
80 update<- total_written*100/total
81 }
82 out.Write(input)
83
84 done <- true
85 return nil
86 }
Da GUIs und damit der Fortschrittsbalken in einer Event-Schleife laufen, bietet sich asynchrones Einholen der Daten an wie etwa in Node.js. Dort springt der Code beim Eintreffen von Daten regelmäßig Callbacks an, aus denen der findige Programmierer den Balken mit der eingetroffenen Datenmenge auffrischt.
Channels und Routines
In Go stehen zum Abfeuern nebenläufiger Programmteile und deren Synchronisation die letztens erörterten Goroutines und so genannte Channels bereit [4]. Listing 2 zeigt, wie sich der lange dauernde Kopiervorgang einer großen Datei in Go mit einem Progress-Bar aus dem Termui-Paket illustrieren lässt. Nach dem Installieren des Pakets mit
$ go get github.com/gizak/termui
und dem Übersetzen des Programms mit
$ go build cpgui.go
kopiert der Aufruf »cpgui foo« eine Datei »foo« nach »foo.bak«. Ist sie relativ groß, zeichnet das aufgespannte UI einen Progress-Bar ins Terminal, dessen Balken sich schrittweise nach rechts vergrößert, bis er am Ende des Diagramms angelangt ist, die Datei fertig kopiert ist und das Programm sich beendet (Abbildung 5).
Listing 2 definiert hierzu in Zeile 19 mit »NewGauge()« aus dem »termui«-Paket eine neue Struktur, die das UI-Element eines Fortschrittsbalkens repräsentiert. Der Anfangswert des von links nach rechts wachsenden Balkens ist mit dem Attribut »Percent« auf 0 gesetzt, der Balken steht also bei 0 Prozent und damit ganz links und ist unsichtbar. Die Zeilen 21 bis 26 definieren noch weitere Attribute wie Farben der einzelnen Elemente, ihre Größe oder die Beschriftung des Grafikelements.
Kommunikationskanäle
Zur Kommunikation zwischen den verschiedenen Programmteilen, die die Daten schreiben beziehungsweise den Balken auffrischen, nutzt Listing 2 die in den Zeilen 29 und 30 definierten Channels, »update« und »done«.
Über »update« teilt die »backup()«-Funktion (die den Channel ab Zeile 56 als Parameter mitbekommt) dem Hauptprogramm mit, wie viele Bytes sie gerade in die neue Datei geschrieben hat. »main()« wartet dazu in einer parallel laufenden Goroutine in einer Endlosschleife ab Zeile 40 auf neue Daten im Channel und blockiert, falls noch keine anliegen. Kommt ein neuer Integerwert aus dem Channel, setzt Zeile 41 das Percent-Attribut des Progress-Bar in »g« auf den neuen von »backup()« nach oben geschickten Prozentwert und zeichnet mit »ui.Render(g)« das Grafikelement neu.
Der zweite Channel (»done«) erlaubt es dann der Kopierfunktion »backup()«, die auch diesen Channel als Parameter vom Hauptprogramm mitbekommt, das Programmende einzuleiten. Hierzu wartet das Hauptprogramm in einer ab Zeile 33 gestarteten Goroutine auf Daten im »done«-Channel. Sobald welche vorliegen (weil Zeile 70 »true« hineingeschickt hat), löst Zeile 35 mit »ui.StopLoop()« den Abschluss der UI-Eventschleife ein, was in Zeile 53 das GUI einreißt und die »main()«-Funktion beendet.
Die Daten der zu kopierenden Datei liest die Funktion »ReadFile()« aus dem mit »go get io/ioutil« zu installierenden Paket »ioutil« in einem Rutsch in ein Array Slice ein, das Daten vom Typ »Byte« enthält. Die Länge der Datei in Bytes ermittelt die Funktion »len()« in Zeile 64 und speichert sie in der Variablen »total«. Zeile 67 legt die neue Datei mit der Endung ».bak« auf der Festplatte an und gibt in »out« ein Writer-Interface zurück. Dies bietet die Funktion »Write()« an, die in Zeile 78 so lange Brocken mit 4096 Bytes Länge hineinschreibt, bis alle Bytes der Originaldatei erfolgreich kopiert sind.
Den jeweils nächsten 4096 Bytes langen Brocken aus dem Puffer »input« holt sich die Anweisung
chunk, input = input[:lim], input[lim:]
in Zeile 77 in den Array Slice »chunk« und streicht die Daten gleichzeitig aus dem Original-Puffer »input«.
Die Schleife ab Zeile 76 wiederholt den Reigen, bis nur noch ein Rest mit weniger als 4096 Bytes im Array Slice »input« verbleibt, den Zeile 82 außerhalb der For-Schleife dann auch noch in die neue Datei schreibt und damit den Kopiervorgang abschließt.
Damit der User den Kopiervorgang bei Bedarf auch manuell unterbrechen kann, definiert Zeile 49 einen Tastatur-Handler für die Taste [Q], der mit »StopLoop()« das GUI zusammenfaltet und das Programm ordnungsgemäß beendet.
Am Ende des Kopiervorgangs schickt Zeile 84 einen Wert in den »done«-Channel, was dann das Hauptprogramm in Zeile 34 umgehend mitbekommt, den Block der Goroutine in Zeile 34 beendet und dann ebenfalls mit Hilfe von »ui.StopLoop()« zu einem geordneten Rückzug veranlasst.
Die Unterhaltung während des Kopiervorgangs ist allerdings teuer erkauft: Das Schreiben der Daten in 4096-Byte-Brocken bremst bei größeren Dateien erheblich. Zudem liest Listing 2 den Inhalt der zu kopierenden Datei erst mal in einem Rutsch in den Speicher, was bei Gigabyte-großen Trümmern vielleicht keine gute Idee ist. Für Hollywoods Filmindustrie reicht’s aber allemal.
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/videos/
Infos
-
Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2018/12/snapshot/
-
Fortschrittsbalken: https://de.wikipedia.org/wiki/Fortschrittsbalken
-
Michael Schilli, “Klassiker neu verpackt”: Linux-Magazin 10/18, S. 78, https://www.linux-magazin.de/ausgaben/2018/10/snapshot-7/
-
Michael Schilli, “Gleichzeitiges Arbeiten”: Linux-Magazin 11/18, S. 106, https://www.linux-magazin.de/ausgaben/2018/11/snapshot-8/









