Leer valores en una variable de shell desde una tubería


205

Estoy tratando de hacer que bash procese datos de stdin que se canalizan, pero no tuve suerte. Lo que quiero decir es que ninguno de los siguientes trabajos:

echo "hello world" | test=($(< /dev/stdin)); echo test=$test
test=

echo "hello world" | read test; echo test=$test
test=

echo "hello world" | test=`cat`; echo test=$test
test=

donde quiero que esté la salida test=hello world. He intentado poner "" comillas "$test"que tampoco funcionan.


1
Tu ejemplo ... echo "hola mundo" | prueba de lectura; echo test = $ test funcionó bien para mí .. resultado: test = hola mundo; ¿bajo qué entorno se está ejecutando esto? Estoy usando bash 4.2 ..
alex.pilon

¿Quieres múltiples líneas en una sola lectura? Su ejemplo solo muestra una línea, pero la descripción del problema no está clara.
Charles Duffy el

2
@ alex.pilon, estoy ejecutando Bash versión 4.2.25, y su ejemplo tampoco funciona para mí. ¿Puede ser una cuestión de una opción de tiempo de ejecución Bash o una variable de entorno? ¿Tengo el ejemplo no funciona con Sh tampoco, entonces puede ser que Bash pueda intentar ser compatible con Sh?
Hibou57

2
@ Hibou57 - Intenté esto nuevamente en bash 4.3.25 y ya no funciona. Mi memoria es borrosa al respecto y no estoy seguro de lo que podría haber hecho para que funcione.
alex.pilon

2
@ Hibou57 @ alex.pilon el último cmd en una tubería debería afectar los vars en bash4> = 4.2 con shopt -s lastpipe- tldp.org/LDP/abs/html/bashver4.html#LASTPIPEOPT
imz - Ivan Zakharyaschev

Respuestas:


162

Utilizar

IFS= read var << EOF
$(foo)
EOF

Usted puede engañar reada aceptar de una tubería de la siguiente manera:

echo "hello world" | { read test; echo test=$test; }

o incluso escribir una función como esta:

read_from_pipe() { read "$@" <&0; }

Pero no tiene sentido: ¡sus asignaciones variables pueden no durar! Una canalización puede generar una subshell, donde el entorno se hereda por valor, no por referencia. Es por eso readque no se molesta con la entrada de una tubería: no está definida.

Para su información, http://www.etalabs.net/sh_tricks.html es una colección ingeniosa de la migaja necesaria para combatir las rarezas e incompatibilidades de las conchas de bourne, sh.


Puede hacer que la tarea dure haciendo esto en su lugar: `test =` `echo" hello world "| {prueba de lectura; echo $ prueba; } `` `
Compholio

2
Intentemos esto de nuevo (aparentemente escapar de los backticks en este marcado es divertido):test=`echo "hello world" | { read test; echo $test; }`
Compholio

¿Puedo preguntar por qué usaste en {}lugar de ()agrupar esos dos comandos?
Jürgen Paul

44
El truco no está en hacer readuna entrada de entrada desde la tubería, sino en usar la variable en el mismo shell que ejecuta el read.
chepner

1
Lo conseguí bash permission deniedal intentar usar esta solución. Mi caso es muy diferente, pero no pude encontrar una respuesta para cualquier lugar, lo que funcionó para mí era (un ejemplo diferente, pero uso similar): pip install -U echo $(ls -t *.py | head -1). En caso de que alguien tenga un problema similar y se encuentre con esta respuesta como yo.
ivan_bilan

111

si desea leer muchos datos y trabajar en cada línea por separado, puede usar algo como esto:

cat myFile | while read x ; do echo $x ; done

si desea dividir las líneas en varias palabras, puede usar múltiples variables en lugar de x de esta manera:

cat myFile | while read x y ; do echo $y $x ; done

alternativamente:

while read x y ; do echo $y $x ; done < myFile

Pero tan pronto como comience a querer hacer algo realmente inteligente con este tipo de cosas, será mejor que use un lenguaje de script como perl donde pueda probar algo como esto:

perl -ane 'print "$F[0]\n"' < myFile

Hay una curva de aprendizaje bastante empinada con perl (o supongo que cualquiera de estos idiomas), pero a la larga le resultará mucho más fácil si desea hacer algo más que los guiones más simples. Recomiendo el Perl Cookbook y, por supuesto, el lenguaje de programación Perl de Larry Wall et al.


8
"alternativamente" es la forma correcta. Sin UUoC y sin subshell. Ver BashFAQ / 024 .
Pausado hasta nuevo aviso.

43

readno leerá de una tubería (o posiblemente el resultado se pierda porque la tubería crea una subshell). Sin embargo, puede usar una cadena here en Bash:

$ read a b c <<< $(echo 1 2 3)
$ echo $a $b $c
1 2 3

Pero vea la respuesta de @ chepner para obtener información sobre lastpipe.


Agradable y simple, fácil de entender. Esta respuesta necesita más votos a favor.
David Parks

42

Esta es otra opción

$ read test < <(echo hello world)

$ echo $test
hello world

24
La ventaja significativa que <(..)tiene $(..)es que <(..)devuelve cada línea a la persona que llama tan pronto como el comando que ejecuta la pone a disposición. $(..), sin embargo, espera a que el comando se complete y genere toda su salida antes de poner cualquier salida a disposición de la persona que llama.
Derek Mahar

37

No soy un experto en Bash, pero me pregunto por qué esto no se ha propuesto:

stdin=$(cat)

echo "$stdin"

Prueba única de que funciona para mí:

$ fortune | eval 'stdin=$(cat); echo "$stdin"'

44
Probablemente sea porque "leer" es un comando bash, y cat es un binario separado que se lanzará en un subproceso, por lo que es menos eficiente.
dj_segfault

10
a veces la simplicidad y la claridad triunfan sobre la eficiencia :)
Rondo

55
definitivamente la respuesta más directa
drwatsoncode

El problema con el que me encontré con esto es que si no se usa una tubería, el script se cuelga.
Dale Anderson

@DaleA Bueno, por supuesto. Cualquier programa que intente leer desde la entrada estándar se "colgará" si no se le alimenta nada.
djanowski

27

bash4.2 introduce la lastpipeopción, que permite que su código funcione como está escrito, ejecutando el último comando en una tubería en el shell actual, en lugar de un subshell.

shopt -s lastpipe
echo "hello world" | read test; echo test=$test

2
ah! tan bueno esto si se prueba en un shell interactivo, también: "set + m" (no requerido en un script .sh)
XXL

14

La sintaxis para una tubería implícita de un comando de shell en una variable bash es

var=$(command)

o

var=`command`

En sus ejemplos, está canalizando datos a una declaración de asignación, que no espera ninguna entrada.


44
Porque $ () se puede anidar fácilmente. Piense en JAVA_DIR = $ (dirname $ (readlink -f $ (which java))), y pruébelo con `. ¡Tendrás que escapar tres veces!
albfan

8

El primer intento estuvo bastante cerca. Esta variación debería funcionar:

echo "hello world" | { test=$(< /dev/stdin); echo "test=$test"; };

y la salida es:

prueba = hola mundo

Necesita llaves después de la tubería para encerrar la tarea a probar y el eco.

Sin las llaves, la asignación para probar (después de la tubería) está en un shell, y el eco "test = $ test" está en un shell separado que no conoce esa asignación. Es por eso que estaba obteniendo "test =" en la salida en lugar de "test = hello world".


8

En mi opinión, la mejor manera de leer stdin en bash es la siguiente, que también te permite trabajar en las líneas antes de que termine la entrada:

while read LINE; do
    echo $LINE
done < /dev/stdin

1
Casi me vuelvo loco antes de encontrar esto. ¡Muchas gracias por compartir!
Samy Dindane

6

Como me enamoro, me gustaría dejar una nota. Encontré este hilo, porque tengo que reescribir un antiguo script sh para que sea compatible con POSIX. Esto básicamente significa evitar el problema de tubería / subshell introducido por POSIX reescribiendo código como este:

some_command | read a b c

dentro:

read a b c << EOF
$(some_command)
EOF

Y código como este:

some_command |
while read a b c; do
    # something
done

dentro:

while read a b c; do
    # something
done << EOF
$(some_command)
EOF

Pero este último no se comporta igual en la entrada vacía. Con la antigua notación, el bucle while no se ingresa en una entrada vacía, ¡pero en la notación POSIX sí! Creo que se debe a la nueva línea antes de EOF, que no se puede omitir. El código POSIX que se comporta más como la antigua notación se ve así:

while read a b c; do
    case $a in ("") break; esac
    # something
done << EOF
$(some_command)
EOF

En la mayoría de los casos, esto debería ser lo suficientemente bueno. Pero desafortunadamente, esto todavía no se comporta exactamente como la antigua notación si some_command imprime una línea vacía. En la antigua notación, el cuerpo while se ejecuta y en la notación POSIX nos separamos frente al cuerpo.

Un enfoque para solucionar esto podría verse así:

while read a b c; do
    case $a in ("something_guaranteed_not_to_be_printed_by_some_command") break; esac
    # something
done << EOF
$(some_command)
echo "something_guaranteed_not_to_be_printed_by_some_command"
EOF

5

Un script inteligente que puede leer datos de PIPE y argumentos de línea de comando:

#!/bin/bash
if [[ -p /proc/self/fd/0 ]]
    then
    PIPE=$(cat -)
    echo "PIPE=$PIPE"
fi
echo "ARGS=$@"

Salida:

$ bash test arg1 arg2
ARGS=arg1 arg2

$ echo pipe_data1 | bash test arg1 arg2
PIPE=pipe_data1
ARGS=arg1 arg2

Explicación: Cuando una secuencia de comandos recibe datos a través de una tubería, entonces el stdin / proc / self / fd / 0 será un enlace simbólico a una tubería.

/proc/self/fd/0 -> pipe:[155938]

Si no, apuntará a la terminal actual:

/proc/self/fd/0 -> /dev/pts/5

La [[ -popción bash puede verificar si es una tubería o no.

cat -lee el de stdin.

Si usamos cat -cuando no hay stdin, esperará para siempre, por eso lo ponemos dentro de la ifcondición.


También puede usar /dev/stdincuál es un enlace a/proc/self/fd/0
Elie G.

3

Colocar algo en una expresión que involucra una tarea no se comporta así.

En cambio, intente:

test=$(echo "hello world"); echo test=$test

2

El siguiente código:

echo "hello world" | ( test=($(< /dev/stdin)); echo test=$test )

funcionará también, pero abrirá otro nuevo sub-shell después de la tubería, donde

echo "hello world" | { test=($(< /dev/stdin)); echo test=$test; }

no lo haré


Tuve que deshabilitar el control de trabajo para hacer uso del método de chepnars (estaba ejecutando este comando desde la terminal):

set +m;shopt -s lastpipe
echo "hello world" | read test; echo test=$test
echo "hello world" | test="$(</dev/stdin)"; echo test=$test

Bash Manual dice :

lastpipe

Si se establece, y el control de trabajo no está activo , el shell ejecuta el último comando de una tubería no ejecutada en segundo plano en el entorno actual del shell.

Nota: el control de trabajo está desactivado de forma predeterminada en un shell no interactivo y, por lo tanto, no necesita el set +mguión interno.


1

Creo que estabas tratando de escribir un script de shell que podría recibir información de stdin. pero mientras intentas hacerlo en línea, te perdiste tratando de crear esa prueba = variable. Creo que no tiene mucho sentido hacerlo en línea, y es por eso que no funciona de la manera esperada.

Estaba tratando de reducir

$( ... | head -n $X | tail -n 1 )

para obtener una línea específica de varias entradas. para poder escribir ...

cat program_file.c | line 34

así que necesito un pequeño programa de shell capaz de leer desde stdin. como tu lo haces.

22:14 ~ $ cat ~/bin/line 
#!/bin/sh

if [ $# -ne 1 ]; then echo enter a line number to display; exit; fi
cat | head -n $1 | tail -n 1
22:16 ~ $ 

Ahí tienes.


-1

Qué tal esto:

echo "hello world" | echo test=$(cat)

Básicamente un engañado de mi respuesta a continuación.
djanowski

Tal vez, diría que el mío es un poco más limpio y más cercano al código publicado en la pregunta original.
bingles
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.