Was ist der beste Weg, um die Ausgabe eines Befehls durch einen Pager zu leiten, wenn (und nur wenn) er zu lang ist?

10

Ich möchte in der Lage sein, einen Befehl so zu verpacken, dass er automatisch durch einen Pager geleitet wird, wenn seine Ausgabe nicht in ein Terminal passt.

Im Moment verwende ich die folgende Shell-Funktion (in zsh unter Arch Linux):

export LESS="-R"

RET="$($@)"
RET_LINES="$(echo "${RET}" | wc -l)"

if [[ $RET_LINES -ge $LINES ]]; then
  echo "${RET}" | ${PAGER:="less"}
else
  echo "${RET}"
fi

aber das überzeugt mich nicht wirklich. Gibt es einen besseren Weg (in Bezug auf Robustheit und Overhead), um das zu erreichen, was ich will? Ich bin auch offen für zsh-spezifischen Code, wenn er den Job gut macht.


Update: Seit ich diese Frage gestellt habe, habe ich eine Antwort gefunden , die eine etwas bessere - wenn auch kompliziertere - Lösung bietet, bei der die meisten $LINESZeilen gepuffert werden, bevor die Ausgabe weitergeleitet wird, lessanstatt alles zwischenzuspeichern. Leider ist das auch nicht wirklich befriedigend, da keine der beiden Lösungen lange, umbrochene Zeilen berücksichtigt. Wenn der obige Code beispielsweise in einer aufgerufenen Funktion gespeichert ist pager_wrap, dann

pager_wrap echo {1..10000}

druckt eine sehr lange Zeile nach stdout, anstatt durch einen Pager zu leiten.

AP
quelle
Vermutlich werden wc -lZeilenumbrüche gezählt, aber was ist, wenn der Inhalt nur aus einer Zeile besteht, aber genug Wraps enthält, um das Terminal zu verlassen?
@TessellatingHeckler Das ist ein guter Punkt, aber ich weiß nicht, wie ich (effizient) nach langen Schlangen suchen soll. Ich mag auch nicht die Tatsache, dass dieser Code die gesamte Ausgabe in einer Variablen speichert, anstatt höchstens $LINESZeilen zu puffern , aber ich weiß auch nicht, wie ich das beheben soll ...
AP
1
Was ist das eigentliche Problem, das Sie lösen möchten? Wären Sie vielleicht damit einverstanden, Befehle immer weiterzuleiten, lesswenn sie automatisch beendet werden, wenn der Text auf einem Bildschirm angezeigt wird und kein Paging erforderlich ist und der Bildschirm beim Beenden nicht gelöscht wird? Sie können das tun - superuser.com/a/106644/67909
@TessellatingHeckler Das wäre in Ordnung, solange es sich nicht um eine vorherige Terminalausgabe handelt. Ich habe über Piping to nachgedacht less -F, aber ich habe es verworfen, weil es die Piping- Ausgabe nicht zurückdruckt. Hast du eine bessere Idee? Jedenfalls habe ich derzeit zwei Anwendungsfälle: Umwickeln ls -lund Umwickeln pacman -Ss [foo].
AP

Antworten:

6

Ich habe eine Lösung, die für die POSIX-Shell-Konformität geschrieben wurde, aber ich habe sie nur in Bash getestet, daher weiß ich nicht genau, ob sie portabel ist. Und ich kenne zsh nicht, also habe ich keinen Versuch unternommen, es zsh-freundlich zu machen. Sie leiten Ihren Befehl hinein; Das Übergeben eines Befehls als Argument (e) an einen anderen Befehl ist ein schlechtes Design * .

Natürlich muss jede Lösung für dieses Problem wissen, wie viele Zeilen und Spalten das Terminal hat. Im folgenden Code habe ich angenommen, dass Sie sich auf die Variablen LINESund COLUMNSumgebungsvariablen verlassen können less. Zuverlässigere Methoden sind:

  • Verwenden Sie rows="${LINES:=$(tput lines)}" und cols="${COLUMNS:=$(tput cols)}", wie von AP vorgeschlagen , oder
  • Schauen Sie sich die Ausgabe von an stty size. Beachten Sie, dass dieser Befehl das Terminal als Standardeingabe haben muss. Wenn es sich also in einem Skript befindet und Sie in das Skript einleiten, müssen Sie stty size <&1(in Bash) oder sagen stty size < /dev/tty. Die Erfassung der Ausgabe ist noch komplizierter.

Die geheime Zutat: Der foldBefehl unterbricht lange Zeilen wie der Bildschirm, sodass das Skript lange Zeilen korrekt verarbeiten kann.

#!/bin/sh
buffer=$(mktemp)
rows="$LINES"
cols="$COLUMNS"
while true
do
      IFS= read -r some_data
      e=$?        # 1 if EOF, 0 if normal, successful read.
      printf "%s" "$some_data" >> "$buffer"
      if [ "$e" = 0 ]
      then
            printf "\n" >> "$buffer"
      fi
      if [ $(fold -w"$cols" "$buffer" | wc -l) -lt "$rows" ]
      then
            if [ "$e" != 0 ]
            then
                  cat "$buffer"
            else
                  continue
            fi
      else
            if [ "$e" != 0 ]
            then
                  "${PAGER:="less"}" < "$buffer"
                  # The above is equivalent to
                  # cat "$buffer"   | "${PAGER:="less"}"
                  # … but that’s a UUOC.
            else
                  cat "$buffer" - | "${PAGER:="less"}"
            fi
      fi
      break
done
rm "$buffer"

So verwenden Sie:

  • Legen Sie das oben genannte in eine Datei; Nehmen wir an, Sie nennen es mypager.
  • (Optional) Legen Sie es in einem Verzeichnis ab, das Ihr Suchpfad ist. zB , $HOME/bin.
  • Machen Sie es durch Eingabe ausführbar chmod +x mypager.
  • Verwenden Sie es in Befehlen wie ps ax | mypageroder ls -la | mypager.
    Wenn Sie den zweiten Schritt übersprungen haben (das Skript in ein Verzeichnis stellen, das Ihr Suchpfad ist), müssen Sie Folgendes tun , wobei ein relativer Pfad wie " " sein kann.ps ax | path_to_mypager/mypagerpath_to_mypager.

* Warum ist die Übergabe eines Befehls als Argument (e) an einen anderen Befehl ein schlechtes Design?

I. Ästhetik / Konformität mit Traditionen / Unix-Philosophie

Unix hat eine Philosophie von Do eine Sache und tun es auch . Wenn ein Programm beispielsweise Daten auf eine bestimmte Weise anzeigt (wie es Pager tun), sollte es nicht auch den Mechanismus aufrufen, der die Daten erzeugt. Dafür sind Pfeifen da.

Nicht viele Unix-Programme führen benutzerdefinierte Befehle oder Programme aus. Schauen wir uns einige an, die dies tun:

  • Die Shell ist wie in Well das Ausführen von benutzerdefinierten Befehlen die Aufgabe der Shell . Es ist das Eine , was die Shell tut. (Natürlich sage ich nicht, dass die Shell ein einfaches Programm ist.)sh -c "command"
  • env, nice, nohup, setsid, su, Und sudo. Diese Programme haben etwas gemeinsam - sie sind alle vorhanden, um ein Programm mit einer geänderten Ausführungsumgebung 1 auszuführen . Sie müssen so arbeiten, wie sie es tun, da Sie mit Unix im Allgemeinen nicht die Ausführungsumgebung eines anderen Prozesses ändern können. Sie müssen Ihren eigenen Prozess ändern und dann forkund / oder exec.
    _______
    1  Ich verwende den Begriff Ausführungsumgebung im weiteren Sinne, die sich auf Umgebungsvariablen nicht nur, aber auch Prozessattribute wie „ nice“ Wert, UID und GIDs, Prozessgruppe, Sitzungs - ID, Steueranschluss, offene Dateien, Arbeitsverzeichnis , umaskWert, ulimits, Signal Dispositionen,alarm Timer usw.
  • Programme, die ein "Shell-Escape" ermöglichen. Das einzige Beispiel, das mir in den Sinn kommt , ist vi/ vim, obwohl ich mir ziemlich sicher bin, dass es noch andere gibt. Dies sind historische Artefakte. Sie sind älter als Fenstersysteme und sogar die Jobkontrolle. Wenn Sie eine Datei bearbeitet haben und etwas anderes tun möchten (z. B. eine Verzeichnisliste anzeigen), müssten Sie Ihre Datei speichern und den Editor verlassen, um zu Ihrer Shell zurückzukehren. Heutzutage können Sie zu einem anderen Fenster wechseln oder Ctrl+ Z(oder Typ :suspend) verwenden, um zu Ihrer Shell zurückzukehren, während Ihr Editor am Leben bleibt, sodass Shell-Escapezeichen möglicherweise veraltet sind.

Ich zähle keine Programme, die andere (fest codierte) Programme ausführen, um ihre Fähigkeiten zu nutzen, anstatt sie zu duplizieren. Beispielsweise können einige Programme diffoder ausführen sort. (Zum Beispiel gibt es Geschichten, die in früheren Versionen spell verwendet wurden sort -u, um eine Liste der in einem Dokument verwendeten Wörter abzurufen und diese Liste dann diff- oder vielleicht comm- mit der Wörterbuch-Wörterbuchliste zu vergleichen und festzustellen, welche Wörter aus dem Dokument nicht enthalten waren das Wörterbuch.)

II. Zeitprobleme

So wie Ihr Skript geschrieben ist, wird die RET="$($@)"Zeile erst abgeschlossen, wenn der aufgerufene Befehl abgeschlossen ist. Daher kann Ihr Skript erst dann mit der Anzeige von Daten beginnen, wenn der Befehl, der sie generiert, abgeschlossen wurde. Der wahrscheinlich einfachste Weg, dies zu beheben, besteht darin, den Befehl zur Datengenerierung vom Datenanzeigeprogramm zu trennen (obwohl es andere Möglichkeiten gibt).

III. Befehlsverlauf

  1. Angenommen, Sie führen einen Befehl mit einer Ausgabe aus, die von Ihrem Anzeigefilter verarbeitet wird, und Sie sehen sich die Ausgabe an und entscheiden, dass Sie diese Ausgabe in einer Datei speichern möchten. Wenn Sie getippt hatten (als hypothetisches Beispiel)

    ps ax | mypager

    Sie können dann eingeben

    !:1 > myfile

    oder drücken und bearbeiten Sie die Zeile entsprechend. Nun, wenn Sie getippt hätten

    mypager "ps ax"

    Sie können immer noch zurückgehen und diesen Befehl bearbeiten ps ax > myfile, aber es ist nicht so einfach.

  2. Oder nehmen Sie an, Sie möchten als ps uaxNächstes ausgeführt werden. Wenn Sie getippt hätten ps ax | mypager, könnten Sie tun

    !:0 u!:*

    Auch hier ist mypager "ps ax"es immer noch machbar, aber wohl schwieriger.

  3. Schauen Sie sich auch die beiden Befehle an: ps ax | mypagerund mypager "ps ax". Angenommen, Sie führen eine historyStunde später eine Liste aus. ISTM, das Sie sich mypager "ps ax"etwas genauer ansehen müssen, um zu sehen, welcher Befehl ausgeführt wird.

IV. Komplexe Befehle / Anführungszeichen

  1. echo {1..10000}ist offensichtlich nur ein Beispielbefehl; ps axist nicht viel besser. Was ist, wenn Sie wollen einfach nur ein etwas tun , wenig etwas realistischer, wie ps ax | grep oracle? Wenn Sie tippen

    mypager ps ax | grep oracle

    es läuft mypager ps ax und leitet die Ausgabe von diesem durch grep oracle. Wenn also die Ausgabe von ps ax30 Zeilen lang ist, mypagerwird sie aufgerufen less, auch wenn die Ausgabe von ps ax | grep oraclenur 3 Zeilen umfasst. Es gibt wahrscheinlich Beispiele, die dramatischer scheitern werden.

    Sie müssen also das tun, was ich zuvor gezeigt habe:

    mypager "ps ax | grep oracle"

    Aber damit RET="$($@)"kann ich nicht umgehen. Es gibt natürlich Möglichkeiten, mit solchen Dingen umzugehen, aber sie werden entmutigt.

  2. Was ist, wenn die Befehlszeile, deren Ausgabe Sie erfassen möchten, noch komplizierter ist? z.B,

    Befehl 1   " arg 1 " |   Befehl 2   ' arg 2 ' $ ' arg 3 '

    wobei die Argumente enthalten unordentlich Kombinationen von Raum, Registerkarte $, |, \, <, >, *, ;, &, [, ], (, ), `, und vielleicht sogar 'und ". Ein solcher Befehl kann schwierig genug sein, um direkt in die Shell zu tippen. Stellen Sie sich nun den Albtraum vor, ihn zitieren zu müssen, um ihn als Argument weiterzugeben mypager.

G-Man sagt "Reinstate Monica"
quelle
Das ist großartig, danke! Seltsamerweise musste ich rows="${LINES:=$(tput lines)}"und verwenden cols="${COLUMNS:=$(tput cols)}", da das Skript auf keine der Umgebungsvariablen zugreifen kann, obwohl es beispielsweise echo $LINESwie von der CLI erwartet funktioniert. Ich habe auch die printf "%s" "$e"Zeile auskommentiert. Ich habe nur noch ein paar Fragen: 1. Können Sie erklären, warum das Übergeben eines Befehls als Argument (e) an einen anderen Befehl ein schlechtes Design ist? Ich verstehe, dass es besser ist, eine Gabel zu vermeiden, aber gibt es andere Gründe?
AP
(Fortsetzung) 2. Warum ruft Ihr Skript fold -w"$cols" "$buffer" | wc -ljedes Mal auf, anstatt beispielsweise eine laufende Zählung von zu führen fold -w"$cols" "$some_data" | wc -l?
AP
(0) Ja, tut mir leid wegen dem printf "%s" "$e". Das war nur eine Debug-Anweisung, die ich nicht in meiner Antwort veröffentlichen wollte. (1) Ich habe Nummer 1 in der Antwort angesprochen. (2) Nun, fold -w"$cols" "$some_data" | wc -lwürde nicht funktionieren, aber ich denke, ich hätte es tun können printf "%s" "$some_data" | fold -w"$cols" | wc -l, und ehrlich gesagt habe ich nicht daran gedacht. Ich glaube, dass mein Ansatz etwas einfacher ist, aber Ihre Idee wäre effizienter, besonders wenn sie $rowsgroß ist. (3) Entschuldigen Sie, dass Sie so lange gebraucht haben, um zu antworten. Ein Truthahn überquerte die Straße, und ich musste herausfinden, warum. :-) ⁠
G-Man sagt 'Reinstate Monica'
Kein Problem. Vielen Dank, dass Sie sich die Zeit genommen haben, die Anschlussfragen zu beantworten.
AP
1
@grundic Ich habe meine Antwort mit einer Erklärung aktualisiert. Wenn Sie immer noch nicht verstehen, beschreiben Sie genau, was Sie verwirrt hat.
G-Man sagt "Reinstate Monica"
3

Dafür ist die -FOption vorgesehen less, obwohl Sie diese -XOption auch verwenden müssen. Andernfalls wird der Text auf Terminals mit einem Terminal auf dem alternativen Bildschirm gedruckt (was bedeutet, dass er nach dem Beenden nicht sofort verfügbar ist less). Dies könnte sich in Zukunft ändern, da derzeit eine Erweiterungsanforderung für -X impliziert wird, wenn der Text mit -F(303) auf einen Bildschirm passt, und RedHat-Systeme offenbar seit 2008 einen Patch dafür haben (obwohl er noch nicht vorgelagert wurde ( Ab dem 14.09.2017 habe ich gerade eine E-Mail an [email protected] gesendet.

Damit:

cmd | less -RXF

Wenn Sie den alternativen Bildschirm immer noch verwenden möchten, wenn die Ausgabe zu lang ist, müssen Sie sich etwas einfallen lassen (auf Systemen, auf denen der oben erwähnte RedHat-Patch nicht vorhanden ist):

page() {
  L=${LINES:-$(tput lines)} C=${COLUMNS:-$(tput cols)} \
    perl -Mopen=locale -MText::Tabs -MText::CharWidth=mbswidth -e '
      while(<STDIN>) {
        if ($pager) {
          print $pager $_;
        } else {
          chomp(my $line = $_);
          $line =~ s/\e\[[\d;]*m//g;
          $l += 1 + int(mbswidth(expand($line)) / $ENV{C});
          $buf .= $_;
          if ($l > $ENV{L}) {
            open $pager, "|-", "less", "-R", @ARGV or die "pager: $!";
            print $pager $buf;
          }
        }
      }
      print $buf unless $pager;' -- "$@"
}

Zu verwenden als:

cmd | page

oder

page < file
page -S < file...

(nicht page file, es ist nur dazu gedacht, stdin zu paginieren).

Wir versuchen, die Länge der Ausgabe zu erraten, indem wir die Farb-Escape-Sequenzen entfernen, die Registerkarten erweitern und die Breite berechnen, damit wir die Anzahl der Terminalzeilen bestimmen können, die eine bestimmte Textzeile anzeigen sollen.

Das sollte funktionieren, solange die Ausgabe keine anderen Escape-Sequenzen oder Steuer- / schlecht codierten Zeichen enthält.

Beachten Sie auch einen signifikanten Unterschied zum RedHat-Patch: Bei der Ausgabe auf einem Bildschirm wird die Ausgabe nicht lessnachbearbeitet (wie das Rendern von Steuerzeichen wie ^Xim umgekehrten Video, das Zusammendrücken von Leerzeilen mit -s...). Das ist zwar näher an dem, was hier gefragt wird, aber in der Praxis ist dies möglicherweise weniger wünschenswert.

Möglicherweise müssen Sie das Text :: CharWidth-Modul installieren, das nicht zu den Standardmodulen gehört ( libtext-charwidth-perlPaket unter Debian).

Stéphane Chazelas
quelle
1
hmm, auf meinem Computer wird ls | less -Fautomatisch der alternative Bildschirm verwendet, wenn die Ausgabe zu lang ist, und nicht, wenn dies nicht der Fall ist. Vermisse ich etwas
Sourcejedi
@sourcejedi, nicht für mich, auch nicht mit der neuesten 487-Version. Haben Sie eine $LESSVariable mit zusätzlichen Optionen?
Stéphane Chazelas
@sourcejedi, siehe Bearbeiten und Pager "weniger": --quit-if-one-Bildschirm ohne --no-init und weniger --quit-if-one-Bildschirm ohne --no-init, was zu pkgs.fedoraproject.org führt /cgit/rpms/less.git/tree/… . Verwenden Sie eine RedHat-Distribution?
Stéphane Chazelas