È possibile creare una variabile stringa su più righe in un Makefile


122

Voglio creare una variabile di makefile che sia una stringa su più righe (ad esempio il corpo di un annuncio di rilascio tramite posta elettronica). qualcosa di simile a

ANNOUNCE_BODY="
Version $(VERSION) of $(PACKAGE_NAME) has been released

It can be downloaded from $(DOWNLOAD_URL)

etc, etc"

Ma non riesco a trovare un modo per farlo. È possibile?

Risposte:


172

Sì, puoi utilizzare la parola chiave define per dichiarare una variabile multilinea, come questa:

define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

La parte difficile è recuperare la variabile multilinea dal makefile. Se fai semplicemente la cosa ovvia di usare "echo $ (ANNOUNCE_BODY)", vedrai il risultato che altri hanno postato qui: la shell cerca di gestire la seconda e le successive righe della variabile come comandi stessi.

Tuttavia, è possibile esportare il valore della variabile così com'è nella shell come variabile di ambiente e quindi referenziarlo dalla shell come una variabile di ambiente (NON una variabile di make). Per esempio:

export ANNOUNCE_BODY
all:
    @echo "$$ANNOUNCE_BODY"

Notare l'uso di $$ANNOUNCE_BODY, che indica un riferimento alla variabile d'ambiente della shell, piuttosto che $(ANNOUNCE_BODY), che sarebbe un normale riferimento alla variabile make. Assicurati anche di usare le virgolette intorno al tuo riferimento alla variabile, per assicurarti che le nuove righe non siano interpretate dalla shell stessa.

Naturalmente, questo particolare trucco può essere sensibile alla piattaforma e alla shell. L'ho testato su Ubuntu Linux con GNU bash 3.2.13; YMMV.


1
export ANNOUNCE_BODYimposta solo la variabile all'interno delle regole - non consente di fare riferimento a $$ ANNOUNCE_BODY per definire altre variabili.
anatoly techtonik

@techtonik se vuoi usare il valore di ANNOUNCE_BODYin altre definizioni di variabili, fai riferimento ad esso come qualsiasi altra variabile make. Ad esempio OTHER=The variable ANNOUNCE_BODY is $(ANNOUNCE_BODY),. Ovviamente avrai ancora bisogno del exporttrucco se vuoi OTHEReseguire un comando.
Eric Melski,

25

Un altro approccio per 'riportare la variabile multi-riga fuori dal makefile' (annotato da Eric Melski come 'la parte difficile'), è pianificare l'uso della substfunzione per sostituire le nuove righe introdotte con definenella stringa multi-riga con \n. Quindi usa -e con echoper interpretarli. Potrebbe essere necessario impostare .SHELL = bash per ottenere un eco che lo faccia.

Un vantaggio di questo approccio è che inserisci anche altri caratteri di escape nel tuo testo e li fai rispettare.

Questo tipo di sintesi sintetizza tutti gli approcci menzionati finora ...

Finisci con:

define newline


endef

define ANNOUNCE_BODY=
As of $(shell date), version $(VERSION) of $(PACKAGE_NAME) has been released.  

It can be downloaded from $(DOWNLOAD_URL).  

endef

someTarget:
    echo -e '$(subst $(newline),\n,${ANNOUNCE_BODY})'

Nota che le virgolette singole sull'eco finale sono cruciali.


4
Notare che "echo -e" non è portabile. Probabilmente dovresti preferire printf (1) invece.
MadScientist

2
ottima risposta, tuttavia, ho dovuto rimuovere il =dopo define ANNOUNCE_BODYper farlo funzionare.
mschilli

13

Supponendo che tu voglia solo stampare il contenuto della tua variabile sullo standard output, c'è un'altra soluzione:

do-echo:
    $(info $(YOUR_MULTILINE_VAR))

1
Questa regola no-op ha prodotto un messaggio indesiderato: make: 'do-echo' is up to date.. Utilizzando un comando "no op" sono stato in grado di silenziarlo:@: $(info $(YOUR_MULTILINE_VAR))
Guillaume Papin

@GuillaumePapin Un po 'in ritardo, ma puoi usare .PHONYper dire al tuo Makefile che non c'è niente da controllare per quella regola. I makefile erano originariamente per i compilatori, se non sbaglio, quindi makesta facendo qualche magia che non capisco per anticipare che la regola non cambierà nulla, e come tale presume che sia "fatto". L'aggiunta .PHONY do-echodel file dirà makedi ignorarlo ed eseguire comunque il codice.
M3D

Puoi posizionare $(info ...)al di fuori di una regola make. Genererà comunque output.
Daniel Stevens


3

Sì. Scappi dalle nuove righe con \:

VARIABLE="\
THIS IS A VERY LONG\
TEXT STRING IN A MAKE VARIABLE"

aggiornare

Ah, vuoi le nuove righe? Allora no, non credo che ci sia alcun modo in Vanilla Make. Tuttavia, puoi sempre utilizzare un here-document nella parte del comando

[Questo non funziona, vedere il commento di MadScientist]

foo:
    echo <<EOF
    Here is a multiple line text
    with embedded newlines.
    EOF

Questo è vero, ma non mi dà alcuna formattazione (newline). Diventa solo una singola riga di testo
jonner

I documenti here su più righe non funzionano come descritto in GNU Make.
Matt B.

3
I documenti qui su più righe all'interno delle ricette non funzioneranno in NESSUNA versione standard di make che supporti lo standard POSIX: lo standard make richiede che ogni riga separata della ricetta debba essere eseguita in una shell separata. Make non esegue alcuna analisi sul comando per dire che è un here-document o meno, e lo gestisce in modo diverso. Se conosci qualche variante di make che lo supporta (non ne ho mai sentito parlare) dovresti probabilmente dichiararlo esplicitamente.
MadScientist

2

Solo un poscritto alla risposta di Eric Melski: puoi includere l'output dei comandi nel testo, ma devi usare la sintassi del Makefile "$ (shell foo)" piuttosto che la sintassi della shell "$ (foo)". Per esempio:

define ANNOUNCE_BODY  
As of $(shell date), version $(VERSION) of $(PACKAGE_NAME) has been released.  

It can be downloaded from $(DOWNLOAD_URL).  

endef

2

Questo non fornisce un here document, ma mostra un messaggio multilinea in un modo che è adatto per le pipe.

=====

MSG = this is a\\n\
multi-line\\n\
message

method1:
     @$(SHELL) -c "echo '$(MSG)'" | sed -e 's/^ //'

=====

Puoi anche usare le macro richiamabili di Gnu:

=====

MSG = this is a\\n\
multi-line\\n\
message

method1:
        @echo "Method 1:"
        @$(SHELL) -c "echo '$(MSG)'" | sed -e 's/^ //'
        @echo "---"

SHOW = $(SHELL) -c "echo '$1'" | sed -e 's/^ //'

method2:
        @echo "Method 2:"
        @$(call SHOW,$(MSG))
        @echo "---"

=====

Ecco l'output:

=====

$ make method1 method2
Method 1:
this is a
multi-line
message
---
Method 2:
this is a
multi-line
message
---
$

=====


1

Perché non usi il carattere \ n nella tua stringa per definire la fine della riga? Aggiungi anche la barra rovesciata extra per aggiungerla su più righe.

ANNOUNCE_BODY=" \n\
Version $(VERSION) of $(PACKAGE_NAME) has been released \n\
\n\
It can be downloaded from $(DOWNLOAD_URL) \n\
\n\
etc, etc"

Preferisco la risposta di Erik Melski, ma questo potrebbe già fare il trucco per te, a seconda dell'applicazione.
Roalt

Ho una domanda su questo. Questo funziona principalmente bene, tranne per il fatto che vedo uno "spazio" extra all'inizio di ogni riga (tranne la prima). Ti è successo questo? Posso mettere tutto il testo in una riga, separato da \ n creando così efficacemente l'output che mi piace. Il problema è che sembra molto brutto nel Makefile stesso!
Shahbaz

Ho trovato una soluzione alternativa. Ho passato il testo $(subst \n ,\n,$(TEXT))anche se vorrei che ci fosse un modo migliore!
Shahbaz


1

Dovresti usare il costrutto Make "define / endef":

define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

Quindi dovresti passare il valore di questa variabile al comando della shell. Ma, se lo fai usando Crea sostituzione variabile, causerà la divisione del comando in più:

ANNOUNCE.txt:
  echo $(ANNOUNCE_BODY) > $@               # doesn't work

Anche il qouting non aiuterà.

Il modo migliore per passare il valore è passarlo tramite la variabile di ambiente:

ANNOUNCE.txt: export ANNOUNCE_BODY:=$(ANNOUNCE_BODY)
ANNOUNCE.txt:
  echo "$${ANNOUNCE_BODY}" > $@

Avviso:

  1. La variabile viene esportata per questo particolare obiettivo, in modo da poter riutilizzare quell'ambiente non verrà inquinato molto;
  2. Usa la variabile d'ambiente (doppie qoutes e parentesi graffe attorno al nome della variabile);
  3. Uso di virgolette attorno alla variabile. Senza di essi le nuove righe andranno perse e tutto il testo apparirà su una riga.

1

Nello spirito di .ONESHELL, è possibile avvicinarsi molto agli ambienti con sfide .ONESHELL:

define _oneshell_newline_


endef

define oneshell
@eval "$$(printf '%s\n' '$(strip                            \
                         $(subst $(_oneshell_newline_),\n,  \
                         $(subst \,\/,                      \
                         $(subst /,//,                      \
                         $(subst ','"'"',$(1))))))' |       \
          sed -e 's,\\n,\n,g' -e 's,\\/,\\,g' -e 's,//,/,g')"
endef

Un esempio di utilizzo potrebbe essere qualcosa del genere:

define TEST
printf '>\n%s\n' "Hello
World\n/$$$$/"
endef

all:
        $(call oneshell,$(TEST))

Questo mostra l'output (assumendo pid 27801):

>
Hello
World\n/27801/

Questo approccio consente alcune funzionalità extra:

define oneshell
@eval "set -eux ; $$(printf '%s\n' '$(strip                            \
                                    $(subst $(_oneshell_newline_),\n,  \
                                    $(subst \,\/,                      \
                                    $(subst /,//,                      \
                                    $(subst ','"'"',$(1))))))' |       \
                     sed -e 's,\\n,\n,g' -e 's,\\/,\\,g' -e 's,//,/,g')"
endef

Queste opzioni di shell:

  • Stampa ogni comando mentre viene eseguito
  • Esci al primo comando fallito
  • Considera l'uso di variabili di shell non definite come un errore

Probabilmente si suggeriranno altre interessanti possibilità.


1

Mi piace di più la risposta di Alhadis. Ma per mantenere la formattazione a colonne, aggiungi un'altra cosa.

SYNOPSIS := :: Synopsis: Makefile\
| ::\
| :: Usage:\
| ::    make .......... : generates this message\
| ::    make synopsis . : generates this message\
| ::    make clean .... : eliminate unwanted intermediates and targets\
| ::    make all ...... : compile entire system from ground-up\
endef

Uscite:

:: Synopsis: Makefile 
:: 
:: Usage: 
:: make .......... : generates this message 
:: make synopsis . : generates this message 
:: make clean .... : eliminate unwanted intermediates and targets 
:: make all ...... : compile entire system from ground-up

La sinossi di un programma dovrebbe essere facile e ovvia da individuare. Consiglierei di aggiungere questo livello di informazioni in un file readme e / o in una manpage. Quando un utente esegue make, generalmente lo fa aspettandosi di avviare un processo di compilazione.

1
Molte volte ho voluto vedere solo un elenco di obiettivi di make. Il tuo commento non ha senso. Ciò che gli utenti si aspettano è irrilevante se impiegano 3 secondi per sapere cosa fare, mentre invece di qualsiasi informazione come questa, a volte possono volerci ore.
Xennex81

1
Usare le aspettative come motivo per fare qualcosa è anche un argomento circolare: perché le persone se lo aspettano, dobbiamo farlo, e poiché lo facciamo, se lo aspettano.
Xennex81

1

Non completamente correlato all'OP, ma si spera che questo possa aiutare qualcuno in futuro. (poiché questa domanda è quella che emerge di più nelle ricerche su Google).

Nel mio Makefile, volevo passare il contenuto di un file, a un comando di build docker, dopo molta costernazione, ho deciso di:

 base64 encode the contents in the Makefile (so that I could have a single line and pass them as a docker build arg...)
 base64 decode the contents in the Dockerfile (and write them to a file)

vedi esempio sotto.

nb: Nel mio caso particolare, volevo passare una chiave ssh, durante la creazione dell'immagine, utilizzando l'esempio da https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/ (utilizzando una build docker in più fasi per clonare un repository git, quindi rilasciare la chiave ssh dall'immagine finale nella seconda fase della build)

Makefile

...
MY_VAR_ENCODED=$(shell cat /path/to/my/file | base64)

my-build:
    @docker build \
      --build-arg MY_VAR_ENCODED="$(MY_VAR_ENCODED)" \
      --no-cache \
      -t my-docker:build .
...

Dockerfile

...
ARG MY_VAR_ENCODED

RUN mkdir /root/.ssh/  && \
    echo "${MY_VAR_ENCODED}" | base64 -d >  /path/to/my/file/in/container
... 

1

Con GNU Make 3.82 e versioni successive, l' .ONESHELLopzione è tua amica quando si tratta di snippet di shell multilinea. Mettendo insieme suggerimenti da altre risposte, ottengo:

VERSION = 1.2.3
PACKAGE_NAME = foo-bar
DOWNLOAD_URL = $(PACKAGE_NAME).somewhere.net

define nwln

endef

define ANNOUNCE_BODY
Version $(VERSION) of $(PACKAGE_NAME) has been released.

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

.ONESHELL:

# mind the *leading* <tab> character
version:
    @printf "$(subst $(nwln),\n,$(ANNOUNCE_BODY))"

Assicurati, quando copi e incolli l'esempio sopra nel tuo editor, che tutti i <tab>caratteri siano preservati, altrimenti il versionbersaglio si romperà!

Si noti che .ONESHELLtutti i target nel Makefile utilizzeranno una singola shell per tutti i loro comandi.


Sfortunatamente questo non funziona: make version printf "Version 1.2.3 of foo-bar has been released. /bin/sh: 1: Syntax error: Unterminated quoted string make: *** [version] Error 2(GNU make 3,81)
blueyed

@blueyed, l'ho appena testato con GNU Make 3.82 e GNU bash 4.2.45 (1) -release: funziona come previsto. Inoltre, controlla la presenza del carattere TAB iniziale, invece di spazi vuoti, davanti @printf ...all'istruzione - sembra che i TAB siano sempre visualizzati come 4 spazi ...
sphakka

Sembra che .ONESHELLsia nuovo nel make 3.82.
blueyed

btw: l'errore quando si usano gli spazi invece di una tabulazione sarebbe *** missing separator. Stop..
blueyed

0

Non è davvero una risposta utile, ma solo per indicare che 'define' non funziona come risposto da Axe (non rientrava in un commento):

VERSION=4.3.1
PACKAGE_NAME=foobar
DOWNLOAD_URL=www.foobar.com

define ANNOUNCE_BODY
    Version $(VERSION) of $(PACKAGE_NAME) has been released
    It can be downloaded from $(DOWNLOAD_URL)
    etc, etc
endef

all:
    @echo $(ANNOUNCE_BODY)

Dà un errore che il comando 'It' non può essere trovato, quindi cerca di interpretare la seconda riga di ANNOUNCE BODY come un comando.


0

Ha funzionato per me:

ANNOUNCE_BODY="first line\\nsecond line"

all:
    @echo -e $(ANNOUNCE_BODY)

0

GNU Makefile può fare cose come le seguenti. È brutto e non dico che dovresti farlo, ma lo faccio in certe situazioni.

PROFILE = \
\#!/bin/sh.exe\n\
\#\n\
\# A MinGW equivalent for .bash_profile on Linux.  In MinGW/MSYS, the file\n\
\# is actually named .profile, not .bash_profile.\n\
\#\n\
\# Get the aliases and functions\n\
\#\n\
if [ -f \$${HOME}/.bashrc ]\n\
then\n\
  . \$${HOME}/.bashrc\n\
fi\n\
\n\
export CVS_RSH="ssh"\n  
#
.profile:
        echo -e "$(PROFILE)" | sed -e 's/^[ ]//' >.profile

make .profile crea un file .profile se non esiste.

Questa soluzione è stata utilizzata laddove l'applicazione utilizzerà solo GNU Makefile in un ambiente shell POSIX. Il progetto non è un progetto open source in cui la compatibilità della piattaforma è un problema.

L'obiettivo era creare un Makefile che faciliti sia l'impostazione che l'utilizzo di un particolare tipo di spazio di lavoro. Il Makefile porta con sé varie risorse semplici senza richiedere cose come un altro archivio speciale, ecc. È, in un certo senso, un archivio di shell. Una procedura può quindi dire cose come rilascia questo Makefile nella cartella in cui lavorare. Configura il tuo spazio di lavoro Invio make workspace, quindi per fare blah, Invio make blah, ecc.

Quello che può diventare complicato è capire cosa citare in shell. Quanto sopra fa il lavoro ed è vicino all'idea di specificare un here document nel Makefile. Se sia una buona idea per uso generale è tutta un'altra questione.


0

Credo che la risposta più sicura per l'uso multipiattaforma sarebbe utilizzare un eco per riga:

  ANNOUNCE.txt:
    rm -f $@
    echo "Version $(VERSION) of $(PACKAGE_NAME) has been released" > $@
    echo "" >> $@
    echo "It can be downloaded from $(DOWNLOAD_URL)" >> $@
    echo >> $@
    echo etc, etc" >> $@

Questo evita di fare qualsiasi supposizione sulla versione di echo disponibile.


0

Usa la sostituzione della stringa :

VERSION := 1.1.1
PACKAGE_NAME := Foo Bar
DOWNLOAD_URL := https://go.get/some/thing.tar.gz

ANNOUNCE_BODY := Version $(VERSION) of $(PACKAGE_NAME) has been released. \
    | \
    | It can be downloaded from $(DOWNLOAD_URL) \
    | \
    | etc, etc

Quindi nella tua ricetta, metti

    @echo $(subst | ,$$'\n',$(ANNOUNCE_BODY))

Questo funziona perché Make sostituisce tutte le occorrenze di (nota lo spazio) e lo scambia con un carattere di nuova riga ( $$'\n'). Puoi pensare alle invocazioni di script di shell equivalenti come qualcosa del genere:

Prima:

$ echo "Version 1.1.1 of Foo Bar has been released. | | It can be downloaded from https://go.get/some/thing.tar.gz | | etc, etc"

Dopo:

$ echo "Version 1.1.1 of Foo Bar has been released.
>
> It can be downloaded from https://go.get/some/thing.tar.gz
> 
> etc, etc"

Non sono sicuro che $'\n'sia disponibile su sistemi non POSIX, ma se puoi accedere a un singolo carattere di nuova riga (anche leggendo una stringa da un file esterno), il principio sottostante è lo stesso.

Se hai molti messaggi come questo, puoi ridurre il rumore utilizzando una macro :

print = $(subst | ,$$'\n',$(1))

Dove lo invoceresti in questo modo:

@$(call print,$(ANNOUNCE_BODY))

Spero che questo aiuti qualcuno. =)


Mi piace di più questo. Ma per mantenere la formattazione a colonne, aggiungi un'altra cosa. `SINOSSI: = :: Sinossi: Makefile \ | :: \ | :: Utilizzo: \ | :: make ..........: genera questo messaggio \ | :: fai la sinossi. : genera questo messaggio \ | :: make clean ....: elimina intermedi e obiettivi indesiderati \ | :: make all ......: compila l'intero sistema da zero \ endef
jlettvin

I commenti non consentono il codice. Manderò come risposta. Mi piace di più questo. Ma per mantenere la formattazione a colonne, aggiungi un'altra cosa. `SYNOPSIS: = :: Synopsis: Makefile`` | :: `` | :: Utilizzo: `` | :: make ..........: genera questo messaggio` `| :: fai la sinossi. : genera questo messaggio` `| :: make clean ....: elimina intermedi e obiettivi indesiderati` `| :: make all ......: compila l'intero sistema da zero` `endef`
jlettvin

@jlettvin Vedi la mia risposta alla tua risposta. Sinossi di Un programma dovrebbe assolutamente non essere incorporato all'interno di un Makefile, soprattutto non come un compito di default.

0

In alternativa puoi usare il comando printf. Questo è utile su OSX o altre piattaforme con meno funzionalità.

Per generare semplicemente un messaggio su più righe:

all:
        @printf '%s\n' \
            'Version $(VERSION) has been released' \
            '' \
            'You can download from URL $(URL)'

Se stai cercando di passare la stringa come argomento a un altro programma, puoi farlo in questo modo:

all:
        /some/command "`printf '%s\n' 'Version $(VERSION) has been released' '' 'You can download from URL $(URL)'`"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.