Guardar la salida del comando que modifica el entorno en una variable

8

¿Cómo guardar la salida de un comando que modifica el entorno en una variable?

Estoy usando bash shell.

Supongamos que tengo:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }

Y ahora, puedo usar comandos para ejecutar f:

$ a=0; b=0; f; echo $a; echo $b; echo $c
Sat Jun 28 21:27:08 CEST 2014: 3 4
3
4

pero me gustaría guardar la salida de fa variable c, así que intenté:

a=0; b=0; c=""; c=$(f); echo $a; echo $b; echo $c

pero desafortunadamente, tengo:

0
0
Sat Jun 28 21:28:03 CEST 2014: 3 4

así que no tengo ningún cambio de entorno aquí.

¿Cómo guardar la salida del comando (no solo la función) en variable y guardar los cambios ambientales?

Sé que $(...)abre una nueva subshell y ese es el problema, pero ¿es posible hacer alguna solución?

faramir
fuente
Bien, entonces el problema es eso $ay $bson variables locales en su ffunción. Podrías export, pero eso parece incompleto.
HalosGhost
1
@HalosGhost: No, no lo creo. Mire el primer ejemplo: los …; f; echo $a; …resultados 3se repiten, por lo que fse modifica una variable de shell (y no solo su propia variable local).
Scott

Respuestas:

2

Si está usando Bash 4 o posterior, puede usar coprocesos :

function f () { a=3; b=4 ; echo "`date`: $a $b"; }
coproc cat
f >&${COPROC[1]}
exec {COPROC[1]}>&-
read c <&${COPROC[0]}
echo a $a
echo b $b
echo c $c

saldrá

a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4

coproccrea un nuevo proceso que ejecuta un comando dado (aquí, cat). Guarda el PID COPROC_PIDy los descriptores de archivos de entrada / salida estándar en una matriz COPROC(como pipe(2), o vea aquí o aquí ).

Aquí ejecutamos la función con salida estándar apuntando a nuestro coproceso en ejecución cat, y luego readdesde él. Como catsolo escupe su entrada de vuelta, obtenemos la salida de la función en nuestra variable. exec {COPROC[1]}>&-solo cierra el descriptor de archivo para que catno siga esperando para siempre.


Tenga en cuenta que readsolo toma una línea a la vez. Puede usar mapfilepara obtener una matriz de líneas, o simplemente usar el descriptor de archivo, sin embargo, desea usarlo de una manera diferente.

exec {COPROC[1]}>&-obras en las versiones actuales de Bash, pero las versiones anteriores de la serie 4 requieren que guarde el descriptor de archivo en una variable simple primero: fd=${COPROC[1]}; exec {fd}>&-. Si su variable no está configurada, cerrará la salida estándar.


Si está utilizando una versión de Bash de la serie 3, puede obtener el mismo efecto mkfifo, pero no es mucho mejor que usar un archivo real en ese punto.

Michael Homer
fuente
Solo puedo usar bash 3.1. Las tuberías son difíciles de usar. ¿Mktemp y es la única opción? ¿No hay aquí una sintaxis u opción especial para ejecutar comandos y obtener resultados, pero con cambios en el entorno?
faramir
Puede usar mkfifo para hacer una tubería con nombre como sustituto de coproc, que no implica realmente escribir los datos en el disco. Con eso redirigiría la salida al fifo y la entrada desde ella en lugar del coproceso. De otra manera no.
Michael Homer
+1 por adaptar mi respuesta para no usar un archivo. Pero, en mis pruebas, el exec {COPROC[1]}>&-comando (al menos a veces) termina el coproceso de inmediato , por lo que su salida ya no está disponible para ser leída. coproc { cat; sleep 1; }Parece funcionar mejor. (Es posible que necesite aumentar el 1si está leyendo más de una línea.)
Scott
1
Hay un significado convencional y bien entendido de "archivo real" en uso aquí que deliberadamente ignora.
Michael Homer
1
"Archivo real" significa un archivo en el disco , tal como lo entienden todos aquí, excepto, supuestamente, usted. Nadie ha implicado ni permitido que un lector razonable infiera que ninguno de los dos era igual a argumentos o variables de entorno. No estoy interesado en continuar esta discusión.
Michael Homer
2

Si ya cuenta con el cuerpo de fejecución en el mismo shell que la persona que llama y, por lo tanto, puede modificar variables como ay b, ¿por qué no hacer que la función simplemente se establezca ctambién? En otras palabras:

$ function f () { a=3; b=4 ; c=$(echo "$(date): $a $b"); }
$ a=0; b=0; f; echo $a; echo $b; echo $c
3
4
Mon Jun 30 14:32:00 EDT 2014: 3 4

Una posible razón puede ser que la variable de salida necesita tener diferentes nombres (c1, c2, etc.) cuando se llama en diferentes lugares, pero puede abordar eso haciendo que la función establezca c_TEMP y que los llamadores lo hagan, c1=$c_TEMPetc.

Godlygeek
fuente
1

Es un Kluge, pero intenta

f > c.file
c=$(cat c.file)

(y, opcionalmente rm c.file).

Scott
fuente
Gracias. Esa es una respuesta simple y funciona, pero tiene algunos inconvenientes. Sé que puedo usar mktemp, pero me gustaría no crear archivos adicionales.
faramir
1

Simplemente asigne todas las variables y escriba la salida al mismo tiempo.

f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }

Ahora si lo haces:

f ; echo "$a $b $c"

Su salida es:

Tue Jul  1 04:58:17 PDT 2014: 3 4
3 4 Tue Jul  1 04:58:17 PDT 2014: 3 4

Tenga en cuenta que este es un código portátil POSIX completo. Inicialmente configuré c=la ''cadena nula porque la expansión de parámetros limita la asignación de variables concurrentes + evaluación a valores numéricos (similares $((var=num))) o nulos o inexistentes, o, en otras palabras, no puede establecer y evaluar una variable a una cadena arbitraria al mismo tiempo si esa variable ya tiene asignado un valor. Así que solo me aseguro de que esté vacío antes de intentarlo. Si no vacíe cantes de intentar asignarlo, la expansión solo devolverá el valor anterior.

Solo para demostrar:

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2

newvalno está asignado a en $clínea porque oldvalse expande en ${word}, mientras que la asignación $((aritmética en línea siempre ocurre. Pero si no tiene y está vacío o desarmado ...=))$c oldval

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a)) 
    c= a=$((a+a))
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval 8

... luego newvalse asigna y se expande a la vez $c.

Todas las demás formas de hacerlo implican alguna forma de evaluación secundaria. Por ejemplo, supongamos que deseo asignar la salida de f()una variable nombrada nameen un punto y varen otro. Como está escrito actualmente, esto no funcionará sin establecer la var en el alcance de la persona que llama. Sin embargo, una forma diferente podría verse así:

f(){ fout_name= fout= set -- "${1##[0-9]*}" "${1%%*[![:alnum:]]*}"
    (: ${2:?invalid or unspecified param - name set to fout}) || set --
    export "${fout_name:=${1:-fout}}=${fout:=$(date): $((a=${a:-50}+1)) $((b=${b:-100}-4))}"
    printf %s\\n "$fout"
}

f &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$a" "$b"

He proporcionado un ejemplo mejor formateado a continuación, pero, llamado como arriba, el resultado es:

sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
51
96

O con diferentes $ENVo argumentos:

b=9 f myvar &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$myvar" \
        "$a" "$b"

###OUTPUT###

Tue Jul  1 19:56:42 PDT 2014: 52 5
myvar
Tue Jul  1 19:56:42 PDT 2014: 52 5
Tue Jul  1 19:56:42 PDT 2014: 52 5
52
5

Probablemente, lo más difícil de hacer bien cuando se trata de evaluar dos veces es asegurarse de que las variables no rompan las comillas y ejecuten código aleatorio. Cuantas más veces se evalúa una variable, más difícil se vuelve. La expansión de parámetros ayuda mucho aquí, y el uso exporten lugar de evales mucho más seguro.

En el ejemplo anterior, f()primero asigna $foutla ''cadena nula y luego establece los parámetros posicionales para probar nombres de variables válidos. Si ambas pruebas no pasan, se emite un mensaje stderry foutse le asigna el valor predeterminado de $fout_name. Sin embargo, independientemente de las pruebas, $fout_namesiempre se asigna a uno fouto al nombre que especifique $fouty, opcionalmente, a su nombre especificado siempre se le asigna el valor de salida de la función. Para demostrar esto escribí este pequeño forbucle:

for v in var '' "wr;\' ong"
    do sleep 10 &&
        a=${a:+$((a*2))} f "$v" || break
    echo "${v:-'' #null}" 
    printf '#\t"$%s" = '"'%s'\n" \
        a "$a" b "$b" \
        fout_name "$fout_name" \
        fout "$fout" \
        '(eval '\''echo "$'\''"$fout_name"\")' \
            "$(eval 'echo "$'"$fout_name"\")"
 done

Juega alrededor de algunos con nombres de variables y expansiones de parámetros. Si tiene alguna pregunta, solo pregunte. Eso solo ejecuta las mismas pocas líneas en la función ya representada aquí. Vale la pena mencionar al menos que las variables $ay se $bcomportan de manera diferente dependiendo de si están definidas en la invocación o si ya están configuradas. Aún así, el forhace casi nada más que formatear el conjunto de datos y proporcionado por f(). Echar un vistazo:

###OUTPUT###

Wed Jul  2 02:50:17 PDT 2014: 51 96
var
#       "$a" = '51'
#       "$b" = '96'
#       "$fout_name" = 'var'
#       "$fout" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:27 PDT 2014: 103 92
'' #null
#       "$a" = '103'
#       "$b" = '92'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:37 PDT 2014: 207 88
wr;\' ong
#       "$a" = '207'
#       "$b" = '88'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
mikeserv
fuente