Shell: Setzen Sie beim Ersetzen von Befehlen die Zeilenumbrüche ('\ n') fort

14

Ich möchte in der Lage sein, die genaue Ausgabe einer Befehlsersetzung zu erfassen, einschließlich der nachfolgenden neuen Zeilenzeichen .

Mir ist klar, dass sie standardmäßig entfernt werden, so dass möglicherweise einige Manipulationen erforderlich sind, um sie zu behalten, und ich möchte den ursprünglichen Beendigungscode beibehalten .

Beispiel: Ein Befehl mit einer variablen Anzahl von nachgestellten Zeilenumbrüchen und Exit-Code:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

Ich möchte etwas laufen wie:

exact_output f

Und muss die Ausgabe sein:

Output: $'\n\n'
Exit: 5

Ich interessiere mich für beide bashund POSIX sh.

Tom Hale
quelle
1
Newline ist Teil von $IFS, daher wird es nicht als Argument erfasst.
Deathgrip
4
@ Deathgrip Es hat nichts zu tun IFS(versuchen ( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" ). Nur Zeilenumbrüche werden entfernt. \tUnd `` nicht und IFShat keinen Einfluss darauf.
PSkocik
Siehe auch: tcsh bewahrt Zeilenumbrüche bei der Befehlsersetzung `...` fürtcsh
Stéphane Chazelas

Antworten:

17

POSIX-Schalen

Der übliche Trick ( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ), um die vollständige Standardausgabe eines Befehls zu erhalten, ist:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

Die Idee ist hinzuzufügen und zu ergänzen .\n. Die Befehlsersetzung entfernt nur das \n . Und du ziehst das .mit aus ${output%.}.

Beachten Sie, dass dies in anderen Shells als zshnicht funktioniert, wenn die Ausgabe NUL-Bytes enthält. Mit yashfunktioniert das nicht, wenn die Ausgabe kein Text ist.

Beachten Sie auch, dass es in einigen Gebietsschemas darauf ankommt, welches Zeichen Sie am Ende einfügen. .sollte in der Regel in Ordnung sein, aber einige andere möglicherweise nicht. Zum Beispiel x(wie in einigen anderen Antworten verwendet) oder @würde in einem Gebietsschema mit den Zeichensätzen BIG5, GB18030 oder BIG5HKSCS nicht funktionieren. In diesen Zeichensätzen endet die Codierung einer Anzahl von Zeichen im selben Byte wie die Codierung von xoder @(0x78, 0x40).

Beispielsweise ist ūin BIG5HKSCS 0x88 0x78 (und x0x78 wie in ASCII müssen alle Zeichensätze auf einem System für alle Zeichen des portablen Zeichensatzes, der englische Buchstaben @und enthält, dieselbe Codierung haben .). Also , wenn cmdwar printf '\x88'und wir eingeführt , xnachdem er, ${output%x}würde nicht abzustreifen , dass xals $outputwürde tatsächlich enthalten ū.

Die Verwendung von .stattdessen könnte theoretisch zu demselben Problem führen, wenn es Zeichen gibt, deren Codierung mit derselben Codierung endet wie ., aber da ich dies vor einiger Zeit überprüft habe, kann ich feststellen, dass keiner der Zeichensätze für die Verwendung in einem Gebietsschema in verfügbar ist Ein Debian-, FreeBSD- oder Solaris-System hat solche Zeichen, was für mich gut genug ist (und warum ich mich entschieden habe ., ist auch das Symbol, um das Ende eines Satzes auf Englisch zu markieren, scheint angebracht).

Ein korrekterer Ansatz, wie er von @Arrow diskutiert wird, besteht darin, das Gebietsschema nur für das Entfernen des letzten Zeichens ( ${output%.}) in C zu ändern, wodurch sichergestellt wird, dass nur ein Byte entfernt wird. Dies würde den Code jedoch erheblich komplizieren und möglicherweise Kompatibilitätsprobleme verursachen seine eigene.

bash / zsh Alternativen

Mit bashund zshkönnen Sie unter der Annahme, dass die Ausgabe keine NULs enthält, auch Folgendes tun:

IFS= read -rd '' output < <(cmd)

Um den Exit-Status von zu erhalten cmd, können Sie wait "$!"; ret=$?in bashaber nicht in tun zsh.

rc / es / akanaga

Der Vollständigkeit halber sei angemerkt, dass rc/ es/ akangadafür einen Operator hat. In ihnen gibt die Befehlsersetzung, ausgedrückt als `cmd(oder `{cmd}für komplexere Befehle), eine Liste zurück (durch Aufteilen auf $ifs, Leerzeichen-Tabulator-Neuzeile standardmäßig). In diesen Shells (im Gegensatz zu Bourne-ähnlichen Shells) erfolgt das Entfernen von Zeilenumbrüchen nur als Teil dieser $ifsAufteilung. Sie können also entweder $ifsdas ``(seps){cmd}Formular leeren oder das Formular verwenden, in dem Sie die Trennzeichen angeben:

ifs = ''; output = `cmd

oder:

output = ``()cmd

In jedem Fall geht der Beendigungsstatus des Befehls verloren. Sie müssten es in die Ausgabe einbetten und anschließend extrahieren, was hässlich werden würde.

Fisch

Bei Fischen erfolgt die Befehlsersetzung mit (cmd)und ohne Unterschale.

set var (cmd)

Erstellt ein $varArray mit allen Zeilen in der Ausgabe von cmdif, $IFSdie nicht leer sind, oder mit der Ausgabe von cmdstripped von bis zu einem (im Gegensatz zu allen in den meisten anderen Shells) Newline-Zeichen, wenn sie $IFSleer sind.

Darin liegt also immer noch ein Problem, (printf 'a\nb')und (printf 'a\nb\n')das gleiche gilt auch für leere $IFS.

Um das zu umgehen, war das Beste, was ich mir einfallen lassen konnte:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

Eine Alternative ist zu tun:

read -z output < (begin; cmd; set ret $status; end | psub)

Borowski-Schale

Die Bourne-Shell hat weder das $(...)Formular noch den ${var%pattern}Operator unterstützt, daher kann es sehr schwierig sein, dies zu erreichen. Ein Ansatz ist die Verwendung von eval und quoting:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

Hier erzeugen wir eine

output='output of cmd
with the single quotes escaped as '\''
';ret=X

übergeben werden an eval. Wie für den POSIX - Ansatz, wenn 'war eine jener Figuren , deren Codierung am Ende der anderen Zeichen gefunden werden können, würden wir ein Problem haben (ein viel schlimmer ein als es ein Befehl Injection - Schwachstelle werden würde), aber zum Glück, wie ., Es ist keine von denen, und diese Anführungszeichen-Technik wird im Allgemeinen von allen verwendet, die Shell-Code zitieren. (Beachten Sie, dass \das Problem besteht. Sie sollten daher nicht verwendet werden. "...") Hier verwenden wir es erst nach a, 'was in Ordnung ist.

tcsh

Siehe tcsh, um Zeilenumbrüche in der Befehlsersetzung "..." zu erhalten

(kümmert sich nicht um den Exit-Status, den Sie beheben können, indem Sie ihn in einer temporären Datei speichern ( echo $status > $tempfile:qnach dem Befehl))

Stéphane Chazelas
quelle
Danke - und vor allem für den Hinweis auf die verschiedenen Zeichensätze. Wenn in einer Variablen zshgespeichert werden NULkann, warum würde das nicht IFS= read -rd '' output < <(cmd)funktionieren? Es muss in der Lage sein, die Länge eines Strings zu speichern. ''Wird der String \0eher als 1-Byte-String als als 0-Byte-String codiert ?
Tom Hale
1
@TomHale, ja, read -d ''wird behandelt als read -d $'\0'( bashauch wenn es $'\0'das gleiche wie ''überall gibt).
Stéphane Chazelas
Sie verschmelzen Zeichen und Bytes. Bitte haben Sie Verständnis dafür, dass sich die ursprüngliche Entität nicht ändern darf, wenn wir genau das entfernen, was hinzugefügt wurde. Es ist nicht so schwierig, ein aufgerufenes Byte zu entfernen , xwenn es das ist, was hinzugefügt wurde. Bitte werfen Sie einen Blick auf meine bearbeitete Antwort.
Pfeil
@Arrow, ja der var=value command evalTrick wurde hier ( auch ) und auf der Austin-Group-Mailingliste schon mal besprochen . Sie werden feststellen, dass es nicht portabel ist (und es ist ziemlich offensichtlich, wenn Sie Dinge wie a=1 command eval 'unset a; a=2'oder noch schlimmer ausprobieren, dass es nicht für solche Zwecke gedacht ist). Das gleiche gilt für das savedVAR=$VAR;...;VAR=$savedVAR, was Sie nicht tun möchten, wenn $VARes anfangs deaktiviert war. Wenn das nur ein theoretisches Problem umgeht (ein Fehler, der in der Praxis nicht gefunden werden kann), lohnt sich die Mühe nicht. Trotzdem werde ich dich beim Ausprobieren unterstützen.
Stéphane Chazelas
Haben Sie einen Link zu der Stelle, an der Sie die Verwendung von LANG=Czum Entfernen eines Bytes aus einer Zeichenfolge verworfen haben ? Sie äußern Bedenken in Bezug auf den eigentlichen Punkt, alle sind leicht zu lösen. (1) es wird kein Unset verwendet. (2) Testen Sie die Variable, bevor Sie sie ändern. @ StéphaneChazelas
Arrow
3

Für die neue Frage funktioniert dieses Skript:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

Bei der Ausführung:

Output:$'\n\n\n'
Exit :25
Done

Die längere Beschreibung

Die übliche Weisheit für POSIX-Shells, sich mit dem Entfernen von zu befassen, \nist:

füge ein x

s=$(printf "%s" "${1}x"); s=${s%?}

Das ist erforderlich , weil die letzte neue Linie ( S ) durch den Befehl Erweiterung entfernt werden pro POSIX - Spezifikation :

Entfernen von Sequenzen eines oder mehrerer Zeichen am Ende der Ersetzung.


Über ein Trailing x.

Es wurde in dieser Frage gesagt, dass ein xmit dem nachfolgenden Byte eines Zeichens in einer Codierung verwechselt werden könnte. Aber wie sollen wir raten, welches oder welches Zeichen in einer Sprache in einer möglichen Kodierung besser ist, das ist, gelinde gesagt, eine schwierige Aussage.

Jedoch; Das ist einfach falsch .

Die einzige Regel, der wir folgen müssen, ist, genau das hinzuzufügen , was wir entfernen.

Es sollte leicht zu verstehen sein , dass , wenn wir etwas zu einer vorhandenen Zeichenfolge (oder Byte - Reihenfolge) hinzufügen und später entfernen wir genau das gleiche , etwas, die ursprüngliche Zeichenfolge (oder Byte - Reihenfolge) müssen gleich sein.

Wohin gehen wir falsch? Wenn wir Zeichen und Bytes mischen .

Wenn wir ein Byte hinzufügen, müssen wir ein Byte entfernen. Wenn wir ein Zeichen hinzufügen, müssen wir genau dasselbe Zeichen entfernen .

Die zweite Option, ein Zeichen hinzuzufügen (und später genau dasselbe Zeichen zu entfernen), kann kompliziert und kompliziert werden, und Codepages und Codierungen können im Weg stehen.

Die erste Option ist jedoch durchaus möglich, und nachdem sie erklärt wurde, wird sie ganz einfach.

Fügen wir ein Byte, ein ASCII-Byte (<127) hinzu und lassen Sie uns ein ASCII-Zeichen im Bereich von az angeben, um die Verschachtelung so gering wie möglich zu halten. Oder wie wir es sagen sollten, ein Byte im Hex-Bereich 0x61- 0x7a. Wählen wir eines davon, vielleicht ein x (wirklich ein Byte Wert 0x78). Wir können ein solches Byte hinzufügen, indem wir ein x mit einem String verketten (nehmen wir an, ein é):

$ a
$ b=${a}x

Wenn wir die Zeichenfolge als Folge von Bytes betrachten, sehen wir:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

Eine Zeichenfolge, die mit einem x endet.

Wenn wir dieses x (Byte-Wert 0x78) entfernen , erhalten wir:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

Das funktioniert ohne Probleme.

Ein etwas schwierigeres Beispiel.

Nehmen wir an, die Zeichenfolge, an der wir interessiert sind, endet in Byte 0xc3:

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

Und lassen Sie uns ein Byte Wert hinzufügen 0xa9

$ b=$a$'\xa9'

Die Zeichenfolge ist jetzt so geworden:

$ echo "$b"
a test string é

Genau das, was ich wollte, die letzten zwei Bytes sind ein Zeichen in utf8 (damit jeder diese Ergebnisse in seiner utf8-Konsole reproduzieren kann).

Wenn wir ein Zeichen entfernen, wird die ursprüngliche Zeichenfolge geändert. Aber das haben wir nicht hinzugefügt, wir haben einen Bytewert hinzugefügt, der zufällig als x geschrieben wird, aber trotzdem als Byte.

Was wir brauchen, um eine Fehlinterpretation von Bytes als Zeichen zu vermeiden. Was wir brauchen, ist eine Aktion, die das verwendete Byte entfernt 0xa9. Tatsächlich scheinen ash, bash, lksh und mksh genau das zu tun:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

Aber nicht ksh oder zsh.

Das ist jedoch sehr einfach zu lösen. Sagen wir all diesen Shells, dass sie das Entfernen von Bytes durchführen sollen:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

Das war's, alle Shells getestet Arbeit (außer Yash) (für den letzten Teil der Zeichenfolge):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

Weisen Sie die Shell einfach an, ein LC_ALL = C-Zeichen zu entfernen. Dies ist genau ein Byte für alle Bytewerte von 0x00bis 0xff.

Lösung für Kommentare:

Für das in den Kommentaren diskutierte Beispiel ist eine mögliche Lösung (die in zsh fehlschlägt):

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

Dadurch wird das Codierungsproblem behoben.

Pfeil
quelle
Gut zu wissen, dass mehr als eine nachgestellte Zeile entfernt werden kann.
Tom Hale
Ich bin damit einverstanden, dass das Festlegen des Gebietsschemas auf C, um sicherzustellen, dass ${var%?}immer ein Byte entfernt wird, theoretisch korrekter ist, aber: 1- LC_ALLund LC_CTYPEüberschreiben $LANG, so dass Sie LC_ALL=C2- festlegen müssen, können Sie das var=${var%?}in einer Subshell nicht wie die Änderung durchführen Sie müssen also den Wert und den Status von LC_ALL(oder Funktionen, die nicht im POSIX- localBereich verfügbar sind) speichern und wiederherstellen. 3- Das Ändern des Gebietsschemas in der Mitte des Skripts wird in einigen Shells wie Yash nicht vollständig unterstützt. Auf der anderen Seite ist die Verwendung in der Praxis .in Zeichensätzen des realen Lebens nie ein Problem, sodass eine Vermischung mit LC_ALL vermieden wird.
Stéphane Chazelas
2

Sie können ein Zeichen nach der normalen Ausgabe ausgeben und dann entfernen:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

Dies ist eine POSIX-kompatible Lösung.

PSkocik
quelle
Aufgrund der Antworten sehe ich, dass meine Frage unklar war. Ich habe es gerade aktualisiert.
Tom Hale