¿Reemplazar variables de entorno en un archivo con sus valores reales?

41

¿Hay una manera fácil de sustituir / evaluar variables de entorno en un archivo? Como digamos que tengo un archivo config.xmlque contiene:

<property>
    <name>instanceId</name>
    <value>$INSTANCE_ID</value>
</property>
<property>
    <name>rootPath</name>
    <value>/services/$SERVICE_NAME</value>
</property>

... etc. Quiero reemplazar $INSTANCE_IDen el archivo con el valor de la INSTANCE_IDvariable de entorno, $SERVICE_NAMEcon el valor de la variable SERVICE_NAMEenv. No sabré a priori qué variables de entorno son necesarias (o más bien, no quiero tener que actualizar el script si alguien agrega una nueva variable de entorno al archivo de configuración). ¡Gracias!

Robert Fraser
fuente
1
¿Cuándo harás algo con el archivo (cat, echo, source, ...) la variable se sustituirá por su valor
Costas
¿El contenido de este archivo xml depende de usted? Si es así, xslt parametrizado ofrece otra forma de inyectar valores y (a diferencia de envsubst y su tipo) garantiza un xml bien formado como resultado.
kojiro

Respuestas:

69

Podrías usar envsubst(parte de gnu gettext):

envsubst < infile

reemplazará las variables de entorno en su archivo con su valor correspondiente. Los nombres de las variables deben consistir únicamente en caracteres ASCII alfanuméricos o de subrayado, no comenzar con un dígito y no deben estar vacíos; de lo contrario, se ignora dicha referencia variable.


Para reemplazar solo ciertas variables de entorno, consulte esta pregunta.

don_crissti
fuente
1
... excepto que no está instalado por defecto en mi imagen acoplable: '- (
Robert Fraser
44
Eso es bueno. Las imágenes de Docker deben ser ligeras y hechas a medida. Por supuesto, siempre puedes agregarle envsubst.
kojiro
O vaya contenedor completo y coloque envsubst en un contenedor por sí mismo. Es un patrón común y una forma de vida si utiliza un sistema operativo como Atomic Host, CoreOS o RancherOS. Atomic específicamente ni siquiera permitirá que la raíz se meta con el sistema de archivos o lo que está instalado, tiene que usar un contenedor.
Kuberchaun
1
Tenga en cuenta que no reemplazará "todas" las variables de entorno, solo aquellas cuyo nombre coincida ^[[:alpha:]_][[:alnum:]_]*$en el entorno local POSIX.
Stéphane Chazelas
Parece ser muy sucinto, sin embargo, no necesariamente correcto con todos los valores de sustitución. No parece respetar los caracteres especiales XML.
EFraim
16

Esto no es muy bueno pero funciona

( echo "cat <<EOF" ; cat config.xml ; echo EOF ) | sh

Si estuviera en un script de shell, se vería así:

#! /bin/sh
cat <<EOF
<property>
    <name>instanceId</name>
    <value>$INSTANCE_ID</value>
</property>
EOF

Editar, segunda propuesta:

eval "echo \"$(cat config.xml)\""

Editar, no estrictamente relacionado con la pregunta, pero en caso de variables leídas del archivo:

(. .env && eval "echo \"$(cat config.xml)\"")
hschou
fuente
El problema con esto es que si el archivo contiene una línea EOF, el shell ejecutará las líneas restantes como comandos. Podríamos cambiar el separador a algo más largo o más complicado, pero todavía hay una posibilidad teórica de colisión. Y alguien podría hacer deliberadamente un archivo con el separador para ejecutar comandos.
ilkkachu
OK, intente esto: eval "echo \" $ (cat config.xml) \ ""
hschou
3
Intente poner algo como "; ls ;"dentro del archivo y evalvuelva a ejecutar ese comando :) Este es prácticamente el mismo problema que con los ataques de inyección SQL. Usted tiene que tener mucho cuidado al mezclar datos con el código (y eso es lo que son los comandos shell), a menos que seas muy , muy seguro de que nadie está tratando de hacer nada para estropear su día.
ilkkachu
No. "; ls;" No hará ningún daño.
hschou
3
@hschou, creo que ilkkachu quiso decir `"; ls ;"`: el formato del comentario se comió los backticks. Pero en realidad ese shoule debe estar `ls`aquí. El punto es que el contenido del archivo conduce a la ejecución de código arbitrario y no hay nada que pueda hacer al respecto.
Gilles 'SO- deja de ser malvado'
8

Si tiene Perl (pero no gettext y envsubst) puede hacer el reemplazo simple con un script corto:

$ export INSTANCE_ID=foo; export SERVICE_NAME=bar;
$ perl -pe 's/\$([_A-Z]+)/$ENV{$1}/g'  < config.xml
<property>
    <name>instanceId</name>
    <value>foo</value>
</property>
<property>
    <name>rootPath</name>
    <value>/services/bar</value>
</property>

Supuse que los nombres de las variables solo tendrán letras mayúsculas y guiones bajos, pero el primer patrón debería ser fácil de modificar según sea necesario. $ENV{...}hace referencia al entorno que Perl ve.

Si desea admitir la ${...}sintaxis o generar un error en las variables no definidas, necesitará más trabajo. Un equivalente cercano de gettext's envsubstsería:

perl -pe 's/\$(\{)?([a-zA-Z_]\w*)(?(1)\})/$ENV{$2}/g'

Aunque creo que alimentar variables como esa a través del entorno del proceso parece un poco dudoso en general: no se pueden usar variables arbitrarias en los archivos (ya que pueden tener significados especiales), y algunos de los valores podrían tener al menos semi- datos sensibles en ellos.

ilkkachu
fuente
Preferiría no usar Perl ya que se supone que es un contenedor acoplable, pero esa parece ser la mejor solución.
Robert Fraser
2
Consulte también perl -pe 's{\$(\{)?(\w+)(?(1)\})}{$ENV{$2} // $&}ge'para sustituir solo las variables que están definidas.
Stéphane Chazelas
1

¿Puedo sugerir mi propio script para esto?

https://github.com/rydnr/set-square/blob/master/.templates/common-files/process-file.sh

#!/bin/bash /usr/local/bin/dry-wit
# Copyright 2016-today Automated Computing Machinery S.L.
# Distributed under the terms of the GNU General Public License v3

function usage() {
cat <<EOF
$SCRIPT_NAME -o|--output output input
$SCRIPT_NAME [-h|--help]
(c) 2016-today Automated Computing Machinery S.L.
    Distributed under the terms of the GNU General Public License v3

Processes a file, replacing any placeholders with the contents of the
environment variables, and stores the result in the specified output file.

Where:
    * input: the input file.
    * output: the output file.
Common flags:
    * -h | --help: Display this message.
    * -v: Increase the verbosity.
    * -vv: Increase the verbosity further.
    * -q | --quiet: Be silent.
EOF
}

# Requirements
function checkRequirements() {
  checkReq envsubst ENVSUBST_NOT_INSTALLED;
}

# Error messages
function defineErrors() {
  export INVALID_OPTION="Unrecognized option";
  export ENVSUBST_NOT_INSTALLED="envsubst is not installed";
  export NO_INPUT_FILE_SPECIFIED="The input file is mandatory";
  export NO_OUTPUT_FILE_SPECIFIED="The output file is mandatory";

  ERROR_MESSAGES=(\
    INVALID_OPTION \
    ENVSUBST_NOT_INSTALLED \
    NO_INPUT_FILE_SPECIFIED \
    NO_OUTPUT_FILE_SPECIFIED \
  );

  export ERROR_MESSAGES;
}

## Parses the input
## dry-wit hook
function parseInput() {

  local _flags=$(extractFlags $@);
  local _flagCount;
  local _currentCount;

  # Flags
  for _flag in ${_flags}; do
    _flagCount=$((_flagCount+1));
    case ${_flag} in
      -h | --help | -v | -vv | -q)
         shift;
         ;;
      -o | --output)
         shift;
         OUTPUT_FILE="${1}";
         shift;
         ;;
    esac
  done

  # Parameters
  if [[ -z ${INPUT_FILE} ]]; then
    INPUT_FILE="$1";
    shift;
  fi
}

## Checking input
## dry-wit hook
function checkInput() {

  local _flags=$(extractFlags $@);
  local _flagCount;
  local _currentCount;
  logDebug -n "Checking input";

  # Flags
  for _flag in ${_flags}; do
    _flagCount=$((_flagCount+1));
    case ${_flag} in
      -h | --help | -v | -vv | -q | --quiet)
         ;;
      -o | --output)
         ;;
      *) logDebugResult FAILURE "fail";
         exitWithErrorCode INVALID_OPTION ${_flag};
         ;;
    esac
  done

  if [[ -z ${INPUT_FILE} ]]; then
    logDebugResult FAILURE "fail";
    exitWithErrorCode NO_INPUT_FILE_SPECIFIED;
  fi

  if [[ -z ${OUTPUT_FILE} ]]; then
      logDebugResult FAILURE "fail";
      exitWithErrorCode NO_OUTPUT_FILE_SPECIFIED;
  fi
}

## Replaces any placeholders in given file.
## -> 1: The file to process.
## -> 2: The output file.
## <- 0 if the file is processed, 1 otherwise.
## <- RESULT: the path of the processed file.
function replace_placeholders() {
  local _file="${1}";
  local _output="${2}";
  local _rescode;
  local _env="$(IFS=" \t" env | awk -F'=' '{printf("%s=\"%s\" ", $1, $2);}')";
  local _envsubstDecl=$(echo -n "'"; IFS=" \t" env | cut -d'=' -f 1 | awk '{printf("${%s} ", $0);}'; echo -n "'";);

  echo "${_env} envsubst ${_envsubstDecl} < ${_file} > ${_output}" | sh;
  _rescode=$?;
  export RESULT="${_output}";
  return ${_rescode};
}

## Main logic
## dry-wit hook
function main() {
  replace_placeholders "${INPUT_FILE}" "${OUTPUT_FILE}";
}
# vim: syntax=sh ts=2 sw=2 sts=4 sr noet
Jose San Leandro
fuente
0

De manera similar a la respuesta de Perl, la sustitución de variables de entorno se puede delegar a la CLI de PHP. La dependencia en PHP puede o no ser aceptable dependiendo de la pila tecnológica en uso.

php -r 'echo preg_replace_callback("/\\$([a-z0-9_]+)/i", function ($matches) { return getenv($matches[1]); }, fread(STDIN, 8192));' < input.file > output.file

Puede ir más allá y ponerlo en un script reutilizable, por ejemplo envsubst:

#!/usr/bin/env php
<?php

echo preg_replace_callback(
    '/\$(?<name>[a-z0-9_]+)/i',
    function ($matches) {
        return getenv($matches['name']);
    },
    file_get_contents('php://stdin')
);

El uso sería:

envsubst < input.file > output.file
Sergii Shymko
fuente