É possível criar uma variável de string de várias linhas em um Makefile


122

Eu quero criar uma variável makefile que é uma string de várias linhas (por exemplo, o corpo de um anúncio de lançamento por e-mail). algo como

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

It can be downloaded from $(DOWNLOAD_URL)

etc, etc"

Mas não consigo encontrar uma maneira de fazer isso. É possível?

Respostas:


172

Sim, você pode usar a palavra-chave define para declarar uma variável multilinha, como esta:

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

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

A parte complicada é retirar sua variável multilinha do makefile. Se você apenas fizer a coisa óbvia de usar "echo $ (ANNOUNCE_BODY)", verá o resultado que outros postaram aqui - o shell tenta manipular a segunda linha e as subsequentes da variável como comandos próprios.

No entanto, você pode exportar o valor da variável no estado em que se encontra para o shell como uma variável de ambiente e, em seguida, referenciá-lo do shell como uma variável de ambiente (NÃO uma variável make). Por exemplo:

export ANNOUNCE_BODY
all:
    @echo "$$ANNOUNCE_BODY"

Observe o uso de $$ANNOUNCE_BODY, indicando uma referência de variável de ambiente do shell, em vez de $(ANNOUNCE_BODY), que seria uma referência regular de variável make. Além disso, certifique-se de usar aspas em torno de sua referência de variável, para garantir que as novas linhas não sejam interpretadas pelo próprio shell.

Claro, esse truque específico pode ser sensível à plataforma e ao shell. Eu testei no Ubuntu Linux com GNU bash 3.2.13; YMMV.


1
export ANNOUNCE_BODYapenas define a variável dentro das regras - não permite fazer referência a $$ ANNOUNCE_BODY para definir outras variáveis.
anatoly techtonik

@techtonik se você quiser usar o valor de ANNOUNCE_BODYem outras definições de variáveis, apenas faça referência a ele como qualquer outra variável make. Por exemplo OTHER=The variable ANNOUNCE_BODY is $(ANNOUNCE_BODY),. Claro que você ainda precisará do exporttruque se quiser OTHERsair em um comando.
Eric Melski

25

Outra abordagem para 'obter sua variável multilinhas de volta do makefile' (apontada por Eric Melski como 'a parte complicada'), é planejar o uso da substfunção para substituir as novas linhas introduzidas defineem sua string multilinhas por \n. Em seguida, use -e com echopara interpretá-los. Pode ser necessário definir .SHELL = bash para obter um eco que faça isso.

Uma vantagem dessa abordagem é que você também coloca outros caracteres de escape em seu texto e os respeita.

Este tipo de sintetiza todas as abordagens mencionadas até agora ...

Você acaba com:

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})'

Observe que as aspas simples no eco final são cruciais.


4
Observe que "echo -e" não é portátil. Você provavelmente deve preferir printf (1) ao invés.
MadScientist

2
ótima resposta, no entanto, tive que remover o =depois define ANNOUNCE_BODYpara colocá-lo em execução.
mschilli

13

Supondo que você deseja apenas imprimir o conteúdo de sua variável na saída padrão, há outra solução:

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

1
Esta regra não-op produziu uma mensagem indesejada: make: 'do-echo' is up to date.. Usando um comando "no op", fui capaz de silenciá-lo:@: $(info $(YOUR_MULTILINE_VAR))
Guillaume Papin

@GuillaumePapin Um pouco tarde, mas você pode usar .PHONYpara dizer ao seu Makefile que não há nada para verificar para essa regra. Makefiles eram originalmente para compiladores, se não me engano, então makeestá fazendo alguma mágica que eu não entendo para antecipar que a regra não mudará nada e, como tal, assume que está 'pronto'. Adicionar .PHONY do-echoem seu arquivo dirá makepara ignorar isso e executar o código de qualquer maneira.
M3D

Você pode colocar $(info ...)fora de uma regra de criação. Ele ainda gerará saída.
Daniel Stevens


3

Sim. Você escapa das novas linhas com \:

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

atualizar

Ah, você quer as novas linhas? Então não, eu não acho que haja nenhuma maneira de fazer baunilha. No entanto, você sempre pode usar um here-document na parte de comando

[Isso não funciona, veja o comentário de MadScientist]

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

Isso é verdade, mas não me dá nenhuma formatação (novas linhas). Torna-se apenas uma única linha de texto
Jonner,

Aqui-documentos multilinhas não funcionam conforme descrito no GNU Make.
Matt B.

3
Multiline aqui, os documentos dentro de receitas não funcionarão em NENHUMA versão padrão do make que suporte o padrão POSIX: o padrão do make requer que cada linha separada da receita deve ser executada em um shell separado. O make não analisa o comando para saber se é um here-document ou não, e o trata de forma diferente. Se você conhece alguma variante do make que suporte isso (nunca ouvi falar de uma), provavelmente deve declarar explicitamente.
MadScientist

2

Apenas um pós-escrito para a resposta de Eric Melski: Você pode incluir a saída de comandos no texto, mas deve usar a sintaxe Makefile "$ (shell foo)" ao invés da sintaxe shell "$ (foo)". Por exemplo:

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

It can be downloaded from $(DOWNLOAD_URL).  

endef

2

Isso não fornece um documento here, mas exibe uma mensagem multilinha de uma forma adequada para canais.

=====

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

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

=====

Você também pode usar macros chamáveis ​​do 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 "---"

=====

Aqui está o resultado:

=====

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

=====


1

Por que você não usa o caractere \ n em sua string para definir o fim da linha? Adicione também a barra invertida extra para adicioná-la em várias linhas.

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

Eu prefiro a resposta de Erik Melski, mas isso pode já servir para você, dependendo da sua aplicação.
Roalt,

Eu tenho uma pergunta sobre isso. Isso funciona principalmente bem, exceto que vejo um "espaço" extra no início de cada linha (exceto a primeira). Isso acontece com você? Posso colocar todo o texto em uma linha, separado por \ n de forma eficaz, criando a saída que gosto. O problema é que parece muito feio no próprio Makefile!
Shahbaz

Eu encontrei uma solução alternativa. Passei o texto $(subst \n ,\n,$(TEXT))embora gostaria que houvesse uma maneira melhor!
Shahbaz


1

Você deve usar a construção de Make "define / endef":

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

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

Então você deve passar o valor desta variável para o comando shell. Mas, se você fizer isso usando Fazer substituição de variável, o comando será dividido em vários:

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

Qouting também não vai ajudar.

A melhor maneira de passar valor é passá-lo por meio da variável de ambiente:

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

Aviso prévio:

  1. A variável é exportada para este destino específico, para que você possa reutilizar esse ambiente não será muito poluído;
  2. Use a variável de ambiente (qoutes duplas e colchetes ao redor do nome da variável);
  3. Uso de aspas em torno da variável. Sem eles, as novas linhas serão perdidas e todo o texto aparecerá em uma linha.

1

No espírito de .ONESHELL, é possível chegar bem perto em ambientes desafiadores .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

Um exemplo de uso seria algo assim:

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

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

Isso mostra a saída (assumindo pid 27801):

>
Hello
World\n/27801/

Essa abordagem permite algumas funcionalidades extras:

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

Essas opções de shell irão:

  • Imprime cada comando à medida que é executado
  • Saia no primeiro comando com falha
  • Trate o uso de variáveis ​​de shell indefinidas como um erro

Outras possibilidades interessantes provavelmente surgirão.


1

Eu gosto mais da resposta de Alhadis. Mas para manter a formatação colunar, adicione mais uma coisa.

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

Saídas:

:: 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

A sinopse de um programa deve ser fácil e óbvia de localizar. Eu recomendo adicionar este nível de informação em um leiame e / ou página de manual. Quando um usuário é executado make, ele geralmente o faz esperando iniciar um processo de construção.

1
Muitas vezes eu quis ver apenas uma lista de metas de fabricação. Seu comentário não faz sentido. O que os usuários esperam é irrelevante se levarem 3 segundos para saber o que fazer, ao passo que, em vez de qualquer informação como essa, às vezes pode levar horas.
Xennex81

1
Usar as expectativas como razão para fazer algo também é um argumento circular: porque as pessoas esperam, devemos fazê-lo e, porque fazemos, elas esperam.
Xennex81

1

Não totalmente relacionado ao OP, mas espero que isso ajude alguém no futuro. (visto que esta questão é a que mais surge nas buscas do google).

No meu Makefile, eu queria passar o conteúdo de um arquivo para um comando docker build, depois de muita consternação, decidi:

 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)

veja o exemplo abaixo.

nb: No meu caso particular, eu queria passar uma chave ssh, durante a construção da imagem, usando o exemplo de https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/ (usando uma compilação docker de vários estágios para clonar um repo git e, em seguida, remova a chave ssh da imagem final no segundo estágio da compilação)

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

Com o GNU Make 3.82 e superior, a .ONESHELLopção é sua quando se trata de fragmentos de shell de várias linhas. Juntando dicas de outras respostas, eu obtenho:

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))"

Certifique-se, ao copiar e colar o exemplo acima em seu editor, de que todos os <tab>caracteres sejam preservados, caso contrário, o versionalvo será quebrado!

Note que .ONESHELLfará com que todos os alvos no Makefile usem um único shell para todos os seus comandos.


Infelizmente isso não funciona: 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)
azulado

@blueyed, acabei de testar com GNU Make 3.82 e GNU bash 4.2.45 (1) -release: funciona como esperado. Além disso, verifique a presença do caractere TAB inicial, em vez de espaços em branco, na frente da @printf ...instrução - parece que os TABs são sempre renderizados como 4 espaços ...
sphakka

Parece que .ONESHELLé novo na marca 3.82.
azulado

btw: o erro ao usar espaços em vez de uma guia seria *** missing separator. Stop..
azulado

0

Não é realmente uma resposta útil, mas apenas para indicar que 'definir' não funciona como respondido por Ax (não cabia em um comentário):

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)

Ele dá um erro que o comando 'It' não pode ser encontrado, então ele tenta interpretar a segunda linha de ANNOUNCE BODY como um comando.


0

Funcionou para mim:

ANNOUNCE_BODY="first line\\nsecond line"

all:
    @echo -e $(ANNOUNCE_BODY)

0

GNU Makefile pode fazer coisas como o seguinte. É feio e não vou dizer que você deve fazer isso, mas eu faço em certas situações.

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 cria um arquivo .profile se não houver um.

Esta solução foi usada onde o aplicativo só usará GNU Makefile em um ambiente de shell POSIX. O projeto não é um projeto de código aberto onde a compatibilidade da plataforma é um problema.

O objetivo era criar um Makefile que facilitasse a configuração e o uso de um determinado tipo de espaço de trabalho. O Makefile traz consigo vários recursos simples sem exigir coisas como outro arquivo especial, etc. É, de certa forma, um arquivo de shell. Um procedimento pode então dizer coisas como soltar este Makefile na pasta para trabalhar. Configure sua área de trabalho make workspace, enter , então para blá, enter make blah, etc.

O que pode ser complicado é descobrir o que deve ser feito. O acima faz o trabalho e está próximo da idéia de especificar um documento here no Makefile. Se é uma boa ideia para uso geral é outra questão.


0

Acredito que a resposta mais segura para uso em várias plataformas seria usar um eco por linha:

  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" >> $@

Isso evita fazer suposições sobre a versão do echo disponível.


0

Use substituição de string :

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

Então, na sua receita, coloque

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

Isso funciona porque o Make está substituindo todas as ocorrências de (observe o espaço) e trocando-o por um caractere de nova linha ( $$'\n'). Você pode pensar nas invocações de script de shell equivalentes como algo assim:

Antes:

$ 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"

Depois de:

$ 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"

Não tenho certeza se $'\n'está disponível em sistemas não POSIX, mas se você pode obter acesso a um único caractere de nova linha (mesmo lendo uma string de um arquivo externo), o princípio subjacente é o mesmo.

Se você tiver muitas mensagens como esta, pode reduzir o ruído usando uma macro :

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

Onde você o invocaria assim:

@$(call print,$(ANNOUNCE_BODY))

Espero que isso ajude alguém. =)


Eu gosto mais deste. Mas para manter a formatação colunar, adicione mais uma coisa. `SINOPSE: = :: Sinopse: Makefile \ | :: \ | :: Uso: \ | :: make ..........: gera esta mensagem \ | :: fazer sinopse. : gera esta mensagem \ | :: make clean ....: elimine intermediários e alvos indesejados \ | :: fazer tudo ......: compilar o sistema inteiro desde o início \ endef
jlettvin

Comentários não permitem código. Enviará como resposta. Eu gosto mais deste. Mas para manter a formatação colunar, adicione mais uma coisa. `SINOPSE: = :: Sinopse: Makefile`` | :: `` | :: Uso: `` | :: make ..........: gera esta mensagem` `| :: fazer sinopse. : gera esta mensagem` `| :: make clean ....: elimina intermediários e alvos indesejados` `| :: make all ......: compila todo o sistema do zero` `endef`
jlettvin

@jlettvin Veja minha resposta à sua resposta. A sinopse de um programa definitivamente não deve ser embutida em um Makefile, especialmente não como uma tarefa padrão.

0

Como alternativa, você pode usar o comando printf. Isso é útil em OSX ou outras plataformas com menos recursos.

Para simplesmente enviar uma mensagem de várias linhas:

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

Se estiver tentando passar a string como um argumento para outro programa, você pode fazer assim:

all:
        /some/command "`printf '%s\n' 'Version $(VERSION) has been released' '' 'You can download from URL $(URL)'`"
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.