Wie bringe ich HEREDOC-Text in eine Shell-Skriptvariable?

9

Ich versuche, HEREDOC-Text auf POSIX-kompatible Weise in eine Shell-Skriptvariable zu bringen. Ich habe es so versucht:

#!/bin/sh

NEWLINE="
"

read_heredoc2() {
  while IFS="$NEWLINE" read -r read_heredoc_line; do
    echo "${read_heredoc_line}"
  done
}

read_heredoc2_result="$(read_heredoc2 <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|



HEREDOC
)"

echo "${read_heredoc2_result}"

Das ergab Folgendes, was falsch ist:

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _  | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|

Folgendes funktioniert, aber ich mag es nicht, wie klobig es ist, eine zufällige Ausgabevariable zu verwenden:

#!/bin/sh

NEWLINE="
"

read_heredoc1() {
  read_heredoc_first=1
  read_heredoc_result=""
  while IFS="$NEWLINE" read -r read_heredoc_line; do
    if [ ${read_heredoc_first} -eq 1 ]; then
      read_heredoc_result="${read_heredoc_line}"
      read_heredoc_first=0
    else
      read_heredoc_result="${read_heredoc_result}${NEWLINE}${read_heredoc_line}"
    fi
  done
}

read_heredoc1 <<'HEREDOC'

                        _                            _ _            
                       | |                          | (_)           
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___ 
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |                                                
            |___/|_|                                                



HEREDOC

echo "${read_heredoc_result}"

Richtige Ausgabe:

                        _                            _ _            
                       | |                          | (_)           
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___ 
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |                                                
            |___/|_|                                                

Irgendwelche Ideen?

Kevin
quelle
Wenn das Banner nur einmal verwendet wird, verwenden Sie es catdirekt mit einem Here-Dokument. Wenn es an vielen Stellen im Skript verwendet wird, speichern Sie es in einer Datei catund von dort aus, genau wie /etc/motdes auf einigen Systemen verwendet wird.
Kusalananda
1
Beachten Sie, dass das Problem , das Sie ist eigentlich ein Bug Bash - Sie bereits eine POSIX - Lösung hatte in der ursprünglichen Versuch, die fein arbeiten in ksh, dash, ash, und die älteste Bourne - Shell ich finden kann. Das Parsen von Bash-Befehlssubstitutionen ist seltsam und war früher noch fehlerhafter.
Michael Homer
@ MichaelHomer Oh, interessant! Wow, ich bin auf der neuesten Fedora 25, Bash 4.3.43-4.fc25
Kevin
1
Ja. Wenn Sie eine 3er-Version erhalten, mit der Sie das Skript ausprobieren können, wird es beim ersten Backtick noch früher unterbrochen, sodass es sich verbessert. Ich bin nicht sicher, ob sie es für einen Fehler halten - wohl verbietet POSIX dieses Verhalten nicht explizit , aber es ist ziemlich klar, dass die Befehlssubstitution alle Zeichen enthält, bis die )und die zitierten Heredocs keine Erweiterungen mehr haben.
Michael Homer

Antworten:

11

Das Problem ist, dass in Bash Inside- $( ... )Escape-Sequenzen (und andere) analysiert werden, obwohl der Heredoc sie selbst nicht haben würde. Sie erhalten eine doppelte Linie, weil \der Zeilenumbruch entgeht. Was Sie sehen, ist wirklich ein Analyseproblem in Bash - andere Shells tun dies nicht. Backticks können auch in älteren Versionen ein Problem sein. Ich habe bestätigt, dass dies ein Fehler in Bash ist und in zukünftigen Versionen behoben wird.

Sie können Ihre Funktion zumindest drastisch vereinfachen:

func() {
    res=$(cat)
}
func <<'HEREDOC'
...
HEREDOC

Wenn Sie die Ausgangsvariable auswählen möchten, kann diese parametriert werden:

func() {
    eval "$1"'=$(cat)'
}
func res<<'HEREDOC'
...
HEREDOC

Oder eine ziemlich hässliche ohne eval:

{ res=$(cat) ; } <<'HEREDOC'
...
HEREDOC

Die {}werden eher benötigt als (), damit die Variable danach verfügbar bleibt.

Je nachdem, wie oft und zu welchem ​​Zweck Sie dies tun, bevorzugen Sie möglicherweise die eine oder andere dieser Optionen. Der letzte ist der prägnanteste für eine einmalige.


Wenn Sie in der Lage sind, zu verwenden zsh, funktioniert Ihre ursprüngliche Befehlsersetzung + heredoc wie sie ist, aber Sie können all dies auch weiter unten reduzieren:

x=$(<<'EOT'
...
EOT
)

Bash unterstützt dies nicht und ich glaube auch nicht, dass eine andere Shell, bei der das Problem auftritt, dies tut.

Michael Homer
quelle
Ein Problem, das ich $(cat)finde , ist das Entfernen von nachgestellten Zeilenumbrüchen
Kevin
1
@ Kevin Könnte die Problemumgehung hier helfen?
Eliah Kagan
@EliahKagan Danke, das könnte helfen, ich werde die ausprobieren.
Kevin
@EliahKagan Ich habe unten eine neue Antwort hinzugefügt.
Kevin
5

Über die OP-Lösung:

  • Sie benötigen keine Auswertung, um eine Variable zuzuweisen, wenn Sie die Verwendung einer konstanten Variablen zulassen.

  • Die allgemeine Struktur des Aufrufs einer Funktion, die das HEREDOC empfängt, könnte ebenfalls implementiert werden.

Eine Lösung, die in allen (vernünftigen) Schalen mit beiden gelösten Elementen funktioniert, ist folgende:

#!/bin/bash
nl="
"

read_heredoc(){
    var=""
    while IFS="$nl" read -r line; do
        var="$var$line$nl"
    done 
}


read_heredoc <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|

HEREDOC

read_heredoc2_result="$str"

printf '%s' "${read_heredoc2_result}"

Eine Lösung für die ursprüngliche Frage.

Eine Lösung, die seit Bash 2.04 (und den neuesten Versionen zsh, lksh, mksh) funktioniert.
Unten finden Sie eine portablere Version (POSIX).

#!/bin/bash
read_heredoc() {
    IFS='' read -d '' -r var <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|



HEREDOC

}

read_heredoc
echo "$var"

Der Kernbefehl

IFS='' read -d '' -r var <<'HEREDOC'

funktioniert wie folgt:

  1. Das Wort HEREDOCwird (einfach) in Anführungszeichen gesetzt, um eine Erweiterung des folgenden Textes zu vermeiden.
  2. Der Inhalt "hier doc" wird im stdin mit bereitgestellt <<.
  3. Die Option -d ''zwingt readdazu, den gesamten Inhalt des "here doc" zu schlürfen.
  4. Die -rOption vermeidet die Interpretation von Zeichen mit Backslash-Anführungszeichen.
  5. Der Kernbefehl ähnelt read var.
  6. Und das letzte Detail ist IFS='', dass vermieden wird, dass beim Lesen führende oder nachfolgende Zeichen im Standard-IFS entfernt werden : spacetabnewline.

In ksh -d ''funktioniert der Nullwert für die Option nicht.
Als Problemumgehung -d $'\r'funktioniert a, wenn der Text keinen "Wagenrücklauf" enthält (wenn $'\r'am Ende jeder Zeile natürlich a hinzugefügt wird).


Eine zusätzliche Anforderung (in Kommentaren) besteht darin, eine POSIX-kompatible Lösung zu generieren.

POSIX

Erweiterung der Idee, damit sie nur mit POSIX-Optionen ausgeführt wird.
Das heißt vor allem nein -dfür read. Das erzwingt einen Lesevorgang für jede Zeile.
Dies erzwingt wiederum die Notwendigkeit, jeweils eine Linie zu erfassen.
Um vareine nachfolgende neue Zeile zu erstellen, muss diese hinzugefügt werden (da der Lesevorgang sie entfernt hat).

#!/bin/sh

nl='
'

read_heredoc() {
    unset var
    while IFS="$nl" read -r line; do
        var="$var$line$nl"
    done <<\HEREDOC

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \ 
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/ 
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___| 
             __/ | | 
            |___/|_| 



HEREDOC

}

read_heredoc
printf '%s' "$var"

Das funktioniert (und wurde getestet) in allen vernünftigen Schalen.

Isaac
quelle
2

Nutzloser Gebrauch von Katze (Zitat \ und `):

myplaceonline="
                       _                            _ _            
 _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | (_)_ __   ___ 
| '_ \` _ \\| | | | '_ \\| |/ _\` |/ __/ _ \\/ _ \\| '_ \\| | | '_ \\ / _ \\
| | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
|_| |_| |_|\\__, | .__/|_|\\__,_|\\___\\___|\\___/|_| |_|_|_|_| |_|\\___|
       |___/|_
"

Oder ohne zu zitieren:

myplaceonline="$(figlet myplaceonline)"
ctx
quelle
Letzteres ist nicht POSIX.
Phk
Sie haben Recht, jetzt sollte es sein?
Ctx
1
@ctx Danke für die Antwort, aber eine nicht angegebene Anforderung von mir ist, dass ich in der Lage sein möchte, nicht zitierte HEREDOCs einzubringen (teilweise, weil ich dies zu einer öffentlichen API-Funktion für eine Bibliothek namens posixcube mache). Die akzeptierte Antwort ist letztendlich richtig, dass es einen Bash-Fehler gibt, der verschachtelte HEREDOCs in der Befehlsersetzung verarbeitet (siehe den Kommentar von Michael Homer in der Frage, die auf eine andere Frage verweist, die auf die Bash-Mailingliste verweist, in der der Fehler von einem Bash-Betreuer bestätigt wird). . Meine Antwort oben am 29. Januar ist meine Problemumgehungslösung und funktioniert gut.
Kevin
1

Um nachfolgende Zeilenumbrüche zu unterstützen, habe ich die Antwort von @MichaelHomer mit meiner ursprünglichen Lösung kombiniert. Ich habe die vorgeschlagenen Problemumgehungen aus dem von @EliahKagan angegebenen Link nicht verwendet, da der erste magische Zeichenfolgen verwendet und die letzten beiden nicht POSIX-kompatibel waren.

#!/bin/sh

NEWLINE="
"

read_heredoc() {
  read_heredoc_result=""
  while IFS="${NEWLINE}" read -r read_heredoc_line; do
    read_heredoc_result="${read_heredoc_result}${read_heredoc_line}${NEWLINE}"
  done
  eval $1'=${read_heredoc_result}'
}

read_heredoc heredoc_str <<'HEREDOC'

                        _                            _ _
                       | |                          | (_)
  _ __ ___  _   _ _ __ | | __ _  ___ ___  ___  _ __ | |_ _ __   ___
 | '_ ` _ \| | | | '_ \| |/ _` |/ __/ _ \/ _ \| '_ \| | | '_ \ / _ \
 | | | | | | |_| | |_) | | (_| | (_|  __/ (_) | | | | | | | | |  __/
 |_| |_| |_|\__, | .__/|_|\__,_|\___\___|\___/|_| |_|_|_|_| |_|\___|
             __/ | |
            |___/|_|




HEREDOC

echo "${heredoc_str}"
Kevin
quelle
@sorontar Ich habe es gerade getestet und die nachfolgende neue Zeile schien mir nicht gelöscht zu sein. Ich bin mir nicht sicher, was Sie mit dem ersten Punkt über die Verwendung einer Variablen zum Lesen der ersten Zeile meinen. In Bezug auf die evalwird es nur für den Namen der "Ausgabevariablen" verwendet. Wenn wir von vertrauenswürdigen Benutzern der Funktion ausgehen, gibt es evalin diesem Beispiel weitere Probleme ?
Kevin
@sorontar Interessanter Punkt über die nachfolgende Newline. Ein Here-Dokument muss mit einer neuen Zeile enden, gefolgt vom Trennzeichen und einer neuen Zeile. Es wird jedoch nicht angegeben, ob die neue Zeile vor dem Begrenzer Teil der Zeichenfolge sein soll oder nicht: pubs.opengroup.org/onlinepubs/9699919799/utilities /…
Kevin
1
Ja, ein Here-Dokument muss mit einem Zeilenumbruch enden und Sie müssen ihn mit einem Code entfernen. Jede " textZeile muss mit einer neuen Zeile enden". Suchen Sie in Definitionen der Textdatei.
Isaac