Cómo diferir la expansión variable

18

Quería inicializar algunas cadenas en la parte superior de mi script con variables que aún no se han establecido, como:

str1='I went to ${PLACE} and saw ${EVENT}'
str2='If you do ${ACTION} you will ${RESULT}'

y más tarde PLACE, EVENT, ACTION, y RESULTse establecerá. Quiero poder imprimir mis cadenas con las variables expandidas. Es mi única opcióneval ? Esto parece funcionar:

eval "echo ${str1}"

es esta norma? ¿Hay una mejor manera de hacer esto? Sería bueno no ejecutar evalteniendo en cuenta que las variables podrían ser cualquier cosa.

Aaron
fuente

Respuestas:

23

Con el tipo de entrada que muestra, la única forma de aprovechar la expansión de shell para sustituir los valores en una cadena es usar evalde alguna forma. Esto es seguro siempre que controle el valor de str1y puede garantizar que solo haga referencia a variables que se conocen como seguras (no contienen datos confidenciales) y no contiene ningún otro carácter especial de shell sin comillas. Debe ampliar la cadena entre comillas dobles o en un documento de aquí, de esa manera solamente "$\`son especiales (que necesita ser precedida por una \en str1).

eval "substituted=\"$str1\""

Sería mucho más robusto definir una función en lugar de una cadena.

fill_template () {
  sentence1="I went to ${PLACE} and saw ${EVENT}"
  sentence2="If you do ${ACTION} you will ${RESULT}"
}

Establezca las variables y luego llame a la función fill_templatepara establecer las variables de salida.

PLACE=Sydney; EVENT=fireworks
ACTION='not learn from history'; RESULT='have to relive history'
fill_template
echo "During my holidays, $sentence1."
echo "Cicero said: \"$sentence2\"."
Gilles 'SO- deja de ser malvado'
fuente
2
Buen trabajo utilizando una función para retrasar la evaluación y evitar la llamada explícita de evaluación.
Clayton Stanley
Buena solución, esto me ayudó mucho. ¡Gracias!
Stuart
8

A medida que entiendo su significado, no creo que ninguna de estas respuestas sea correcta. evalno es necesario de ninguna manera, ni tiene ninguna necesidad incluso de evaluar dos veces sus variables.

Es cierto, @Gilles se acerca mucho, pero no aborda el problema de posiblemente anular los valores y cómo deberían usarse si los necesita más de una vez. Después de todo, una plantilla debe usarse más de una vez, ¿verdad?

Creo que es más importante el orden en el que los evalúas. Considera lo siguiente:

PARTE SUPERIOR

Aquí establecerá algunos valores predeterminados y se preparará para imprimirlos cuando se llame ...

#!/bin/sh
    _top_of_script_pr() ( 
        IFS="$nl" ; set -f #only split at newlines and don't expand paths
        printf %s\\n ${strings}
    ) 3<<-TEMPLATES
        ${nl=
}
        ${PLACE:="your mother's house"}
        ${EVENT:="the unspeakable."}
        ${ACTION:="heroin"}
        ${RESULT:="succeed."}
        ${strings:="
            I went to ${PLACE} and saw ${EVENT}
            If you do ${ACTION} you will ${RESULT}
        "}
    #END
    TEMPLATES

MEDIO

Aquí es donde define otras funciones para llamar a su función de impresión en función de sus resultados ...

    EVENT="Disney on Ice."
    _more_important_function() { #...some logic...
        [ $((1+one)) -ne 2 ] && ACTION="remedial mathematics"
            _top_of_script_pr
    }
    _less_important_function() { #...more logic...
        one=2
        : "${ACTION:="calligraphy"}"
        _top_of_script_pr
    }

FONDO

Ya lo tiene todo configurado, así que aquí es donde ejecutará y extraerá sus resultados.

    _less_important_function
    : "${PLACE:="the cemetery"}" 
    _more_important_function
    : "${RESULT:="regret it."}" 
    _less_important_function    

RESULTADOS

Analizaré por qué en un momento, pero ejecutar lo anterior produce los siguientes resultados:

_less_important_function()'s primer intento:

Fui a la casa de tu madre y vi Disney on Ice.

Si haces caligrafía tendrás éxito.

luego _more_important_function():

Fui al cementerio y vi Disney on Ice.

Si haces matemáticas correctivas tendrás éxito.

_less_important_function() de nuevo:

Fui al cementerio y vi Disney on Ice.

Si haces matemáticas correctivas te arrepentirás.

CÓMO FUNCIONA:

La característica clave aquí es el concepto de conditional ${parameter} expansion.Puede establecer una variable en un valor solo si no está establecida o es nula utilizando el formulario:

${var_name: =desired_value}

Si, en cambio, desea establecer solo una variable no establecida, omitirá los :colonvalores nulos y permanecerá como está.

EN ALCANCE:

Puede notar eso en el ejemplo anterior $PLACEy $RESULTcambiarlo cuando se configura a través de parameter expansionaunque _top_of_script_pr()ya se haya llamado, presumiblemente configurándolos cuando se ejecuta. La razón por la que esto funciona es que _top_of_script_pr()es una ( subshelled )función: la incluí en parenslugar de la { curly braces }utilizada para los demás. Debido a que se llama en una subshell, cada variable que establece es locally scopedy cuando regresa a su shell principal esos valores desaparecen.

Pero cuando se _more_important_function()establece $ACTIONes globally scopedasí, afecta a la _less_important_function()'ssegunda evaluación de $ACTIONporque _less_important_function()establece $ACTIONsolo a través de${parameter:=expansion}.

:NULO

¿Y por qué utilizo el :colon?Pozo principal? La manpágina te dirá que : does nothing, gracefully., como ves, parameter expansiones exactamente lo que suena, expandsal valor de ${parameter}.So, cuando establecemos una variable con, ${parameter:=expansion}nos queda su valor, que el shell Intente ejecutar en línea. Si intentara ejecutarse the cemetery, solo te escupiría algunos errores. PLACE="${PLACE:="the cemetery"}"produciría los mismos resultados, pero también es redundante en este caso y preferí que el shell: ${did:=nothing, gracefully}.

Te permite hacer esto:

    echo ${var:=something or other}
    echo $var
something or other
something or other

AQUÍ-DOCUMENTOS

Y, por cierto, la definición en línea de una variable nula o sin definir también es la razón por la que funciona lo siguiente:

    <<HEREDOC echo $yo
        ${yo=yoyo}
    HEREDOC
yoyo

La mejor manera de pensar en un here-document es como un archivo real transmitido a un descriptor de archivo de entrada. Más o menos eso es lo que son, pero diferentes shells los implementan de manera ligeramente diferente.

En cualquier caso, si no cita el <<LIMITERflujo, lo transfiere y lo evalúa. Por lo expansion.tanto, declarar una variable en un here-documentpuede funcionar, pero solo a través de lo expansioncual lo limita a establecer solo variables que aún no están establecidas. Aún así, eso se adapta perfectamente a sus necesidades tal como las describió, ya que sus valores predeterminados siempre se establecerán cuando llame a la función de impresión de su plantilla.

POR QUÉ NO eval?

Bueno, el ejemplo que he presentado proporciona un medio seguro y efectivo de aceptar. parameters.Debido a que maneja el alcance, cada variable dentro de set via ${parameter:=expansion}se puede definir desde afuera. Entonces, si pones todo esto en un script llamado template_pr.sh y ejecutas:

 % RESULT=something_else template_pr.sh

Obtendrías:

Fui a la casa de tu madre y vi Disney on Ice

Si haces caligrafía, harás algo

Fui al cementerio y vi Disney on Ice

Si haces matemáticas correctivas, harás algo

Fui al cementerio y vi Disney on Ice

Si haces matemáticas correctivas, harás algo

Esto no funcionaría para aquellas variables que se configuraron literalmente en el script, como $EVENT, $ACTION,y$one, pero solo las definí de esa manera para demostrar la diferencia.

En cualquier caso, la aceptación de una entrada desconocida en una evaleddeclaración es inherentemente insegura, mientras que parameter expansionestá específicamente diseñada para hacerlo.

mikeserv
fuente
1

Puede usar marcadores de posición para plantillas de cadena en lugar de variables sin expandir. Esto se volverá complicado bastante rápido. Si lo que está haciendo es una plantilla muy pesada, es posible que desee considerar un idioma con una biblioteca de plantillas real.

format_template() {
    changed_str=$1

    for word in $changed_str; do
        if [[ $word == %*% ]]; then
            var="${word//\%/}"
            changed_str="${changed_str//$word/${!var}}"
        fi
    done
}

str1='I went to %PLACE% and saw %EVENT%'
PLACE="foo"
EVENT="bar"
format_template "$str1"
echo "$changed_str"

La desventaja de lo anterior es que la variable de plantilla debe ser su propia palabra (por ejemplo, no se puede hacer "%prefix%foo"). Esto podría solucionarse con algunas modificaciones, o simplemente codificando la variable de plantilla en lugar de que sea dinámica.

jordanm
fuente