Perintah multiline bash di makefile

120

Saya memiliki cara yang sangat nyaman untuk mengkompilasi proyek saya melalui beberapa baris perintah bash. Tapi sekarang saya perlu mengkompilasinya melalui makefile. Mempertimbangkan, bahwa setiap perintah dijalankan di cangkangnya sendiri, pertanyaan saya adalah apa cara terbaik untuk menjalankan perintah bash multi-baris, bergantung satu sama lain, di makefile? Misalnya seperti ini:

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

Juga, dapatkah seseorang menjelaskan mengapa bahkan perintah baris tunggal bash -c 'a=3; echo $a > file'berfungsi dengan benar di terminal, tetapi membuat file kosong dalam kasus makefile?

Jofsey
sumber
4
Bagian kedua harus menjadi pertanyaan terpisah. Menambahkan pertanyaan lain hanya akan menimbulkan keributan.
l0b0

Jawaban:

136

Anda dapat menggunakan garis miring terbalik untuk kelanjutan garis. Namun perhatikan bahwa shell menerima seluruh perintah yang digabungkan menjadi satu baris, jadi Anda juga perlu mengakhiri beberapa baris dengan titik koma:

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

Tetapi jika Anda hanya ingin mengambil seluruh daftar yang dikembalikan oleh findpemanggilan dan meneruskannya ke gcc, Anda sebenarnya tidak memerlukan perintah multiline:

foo:
    gcc `find`

Atau, menggunakan $(command)pendekatan konvensional shell (perhatikan $pelolosannya):

foo:
    gcc $$(find)
Eldar Abusalimov
sumber
2
Untuk multiline, Anda masih perlu menghindari tanda dolar, seperti yang ditunjukkan dalam komentar di tempat lain.
tripleee
1
Ya, jawaban singkat 1. $: = $$ 2. tambahkan multiline dengan \ BTW bash guys biasanya merekomendasikan untuk mengganti panggilan `find` dengan $$ (find)
Yauhen Yakimovich
89

Seperti yang ditunjukkan dalam pertanyaan, setiap sub-perintah dijalankan di cangkangnya sendiri . Ini membuat penulisan skrip shell non-trivial sedikit berantakan - tetapi itu mungkin! Solusinya adalah dengan mengkonsolidasikan skrip Anda ke dalam apa yang membuat akan mempertimbangkan satu sub-perintah (satu baris).

Tip untuk menulis skrip shell dalam makefiles:

  1. Hindari penggunaan skrip $dengan mengganti dengan$$
  2. Ubah skrip agar berfungsi sebagai satu baris dengan menyisipkan di ;antara perintah
  3. Jika Anda ingin menulis skrip pada banyak baris, escape end-of-line dengan \
  4. Secara opsional, mulai dengan set -emencocokkan penyediaan make untuk dibatalkan pada kegagalan sub-perintah
  5. Ini benar-benar opsional, tetapi Anda dapat mengurung skrip dengan ()atau {}untuk menekankan keterpaduan dari beberapa urutan baris - bahwa ini bukan urutan perintah makefile yang khas

Inilah contoh yang terinspirasi oleh OP:

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
sumber
4
Anda mungkin juga ingin SHELL := /bin/bashdi makefile Anda untuk mengaktifkan fitur khusus BASH seperti substitusi proses .
Brent Bradburn
8
Poin halus: Spasi setelahnya {sangat penting untuk mencegah interpretasi {setsebagai perintah yang tidak diketahui.
Brent Bradburn
3
Poin halus # 2: Dalam makefile mungkin tidak masalah, tetapi perbedaan antara membungkus dengan {}dan ()membuat perbedaan besar jika Anda terkadang ingin menyalin skrip dan menjalankannya langsung dari prompt shell. Anda dapat mendatangkan malapetaka pada instance shell Anda dengan mendeklarasikan variabel, dan terutama memodifikasi status dengan set, di dalam {}. ()mencegah skrip mengubah lingkungan Anda, yang mungkin lebih disukai. Contoh ( ini akan mengakhiri sesi shell Anda ): { set -e ; } ; false.
Brent Bradburn
Anda dapat memasukkan komentar dengan menggunakan formulir berikut:; command ; ## my comment \` (the comment is between `dan` \ `). Ini tampaknya berfungsi dengan baik kecuali jika Anda menjalankan perintah secara manual (dengan salin dan tempel), riwayat perintah akan menyertakan komentar dengan cara yang merusak perintah (jika Anda mencoba menggunakannya kembali). [Catatan: Penyorotan sintaks rusak untuk komentar ini karena penggunaan garis miring terbalik di dalam backtick.]
Brent Bradburn
Jika Anda ingin memutuskan string dalam bash / perl / script di dalam makefile, tutup kutipan string, garis miring terbalik, baris baru, (tanpa indentasi) kutipan string terbuka. Contoh; perl -e QUOTE print 1; QUOTE BACKSLASH NEWLINE QUOTE print 2 QUOTE
mosh
2

Apa yang salah dengan hanya menjalankan perintah?

foo:
       echo line1
       echo line2
       ....

Dan untuk pertanyaan kedua Anda, Anda harus keluar dari $dengan menggunakan $$, yaitu bash -c '... echo $$a ...'.

EDIT: Contoh Anda dapat ditulis ulang menjadi satu baris skrip seperti ini:

gcc $(for i in `find`; do echo $i; done)
JesperE
sumber
5
Penyebab "setiap perintah dijalankan di shellnya sendiri", sedangkan saya perlu menggunakan hasil dari command1 di command2. Seperti di contoh.
Jofsey
Jika Anda perlu menyebarkan informasi di antara pemanggilan shell, Anda perlu menggunakan beberapa bentuk penyimpanan eksternal, seperti file sementara. Tetapi Anda selalu dapat menulis ulang kode Anda untuk menjalankan beberapa perintah di shell yang sama.
JesperE
+1, terutama untuk versi yang disederhanakan. Dan bukankah itu sama dengan melakukan hanya `` gcc `find```?
Eldar Abusalimov
1
Ya itu. Tapi tentu saja Anda bisa melakukan hal-hal yang lebih kompleks dari sekedar 'echo'.
JesperE
2

Tentu saja, cara yang tepat untuk menulis Makefile adalah dengan mendokumentasikan target mana yang bergantung pada sumber mana. Dalam kasus sepele, solusi yang diusulkan akan membuat fooketergantungan pada dirinya sendiri, tetapi tentu saja, makecukup pintar untuk melepaskan ketergantungan melingkar. Tetapi jika Anda menambahkan file sementara ke direktori Anda, itu akan "secara ajaib" menjadi bagian dari rantai ketergantungan. Lebih baik membuat daftar eksplisit dependensi sekali dan untuk semua, mungkin melalui skrip.

GNU membuat tahu bagaimana menjalankan gccuntuk menghasilkan sebuah executable dari sekumpulan .cdan .hfile, jadi mungkin semua yang Anda butuhkan adalah

foo: $(wildcard *.h) $(wildcard *.c)
tripleee
sumber
1

Direktif ONESHELL memungkinkan untuk menulis beberapa resep baris untuk dieksekusi dalam pemanggilan shell yang sama.

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

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

Namun ada kekurangannya: karakter awalan khusus ('@', '-', dan '+') diinterpretasikan secara berbeda.

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

Jean-Baptiste Poittevin
sumber