Mehrzeilige Bash-Befehle im Makefile

120

Ich habe eine sehr komfortable Möglichkeit, mein Projekt über einige Zeilen mit Bash-Befehlen zu kompilieren. Aber jetzt muss ich es über Makefile kompilieren. In Anbetracht der Tatsache, dass jeder Befehl in einer eigenen Shell ausgeführt wird, ist meine Frage, wie man einen mehrzeiligen Bash-Befehl, der voneinander abhängig ist, am besten in einem Makefile ausführt. Zum Beispiel so:

for i in `find`
do
    all="$all $i"
done
gcc $all

Kann jemand erklären, warum selbst einzeilige Befehle bash -c 'a=3; echo $a > file'im Terminal korrekt funktionieren, aber im Makefile-Fall eine leere Datei erstellen?

Jofsey
quelle
4
Der zweite Teil sollte eine separate Frage sein. Das Hinzufügen einer weiteren Frage erzeugt nur Rauschen.
10b0

Antworten:

136

Sie können einen Backslash für die Zeilenfortsetzung verwenden. Beachten Sie jedoch, dass die Shell den gesamten Befehl in einer einzigen Zeile verkettet empfängt. Sie müssen daher auch einige Zeilen mit einem Semikolon abschließen:

foo:
    for i in `find`;     \
    do                   \
        all="$$all $$i"; \
    done;                \
    gcc $$all

Wenn Sie jedoch nur die gesamte vom findAufruf zurückgegebene Liste übernehmen und an übergeben möchten, gccbenötigen Sie nicht unbedingt einen mehrzeiligen Befehl:

foo:
    gcc `find`

Oder verwenden Sie einen eher konventionellen $(command)Ansatz (beachten Sie jedoch die $Flucht):

foo:
    gcc $$(find)
Eldar Abusalimov
quelle
2
Bei Multiline müssen Sie sich immer noch den Dollarzeichen entziehen, wie in den Kommentaren an anderer Stelle angegeben.
Tripleee
1
Ja, eine kurze Antwort 1 $: = $$ 2. Append mehrzeilige mit \ BTW bash Jungs in der Regel empfehlen `find` Aufruf zu ersetzen $$ (find)
Yauhen Yakimovich
89

Wie in der Frage angegeben, wird jeder Unterbefehl in einer eigenen Shell ausgeführt . Dies macht das Schreiben von nicht trivialen Shell-Skripten etwas chaotisch - aber es ist möglich! Die Lösung besteht darin, Ihr Skript so zu konsolidieren, dass make einen einzelnen Unterbefehl (eine einzelne Zeile) berücksichtigt.

Tipps zum Schreiben von Shell-Skripten in Makefiles:

  1. Vermeiden Sie die Verwendung des Skripts $durch Ersetzen durch$$
  2. Konvertieren Sie das Skript so, dass es als einzelne Zeile funktioniert, indem Sie ;zwischen Befehle einfügen
  3. Wenn Sie das Skript in mehreren Zeilen schreiben möchten, schließen Sie das Zeilenende mit \
  4. Beginnen Sie optional mit set -eder Übereinstimmung von make, um bei einem Unterbefehlsfehler abzubrechen
  5. Dies ist völlig optional, aber Sie können das Skript mit ()oder {}um die Kohäsivität einer mehrzeiligen Sequenz zu klammern - dies ist keine typische Makefile-Befehlssequenz

Hier ist ein vom OP inspiriertes Beispiel:

mytarget:
    { \
    set -e ;\
    msg="header:" ;\
    for i in $$(seq 1 3) ; do msg="$$msg pre_$${i}_post" ; done ;\
    msg="$$msg :footer" ;\
    echo msg=$$msg ;\
    }
Brent Bradburn
quelle
4
Möglicherweise möchten Sie SHELL := /bin/bashin Ihrem Makefile auch BASH-spezifische Funktionen wie die Prozessersetzung aktivieren .
Brent Bradburn
8
Subtiler Punkt: Das Leerzeichen danach {ist entscheidend, um die Interpretation {setals unbekannter Befehl zu verhindern .
Brent Bradburn
3
Subtiler Punkt 2: Im Makefile spielt es wahrscheinlich keine Rolle, aber die Unterscheidung zwischen Wrapping mit {}und ()macht einen großen Unterschied, wenn Sie das Skript manchmal kopieren und direkt über eine Shell-Eingabeaufforderung ausführen möchten. Sie können Ihre Shell-Instanz verwüsten, indem Sie Variablen deklarieren und insbesondere den Status setinnerhalb von ändern {}. ()verhindert, dass das Skript Ihre Umgebung ändert, was wahrscheinlich bevorzugt wird. Beispiel ( dies beendet Ihre Shell-Sitzung ) : { set -e ; } ; false.
Brent Bradburn
Sie können Kommentare mithilfe des folgenden Formulars command ; ## my comment \` (the comment is between einfügen : ; `und` \ `). Dies scheint gut zu funktionieren, außer dass, wenn Sie den Befehl manuell ausführen (durch Kopieren und Einfügen), der Befehlsverlauf den Kommentar so enthält, dass der Befehl unterbrochen wird (wenn Sie versuchen, ihn wiederzuverwenden). [Hinweis: Die Syntaxhervorhebung ist für diesen Kommentar aufgrund der Verwendung von Backslash im Backtick unterbrochen.]
Brent Bradburn
Wenn Sie eine Zeichenfolge in bash / perl / script in makefile unterbrechen möchten, schließen Sie das Zeichenfolgenzitat, den Backslash, den Zeilenumbruch und das offene Zeichenfolgenzitat (ohne Einzug). Beispiel; perl -e QUOTE print 1; QUOTE BACKSLASH NEWLINE QUOTE Druck 2 QUOTE
Mosh
2

Was ist falsch daran, nur die Befehle aufzurufen?

foo:
       echo line1
       echo line2
       ....

Und für Ihre zweite Frage müssen Sie dem entkommen, $indem Sie $$stattdessen verwenden, dh bash -c '... echo $$a ...'.

BEARBEITEN: Ihr Beispiel könnte wie folgt in ein einzeiliges Skript umgeschrieben werden:

gcc $(for i in `find`; do echo $i; done)
JesperE
quelle
5
Ursache "Jeder Befehl wird in einer eigenen Shell ausgeführt", während ich das Ergebnis von Befehl1 in Befehl2 verwenden muss. Wie im Beispiel.
Jofsey
Wenn Sie Informationen zwischen Shell-Aufrufen weitergeben müssen, müssen Sie einen externen Speicher verwenden, z. B. eine temporäre Datei. Sie können Ihren Code jedoch jederzeit neu schreiben, um mehrere Befehle in derselben Shell auszuführen.
JesperE
+1, insbesondere für die vereinfachte Version. Und ist es nicht dasselbe wie nur "gcc" find ""?
Eldar Abusalimov
1
Ja, so ist es. Aber Sie könnten natürlich komplexere Dinge tun als nur "Echo".
JesperE
2

Der richtige Weg, ein Makefile zu schreiben, besteht natürlich darin, tatsächlich zu dokumentieren, welche Ziele von welchen Quellen abhängen. Im trivialen Fall wird die vorgeschlagene Lösung von sich selbst fooabhängen, ist aber natürlich makeklug genug, um eine zirkuläre Abhängigkeit aufzuheben. Wenn Sie Ihrem Verzeichnis jedoch eine temporäre Datei hinzufügen, wird diese "auf magische Weise" Teil der Abhängigkeitskette. Es ist besser, ein für alle Mal eine explizite Liste von Abhängigkeiten zu erstellen, möglicherweise über ein Skript.

GNU make weiß, wie man gcceine ausführbare Datei aus einer Reihe von .cund .hDateien erstellt, also beträgt alles, was Sie wirklich brauchen, vielleicht

foo: $(wildcard *.h) $(wildcard *.c)
Tripleee
quelle
0

Mit der ONESHELL-Direktive können mehrere Zeilenrezepte geschrieben werden, die in demselben Shell-Aufruf ausgeführt werden sollen.

SOURCE_FILES = $(shell find . -name '*.c')

.ONESHELL:
foo: ${SOURCE_FILES}
    for F in $^; do
        gcc -c $$F
    done

Es gibt jedoch einen Nachteil: Sonderpräfixzeichen ('@', '-' und '+') werden unterschiedlich interpretiert.

https://www.gnu.org/software/make/manual/html_node/One-Shell.html

Jean-Baptiste Poittevin
quelle