537 Stimmen

Wie fügt man einer Shell-Skript eine Fortschrittsleiste hinzu?

Beim Skripten in bash oder einer anderen Shell in *NIX, wenn ein Befehl ausgeführt wird, der länger als ein paar Sekunden dauern wird, wird eine Fortschrittsanzeige benötigt.

Zum Beispiel das Kopieren einer großen Datei oder das Öffnen einer großen TAR-Datei.

Welche Möglichkeiten empfehlen Sie, um Fortschrittsanzeigen zu Shell-Skripts hinzuzufügen?

0 Stimmen

Siehe auch stackoverflow.com/questions/12498304/… für Beispiele der Kontrolllogik (Hintergrund einer Aufgabe und etwas tun, bis sie beendet ist).

4 Stimmen

Es gibt eine Reihe von Anforderungen, die wir beim Skripten häufig nützlich finden. Protokollierung, Anzeige des Fortschritts, Farben, schicke Ausgaben usw... Ich habe immer das Gefühl gehabt, dass es irgendeine Art von einfachem Skript-Framework geben sollte. Schließlich habe ich beschlossen, eines zu implementieren, da ich keines finden konnte. Möglicherweise finden Sie dies hilfreich. Es ist rein Bash, ich meine nur Bash. github.com/SumuduLansakara/JustBash

0 Stimmen

Sollte dies nicht verschoben werden nach unix.stackexchange.com?

787voto

Mitch Haile Punkte 11478

Sie können dies implementieren, indem Sie eine Zeile überschreiben. Verwenden Sie \r, um ohne das Schreiben von \n zum Terminal zurückzukehren.

Schreiben Sie \n, wenn Sie fertig sind, um zur nächsten Zeile zu gelangen.

Verwenden Sie echo -ne um:

  1. nicht \n zu drucken und
  2. Escape-Sequenzen wie \r zu erkennen.

Hier ist eine Demonstration:

echo -ne '#####                     (33%)\r'
sleep 1
echo -ne '#############             (66%)\r'
sleep 1
echo -ne '#######################   (100%)\r'
echo -ne '\n'

In einem Kommentar unten erwähnt puk, dass dies "fehlschlägt", wenn Sie mit einer langen Zeile beginnen und dann eine kurze Zeile schreiben möchten: In diesem Fall müssen Sie die Länge der langen Zeile überschreiben (zum Beispiel mit Leerzeichen).

25 Stimmen

Laut der echo-Manpage (zumindest auf MacOS X) verwenden sh/bash ihren eigenen integrierten echo-Befehl, der "-n" nicht akzeptiert ... daher müssen Sie am Ende des Strings \r\c hinzufügen, anstatt nur \r.

1 Stimmen

Ich glaube, du meinst, dass die Mac-Version kein -e akzeptiert? Du hast recht, dass -e anscheinend eine GNU-Erweiterung ist.

63 Stimmen

Die portable Möglichkeit, dies auszugeben, besteht darin, printf anstelle von echo zu verwenden.

77voto

fearside Punkte 697

Eine einfache Fortschrittsbalkenfunktion, die ich neulich geschrieben habe:

#!/bin/bash
# 1. Erstellen der ProgressBar-Funktion
# 1.1 Eingabe ist der aktuelleZustand($1) und der gesamtZustand($2)
function ProgressBar {
# Daten verarbeiten
    let _progress=(${1}*100/${2}*100)/100
    let _done=(${_progress}*4)/10
    let _left=40-$_done
# Fortschrittsbalken-Stringlängen erstellen
    _fill=$(printf "%${_done}s")
    _empty=$(printf "%${_left}s")

# 1.2 Fortschrittsbalken-Strings erstellen und die ProgressBar-Zeile ausgeben
# 1.2.1 Ausgabebeispiel:                           
# 1.2.1.1 Fortschritt : [########################################] 100%
printf "\rFortschritt : [${_fill// /#}${_empty// /-}] ${_progress}%%"

}

# Variablen
_start=1

# Dies dient als die "gesamtZustand" Variable für die ProgressBar-Funktion
_end=100

# Proof of concept
for number in $(seq ${_start} ${_end})
do
    sleep 0.1
    ProgressBar ${number} ${_end}
done
printf '\nFertig!\n'

Oder hol es dir von,
https://github.com/fearside/ProgressBar/

0 Stimmen

Können Sie die Zeile unter 1.2.1.1 bitte erklären? Führen Sie eine sed-Substitution mit den _fill- und _empty-Variablen durch? Ich bin verwirrt.

0 Stimmen

Statt sed zu verwenden, verwende ich die interne "Substring-Ersetzung" von bash, da dies eine einfache Aufgabe ist und ich die internen Funktionen von bash für diese Art von Arbeit bevorzuge. Der Code sieht auch schöner aus. :-) Überprüfen Sie hier tldp.org/LDP/abs/html/string-manipulation.html und suchen Sie nach Substring-Ersatz.

0 Stimmen

Und ${_fill} wird als ${_done} Anzahl von Leerzeichen zugewiesen. Das ist wunderschön. Tolle Arbeit Mann. Ich werde das auf jeden Fall in all meinen Bash-Skripten verwenden haha

58voto

Seth Wegner Punkte 585

Verwenden Sie den Linux-Befehl pv.

Es kennt die Größe nicht, wenn es sich mitten im Pipeline befindet, aber es gibt eine Geschwindigkeit und Gesamtwert, und von dort aus können Sie herausfinden, wie lange es dauern sollte und Feedback erhalten, damit Sie wissen, dass es nicht stecken geblieben ist.

51voto

Édouard Lopez Punkte 35960

Ich habe nach etwas gesucht, das sexyer ist als die ausgewählte Antwort, also habe ich mein eigenes Skript gemacht.

Vorschau

progress-bar.sh in Aktion

Quelle

Ich habe es auf GitHub progress-bar.sh hochgeladen

progress-bar() {
  local duration=${1}

    already_done() { for ((done=0; done<$elapsed; done++)); do printf ""; done }
    remaining() { for ((remain=$elapsed; remain<$duration; remain++)); do printf " "; done }
    percentage() { printf "| %s%%" $(( (($elapsed)*100)/($duration)*100/100 )); }
    clean_line() { printf "\r"; }

  for (( elapsed=1; elapsed<=$duration; elapsed++ )); do
      already_done; remaining; percentage
      sleep 1
      clean_line
  done
  clean_line
}

Verwendung

 progress-bar 100

4 Stimmen

Ich verstehe nicht, wie dies in irgendeine Verarbeitung integriert wird, bei der die Länge des Prozesses nicht bekannt ist. Wie stoppe ich den Fortschrittsbalken, wenn mein Prozess früher beendet ist, z.B. beim Entpacken einer Datei.

0 Stimmen

Ich denke, die Verwendung sollte progress-bar 100 sein.

0 Stimmen

Ein attraktiver Fortschritt in der Tat. Wie kann er mit einer Funktion verknüpft werden, die langwierige Aktionen auf entfernten Servern über ssh ausführt? Ich meine, wie ist es möglich, die Dauer eines Upgrades (zum Beispiel) auf entfernten Servern zu messen?

34voto

cprn Punkte 1265

Habe nicht gesehen, etwas ähnliches und alle benutzerdefinierten Funktionen hier scheinen auf Rendering allein so zu konzentrieren... meine sehr einfache POSIX-konforme Lösung unten mit Schritt für Schritt Erklärungen, weil diese Frage nicht trivial ist.

TL;DR

Das Rendern des Fortschrittsbalkens ist sehr einfach. Abzuschätzen, wie viel davon gerendert werden soll, ist eine andere Sache. So wird der Fortschrittsbalken gerendert (animiert) - Sie können dieses Beispiel kopieren und in eine Datei einfügen und ausführen:

#!/bin/sh

BAR='####################'   # this is full bar, e.g. 20 chars

for i in {1..20}; do
    echo -ne "\r${BAR:0:$i}" # print $i chars of $BAR from 0 position
    sleep .1                 # wait 100ms between "frames"
done
  • {1..20} - Werte von 1 bis 20
  • echo - zum Terminal drucken (d.h. zu stdout )
  • echo -n - Druck ohne neue Zeile am Ende
  • echo -e - Sonderzeichen beim Drucken interpretieren
  • "\r" - Wagenrücklauf, ein spezielles Zeichen, um zum Anfang der Zeile zurückzukehren

Man kann jeden Inhalt mit beliebiger Geschwindigkeit rendern, so dass diese Methode sehr universell ist, z.B. wird sie oft für die Visualisierung von "Hacking" in albernen Filmen verwendet, kein Scherz.

Vollständige Antwort (von Null bis zum Arbeitsbeispiel)

Das eigentliche Problem besteht darin, wie man die $i Wert, d.h. wie viel vom Fortschrittsbalken angezeigt werden soll. In dem obigen Beispiel habe ich es einfach inkrementieren lassen for Schleife, um das Prinzip zu veranschaulichen, aber eine reale Anwendung würde eine Endlosschleife verwenden und die $i Variable bei jeder Iteration. Für diese Berechnung werden folgende Bestandteile benötigt:

  1. wie viel Arbeit noch zu tun ist
  2. wie viel Arbeit bisher geleistet wurde

Im Falle von cp benötigt er die Größe einer Quelldatei und die Größe der Zieldatei:

#!/bin/sh

src="/path/to/source/file"
tgt="/path/to/target/file"

cp "$src" "$tgt" &                     # the & forks the `cp` process so the rest
                                       # of the code runs without waiting (async)

BAR='####################'

src_size=$(stat -c%s "$src")           # how much there is to do

while true; do
    tgt_size=$(stat -c%s "$tgt")       # how much has been done so far
    i=$(( $tgt_size * 20 / $src_size ))
    echo -ne "\r${BAR:0:$i}"
    if [ $tgt_size == $src_size ]; then
        echo ""                        # add a new line at the end
        break;                         # break the loop
    fi
    sleep .1
done
  • foo=$(bar) - laufen bar in einem Unterprozess und speichern dessen stdout a $foo
  • stat - Dateistatistiken drucken auf stdout
  • stat -c - einen formatierten Wert drucken
  • %s - Format für die Gesamtgröße

Bei Vorgängen wie dem Entpacken von Dateien ist die Berechnung der Quellgröße etwas schwieriger, aber immer noch so einfach wie die Ermittlung der Größe einer unkomprimierten Datei:

#!/bin/sh
src_size=$(gzip -l "$src" | tail -n1 | tr -s ' ' | cut -d' ' -f3)
  • gzip -l - Info über Zip-Archiv drucken
  • tail -n1 - mit 1 Zeile von unten arbeiten
  • tr -s ' ' - mehrere Leerzeichen in ein einziges übersetzen ("quetschen")
  • cut -d' ' -f3 - 3. durch Leerzeichen getrenntes Feld (Spalte) ausschneiden

Hier liegt der Kern des Problems, das ich bereits erwähnt habe. Diese Lösung ist immer weniger allgemein. Alle Berechnungen des tatsächlichen Fortschritts sind eng an die Domäne gebunden, die Sie zu visualisieren versuchen, sei es eine einzelne Dateioperation, ein Timer-Countdown, eine steigende Anzahl von Dateien in einem Verzeichnis, eine Operation an mehreren Dateien usw., daher können sie nicht wiederverwendet werden. Der einzige wiederverwendbare Teil ist das Rendern des Fortschrittsbalkens. Um ihn wiederzuverwenden, müssen Sie ihn abstrahieren und in einer Datei speichern (z. B. /usr/lib/progress_bar.sh ), dann definieren Sie Funktionen, die für Ihren Bereich spezifische Eingabewerte berechnen. So könnte ein verallgemeinerter Code aussehen (ich habe auch die $BAR dynamisch, weil die Leute danach gefragt haben, der Rest sollte inzwischen klar sein):

#!/bin/bash

BAR_length=50
BAR_character='#'
BAR=$(printf %${BAR_length}s | tr ' ' $BAR_character)

work_todo=$(get_work_todo)             # how much there is to do

while true; do
    work_done=$(get_work_done)         # how much has been done so far
    i=$(( $work_done * $BAR_length / $work_todo ))
    echo -ne "\r${BAR:0:$i}"
    if [ $work_done == $work_todo ]; then
        echo ""
        break;
    fi
    sleep .1
done
  • printf - ein Builtin zum Drucken von Daten in einem bestimmten Format
  • printf %50s - nichts drucken, sondern mit 50 Leerzeichen auffüllen
  • tr ' ' '#' - jedes Leerzeichen in ein Rautenzeichen übersetzen

Und so würden Sie es verwenden:

#!/bin/bash

src="/path/to/source/file"
tgt="/path/to/target/file"

function get_work_todo() {
    echo $(stat -c%s "$src")
}

function get_work_done() {
    [ -e "$tgt" ] &&                   # if target file exists
        echo $(stat -c%s "$tgt") ||    # echo its size, else
        echo 0                         # echo zero
}

cp "$src" "$tgt" &                     # copy in the background

source /usr/lib/progress_bar.sh        # execute the progress bar

Natürlich können Sie dies in eine Funktion verpacken, umschreiben, um mit gepipten Streams zu arbeiten, die geforkete Prozess-ID mit $! und übergibt sie an progress_bar.sh so könnte es erraten wie man die zu leistende Arbeit und die geleistete Arbeit berechnet, was auch immer Ihr Gift ist.

Nebenbemerkungen

Zu diesen beiden Dingen werde ich am häufigsten befragt:

  1. ${} : In den obigen Beispielen verwende ich ${foo:A:B} . Der Fachbegriff für diese Syntax lautet Parameter Erweiterung eine eingebaute Shell-Funktionalität, die es erlaubt, eine Variable (Parameter) zu manipulieren, z. B. eine Zeichenkette mit : aber auch andere Dinge zu tun - es wird keine Subshell erzeugt. Die prominenteste Beschreibung der Parametererweiterung, die mir einfällt (die nicht vollständig POSIX-kompatibel ist, aber den Leser das Konzept gut verstehen lässt), ist in der man bash Seite.
  2. $() : In den obigen Beispielen verwende ich foo=$(bar) . Es erzeugt eine separate Shell in einem Unterprozess (auch bekannt als Unterschale ), läuft die bar Befehl und weist seine Standardausgabe einem $foo variabel. Es ist nicht dasselbe wie Prozess-Substitution und es ist etwas ganz anderes als Rohr ( | ). Am wichtigsten ist, dass es funktioniert. Manche sagen, dass dies vermieden werden sollte, weil es langsam ist. Ich behaupte, dass dies hier "in Ordnung" ist, denn was immer dieser Code zu visualisieren versucht, dauert lange genug, um einen Fortschrittsbalken zu benötigen. Mit anderen Worten: Subshells sind nicht der Engpass. Der Aufruf einer Subshell erspart mir auch die Mühe, zu erklären, warum return ist nicht das, wofür die meisten Menschen es halten, was ist ein Beenden Status und warum die Übergabe von Werten aus Funktionen in Shells nicht das ist, wozu Shell-Funktionen im Allgemeinen gut sind. Um mehr über all das herauszufinden, empfehle ich wiederum die man bash Seite.

Fehlersuche

Wenn Ihre Shell tatsächlich sh anstelle von bash ausführt, oder eine wirklich alte bash, wie z.B. Standard-OSX, dann kann es sein, dass sie sich bei echo -ne "\r${BAR:0:$i}" . Der genaue Fehler ist Bad substitution . Wenn dies bei Ihnen der Fall ist, können Sie stattdessen im Kommentarbereich Folgendes angeben echo -ne "\r$(expr "x$name" : "x.\{0,$num_skip\}\(.\{0,$num_keep\}\)")" um einen portableren Posix-kompatiblen / weniger lesbaren Teilstring-Abgleich durchzuführen.

Ein vollständiges, funktionierendes /bin/sh-Beispiel:

#!/bin/sh

src=100
tgt=0

get_work_todo() {
    echo $src
}

do_work() {
    echo "$(( $1 + 1 ))"
}

BAR_length=50
BAR_character='#'
BAR=$(printf %${BAR_length}s | tr ' ' $BAR_character)
work_todo=$(get_work_todo)             # how much there is to do
work_done=0
while true; do
    work_done="$(do_work $work_done)"
    i=$(( $work_done * $BAR_length / $work_todo ))
    n=$(( $BAR_length - $i ))
    printf "\r$(expr "x$BAR" : "x.\{0,$n\}\(.\{0,$i\}\)")"
    if [ $work_done = $work_todo ]; then
        echo "\n"
        break;
    fi
    sleep .1
done

2 Stimmen

Für diejenigen, die die einfachste Lösung wollen, habe ich meine einfach mit der ersten Antwort von cprn erstellt. Es handelt sich um eine sehr einfache Fortschrittsleiste in einer Funktion, die eine dumme Proportionalitätsregel verwendet, um die Leiste zu zeichnen: pastebin.com/9imhRLYX

1 Stimmen

Es ist korrekt, wenn Sie bash verwenden und nicht sh, sonst können einige Leute eine Bad substitution auf ${BAR:0:$i} haben.

0 Stimmen

Du könntest recht haben. Heutzutage ist sh in vielen Distributionen mit bash verknüpft oder mit einem Skript, das den bash --posix Kompatibilitätsmodus ausführt, und ich vermute, dass es 2016 auf meinem System so war, als ich diese Antwort geschrieben und getestet habe. Wenn es bei dir nicht funktioniert, kannst du ${name:n:l} durch $(expr "x$name" : "x.\{0,$n\}\(.\{0,$l\}\)") ersetzen, was in jeder POSIX-Shell funktioniert (ursprünglich in ksh93 entstanden und auch in zsh, mksh und busyboxsh vorhanden ist). Ich lasse die originale Antwort jedoch aus Gründen der Lesbarkeit und weil sie in der überwiegenden Mehrheit der Fälle einfach funktionieren sollte.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X