¿Es posible crear una variable de cadena de varias líneas en un Makefile?

122

Quiero crear una variable de archivo MAKE que sea una cadena de varias líneas (por ejemplo, el cuerpo de un anuncio de publicación por correo electrónico). algo como

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

It can be downloaded from $(DOWNLOAD_URL)

etc, etc"

Pero parece que no puedo encontrar una manera de hacer esto. ¿Es posible?

Jonner
fuente

Respuestas:

172

Sí, puede usar la palabra clave define para declarar una variable de varias líneas, como esta:

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

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

La parte complicada es recuperar la variable de varias líneas del archivo MAKE. Si simplemente hace lo obvio de usar "echo $ (ANNOUNCE_BODY)", verá el resultado que otros han publicado aquí: el shell intenta manejar la segunda línea y las siguientes de la variable como comandos ellos mismos.

Sin embargo, puede exportar el valor de la variable tal cual al shell como una variable de entorno y luego hacer referencia a él desde el shell como una variable de entorno (NO una variable make). Por ejemplo:

export ANNOUNCE_BODY
all:
    @echo "$$ANNOUNCE_BODY"

Tenga en cuenta el uso de $$ANNOUNCE_BODY, que indica una referencia de variable de entorno de shell, en lugar de $(ANNOUNCE_BODY), que sería una referencia de variable make regular. También asegúrese de usar comillas alrededor de su referencia de variable, para asegurarse de que las líneas nuevas no sean interpretadas por el propio shell.

Por supuesto, este truco en particular puede ser sensible a la plataforma y al shell. Lo probé en Ubuntu Linux con GNU bash 3.2.13; YMMV.

Eric Melski
fuente
1
export ANNOUNCE_BODYsolo establece la variable dentro de las reglas; no permite hacer referencia a $$ ANNOUNCE_BODY para definir otras variables.
anatoly techtonik
@techtonik si desea utilizar el valor de ANNOUNCE_BODYen otras definiciones de variables, simplemente haga referencia a él como cualquier otra variable make. Por ejemplo OTHER=The variable ANNOUNCE_BODY is $(ANNOUNCE_BODY),. Por supuesto, aún necesitará el exporttruco si quiere OTHERsalir en un comando.
Eric Melski
25

Otro enfoque para 'sacar su variable multilínea del archivo MAKE' (señalada por Eric Melski como 'la parte complicada'), es planificar el uso de la substfunción para reemplazar las nuevas líneas introducidas defineen su cadena multilínea con \n. Luego usa -e conecho para interpretarlos. Es posible que deba configurar .SHELL = bash para obtener un eco que haga esto.

Una ventaja de este enfoque es que también coloca otros caracteres de escape en su texto y los respeta.

Este tipo de sintetiza todos los enfoques mencionados hasta ahora ...

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

Tenga en cuenta que las comillas simples en el eco final son cruciales.

malcook
fuente
4
Tenga en cuenta que "echo -e" no es portátil. Probablemente debería preferir printf (1) en su lugar.
MadScientist
2
gran respuesta, sin embargo, tuve que eliminar el =después define ANNOUNCE_BODYpara que funcionara.
mschilli
13

Suponiendo que solo desea imprimir el contenido de su variable en la salida estándar, existe otra solución:

do-echo:
    $(info $(YOUR_MULTILINE_VAR))
superwhoopy
fuente
1
Esta regla no-op produce un mensaje no deseado: make: 'do-echo' is up to date.. Al usar un comando "no op" y pude silenciarlo:@: $(info $(YOUR_MULTILINE_VAR))
Guillaume Papin
@GuillaumePapin Un poco tarde, pero puede usar .PHONYpara decirle a su Makefile que no hay nada que verificar para esa regla. Los Makefiles fueron originalmente para compiladores, si no me equivoco, también makeestá haciendo algo de magia que no entiendo para anticipar que la regla no cambiará nada y, como tal, asume que está 'hecho'. Agregar .PHONY do-echosu archivo le dirá makeque ignore esto y ejecute el código de todos modos.
M3D
Puede colocar $(info ...)fuera de una regla de marca. Seguirá generando salida.
Daniel Stevens
Documentación: Hacer funciones de control
Daniel Stevens
3

Si. Escapas de las nuevas líneas con \:

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

actualizar

Ah, ¿quieres las nuevas líneas? Entonces no, no creo que haya ninguna forma en Vanilla Make. Sin embargo, siempre puede usar un documento aquí en la parte de comando

[Esto no funciona, vea el comentario de MadScientist]

foo:
    echo <<EOF
    Here is a multiple line text
    with embedded newlines.
    EOF
Charlie martin
fuente
Esto es cierto, pero no me da ningún formato (líneas nuevas). Simplemente se convierte en una sola línea de texto
jonner
Los documentos aquí de varias líneas no funcionan como se describe en GNU Make.
Matt B.
3
Aquí, los documentos de varias líneas dentro de las recetas no funcionarán en CUALQUIER versión estándar de make que admita el estándar POSIX: el estándar make requiere que cada línea separada de la receta se ejecute en un shell separado. Make no analiza el comando para indicar si es un documento aquí o no, y lo maneja de manera diferente. Si conoce alguna variante de marca que admita esto (nunca he oído hablar de una), probablemente debería indicarlo explícitamente.
MadScientist
2

Solo una posdata a la respuesta de Eric Melski: puede incluir la salida de los comandos en el texto, pero debe usar la sintaxis de Makefile "$ (shell foo)" en lugar de la sintaxis de shell "$ (foo)". Por ejemplo:

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

It can be downloaded from $(DOWNLOAD_URL).  

endef
Jim Van Zandt
fuente
2

Esto no da un documento aquí, pero muestra un mensaje de varias líneas de una manera adecuada para tuberías.

=====

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

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

=====

También puede utilizar las macros invocables de 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 "---"

=====

Aquí está el resultado:

=====

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

=====

Paul Sander
fuente
1

¿Por qué no utiliza el carácter \ n en su cadena para definir el final de la línea? También agregue la barra invertida adicional para agregarla en varias líneas.

ANNOUNCE_BODY=" \n\
Version $(VERSION) of $(PACKAGE_NAME) has been released \n\
\n\
It can be downloaded from $(DOWNLOAD_URL) \n\
\n\
etc, etc"
Roalt
fuente
Prefiero la respuesta de Erik Melski, pero esto podría ser suficiente para usted, dependiendo de su aplicación.
Roalt
Tengo una pregunta sobre esto. Esto funciona principalmente bien, excepto que veo un "espacio" adicional al principio de cada línea (excepto la primera). ¿Te pasa esto a ti? Puedo poner todo el texto en una línea, separado por \ n creando de manera tan efectiva la salida que me gusta. ¡El problema es que se ve muy feo en el propio Makefile!
Shahbaz
Encontré una solución. ¡Pasé el texto $(subst \n ,\n,$(TEXT))aunque desearía que hubiera una mejor manera!
Shahbaz
1

Deberías usar "definir / endef" Make construct:

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

It can be downloaded from $(DOWNLOAD_URL).

etc, etc.
endef

Entonces debe pasar el valor de esta variable al comando de shell. Pero, si hace esto usando Hacer sustitución de variable, hará que el comando se divida en múltiples:

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

Qouting tampoco ayudará.

La mejor manera de pasar el valor es pasarlo a través de la variable de entorno:

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

Aviso:

  1. La variable se exporta para este objetivo en particular, de modo que pueda reutilizar ese entorno que no se contaminará mucho;
  2. Utilice la variable de entorno (doble qoutes y corchetes alrededor del nombre de la variable);
  3. Uso de comillas alrededor de variables. Sin ellos, las nuevas líneas se perderán y todo el texto aparecerá en una línea.
Maxim Kulkin
fuente
1

En el espíritu de .ONESHELL, es posible acercarse bastante en entornos desafiados por .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 ejemplo de uso sería algo como esto:

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

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

Eso muestra la salida (asumiendo pid 27801):

>
Hello
World\n/27801/

Este enfoque permite algunas funciones adicionales:

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

Estas opciones de shell:

  • Imprime cada comando a medida que se ejecuta
  • Salir con el primer comando fallido
  • Trate el uso de variables de shell no definidas como un error

Es probable que surjan otras posibilidades interesantes.

Conde
fuente
1

Me gusta más la respuesta de alhadis. Pero para mantener el formato de columnas, agregue una cosa más.

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

Salidas:

:: 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
jlettvin
fuente
La sinopsis de un programa debe ser fácil y obvia de localizar. Recomiendo agregar este nivel de información en un archivo Léame y / o página de manual. Cuando un usuario ejecuta make, generalmente lo hace esperando comenzar un proceso de compilación.
1
Muchas veces he querido ver una lista de objetivos marcados. Tu comentario no tiene sentido. Lo que los usuarios esperan es irrelevante si les toma 3 segundos saber qué hacer, mientras que en lugar de cualquier información como esta, a veces puede llevar horas.
Xennex81
1
Usar las expectativas como razón para hacer algo también es un argumento circular: porque la gente lo espera, debemos hacerlo, y porque lo hacemos, ellos lo esperan.
Xennex81
1

No está completamente relacionado con el OP, pero con suerte esto ayudará a alguien en el futuro. (ya que esta pregunta es la que más surge en las búsquedas de google).

En mi Makefile, quería pasar el contenido de un archivo a un comando de compilación de la ventana acoplable, después de mucha consternación, decidí:

 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)

vea el ejemplo a continuación.

nb: En mi caso particular, quería pasar una clave ssh, durante la construcción de la imagen, usando el ejemplo de https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/ (usando una compilación de docker de múltiples etapas para clonar un repositorio de git, luego suelte la clave ssh de la imagen final en la segunda etapa de la compilación)

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
... 
mlo55
fuente
1

Con GNU Make 3.82 y superior, la .ONESHELLopción es su amiga cuando se trata de fragmentos de shell de varias líneas. Al juntar pistas de otras respuestas, obtengo:

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

Al copiar y pegar el ejemplo anterior en su editor, asegúrese de que <tab>se conservan todos los caracteres; de lo contrario, el versionobjetivo se romperá.

Tenga en cuenta que .ONESHELLhará que todos los destinos del Makefile usen un solo shell para todos sus comandos.

sphakka
fuente
Desafortunadamente, eso no 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)
blueyed
@blueyed, acabo de probarlo con GNU Make 3.82 y GNU bash 4.2.45 (1) -release: funciona como se esperaba. Además, verifique la presencia del carácter TAB principal, en lugar de espacios en blanco, delante de la @printf ...declaración; parece que las TAB siempre se representan como 4 espacios ...
sphakka
Parece que .ONESHELLes nuevo en make 3.82.
blueyed
por cierto: el error al usar espacios en lugar de una pestaña sería *** missing separator. Stop..
blueyed
0

No es realmente una respuesta útil, pero solo para indicar que 'definir' no funciona como respondió Axe (no encajaba en un comentario):

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)

Da un error de que no se puede encontrar el comando 'It', por lo que intenta interpretar la segunda línea de ANNOUNCE BODY como un comando.

Roalt
fuente
0

Funcionó para mí:

ANNOUNCE_BODY="first line\\nsecond line"

all:
    @echo -e $(ANNOUNCE_BODY)
fdb
fuente
0

GNU Makefile puede hacer cosas como las siguientes. Es feo y no diré que debas hacerlo, pero lo hago en ciertas situaciones.

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 archivo .profile si no existe.

Esta solución se usó donde la aplicación solo usará GNU Makefile en un entorno de shell POSIX. El proyecto no es un proyecto de código abierto donde la compatibilidad de la plataforma es un problema.

El objetivo era crear un Makefile que facilitara tanto la configuración como el uso de un tipo particular de espacio de trabajo. El Makefile trae consigo varios recursos simples sin requerir cosas como otro archivo especial, etc. Es, en cierto sentido, un archivo de shell. Luego, un procedimiento puede decir cosas como colocar este Makefile en la carpeta para trabajar. Configure su espacio de trabajo make workspace, ingrese , luego haga bla, ingrese make blah, etc.

Lo que puede resultar complicado es averiguar qué cotizar. Lo anterior hace el trabajo y se acerca a la idea de especificar un documento aquí en el Makefile. Si es una buena idea para uso general es otra cuestión.

kbulgrien
fuente
0

Creo que la respuesta más segura para el uso multiplataforma sería usar un eco por línea:

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

Esto evita hacer suposiciones sobre la versión de echo disponible.

Ben Martín
fuente
0

Utilice la sustitución de cadenas :

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

Luego, en tu receta, pon

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

Esto funciona porque Make está sustituyendo todas las apariciones de (observe el espacio) y lo intercambia con un carácter de nueva línea ( $$'\n'). Puede pensar en las invocaciones de script de shell equivalentes como algo como esto:

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

Después:

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

No estoy seguro de si $'\n'está disponible en sistemas que no son POSIX, pero si puede obtener acceso a un solo carácter de nueva línea (incluso leyendo una cadena de un archivo externo), el principio subyacente es el mismo.

Si tiene muchos mensajes como este, puede reducir el ruido utilizando un macro :

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

Donde lo invocarías así:

@$(call print,$(ANNOUNCE_BODY))

Espero que esto ayude a alguien. =)


fuente
Me gusta más este. Pero para mantener el formato de columnas, agregue una cosa más. `SINOPSIS: = :: Sinopsis: Makefile \ | :: \ | :: Uso: \ | :: make ..........: genera este mensaje \ | :: hacer sinopsis. : genera este mensaje \ | :: limpiar ....: eliminar intermediarios y objetivos no deseados \ | :: make all ......: compila todo el sistema desde cero \ endef
jlettvin
Los comentarios no admiten código. Enviará como respuesta. Me gusta más este. Pero para mantener el formato de columnas, agregue una cosa más. `SINOPSIS: = :: Sinopsis: Makefile`` | :: `` | :: Uso: `` | :: make ..........: genera este mensaje` `| :: hacer sinopsis. : genera este mensaje` `| :: hacer limpio ....: eliminar intermedios y objetivos no deseados` `| :: make all ......: compila todo el sistema desde cero` `endef`
jlettvin
@jlettvin Vea mi respuesta a su respuesta. La sinopsis de un programa definitivamente no debe estar incrustada dentro de un Makefile, especialmente no como una tarea predeterminada.
0

Como alternativa, puede utilizar el comando printf. Esto es útil en OSX u otras plataformas con menos funciones.

Para simplemente generar un mensaje de varias líneas:

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

Si está intentando pasar la cadena como un argumento a otro programa, puede hacerlo así:

all:
        /some/command "`printf '%s\n' 'Version $(VERSION) has been released' '' 'You can download from URL $(URL)'`"
zeroimpl
fuente