Lire des valeurs dans une variable shell à partir d'un tuyau


205

J'essaie d'obtenir que bash traite les données de stdin qui sont acheminées, mais pas de chance. Ce que je veux dire, c'est qu'aucun des travaux suivants:

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=

où je veux que la sortie soit test=hello world. J'ai essayé de mettre des guillemets "" "$test"qui ne fonctionnent pas non plus.


1
Votre exemple .. echo "hello world" | test de lecture; echo test = $ test a bien fonctionné pour moi .. résultat: test = hello world; sous quel environnement fonctionne-t-il? J'utilise bash 4.2 ..
alex.pilon

Voulez-vous plusieurs lignes en une seule lecture? Votre exemple ne montre qu'une seule ligne, mais la description du problème n'est pas claire.
Charles Duffy

2
@ alex.pilon, j'utilise la version 4.2.25 de Bash, et son exemple ne fonctionne pas pour moi aussi. Peut-être s'agit-il d'une option d'exécution Bash ou d'une variable d'environnement? J'ai l'exemple ne fonctionne pas non plus avec Sh, alors peut-être que Bash peut essayer d'être compatible avec Sh?
Hibou57

2
@ Hibou57 - J'ai essayé à nouveau dans bash 4.3.25 et cela ne fonctionne plus. Ma mémoire est floue à ce sujet et je ne sais pas ce que j'ai pu faire pour le faire fonctionner.
alex.pilon

2
@ Hibou57 @ alex.pilon la dernière cmd d'une pipe devrait affecter les vars dans bash4> = 4.2 avec shopt -s lastpipe- tldp.org/LDP/abs/html/bashver4.html#LASTPIPEOPT
imz - Ivan Zakharyaschev

Réponses:


162

Utilisation

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

Vous pouvez tromper readen acceptant d'un tuyau comme celui-ci:

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

ou même écrire une fonction comme celle-ci:

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

Mais cela ne sert à rien - vos affectations de variables peuvent ne pas durer! Un pipeline peut générer un sous-shell, où l'environnement est hérité par valeur, et non par référence. C'est pourquoi readne se soucie pas de l'entrée d'un tuyau - c'est indéfini.

FYI, http://www.etalabs.net/sh_tricks.html est une collection astucieuse de la cruauté nécessaire pour lutter contre les bizarreries et les incompatibilités des coquilles de bourne, sh.


Vous pouvez faire durer l'affectation en faisant ceci à la place: `test =` `echo" hello world "| {lire le test; echo $ test; } `` `
Compholio

2
Essayons à nouveau (échapper apparemment aux backticks dans ce balisage est amusant):test=`echo "hello world" | { read test; echo $test; }`
Compholio

puis-je demander pourquoi vous avez utilisé {}au lieu de ()regrouper ces deux commandes?
Jürgen Paul

4
L'astuce ne consiste pas à readprendre l'entrée du pipe, mais à utiliser la variable dans le même shell qui exécute le read.
chepner

1
Obtenu bash permission deniedlorsque vous essayez d'utiliser cette solution. Mon cas est tout à fait différent, mais je ne pouvais pas trouver une réponse pour nulle part, ce qui a fonctionné pour moi était (un autre exemple , mais l' utilisation similaire): pip install -U echo $(ls -t *.py | head -1). Au cas où quelqu'un aurait un problème similaire et tombe sur cette réponse comme moi.
ivan_bilan

111

si vous voulez lire beaucoup de données et travailler sur chaque ligne séparément, vous pouvez utiliser quelque chose comme ceci:

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

si vous voulez diviser les lignes en plusieurs mots, vous pouvez utiliser plusieurs variables à la place de x comme ceci:

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

alternativement:

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

Mais dès que vous commencez à vouloir faire quelque chose de vraiment intelligent avec ce genre de chose, vous préférez un langage de script comme perl où vous pouvez essayer quelque chose comme ceci:

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

Il y a une courbe d'apprentissage assez abrupte avec perl (ou je suppose que n'importe lequel de ces langages) mais vous le trouverez beaucoup plus facile à long terme si vous voulez faire autre chose que le plus simple des scripts. Je recommanderais le livre de recettes Perl et, bien sûr, le langage de programmation Perl de Larry Wall et al.


8
"alternativement" est la bonne façon. Pas d'UUoC et pas de sous-coque. Voir BashFAQ / 024 .
pause jusqu'à nouvel ordre.

43

readne lira pas à partir d'un tuyau (ou peut-être le résultat est perdu car le tuyau crée un sous-shell). Vous pouvez cependant utiliser une chaîne ici dans Bash:

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

Mais voir la réponse de @ chepner pour plus d'informations lastpipe.


Nice et simple one-liner, facile à comprendre. Cette réponse a besoin de plus de votes positifs.
David Parks

42

Ceci est une autre option

$ read test < <(echo hello world)

$ echo $test
hello world

24
L'avantage significatif qui <(..)a plus $(..)est que <(..)renvoie chaque ligne à l'appelant dès que la commande qu'il exécute le rend disponible. $(..)attend cependant que la commande se termine et génère toutes ses sorties avant de mettre toute sortie à la disposition de l'appelant.
Derek Mahar

37

Je ne suis pas un expert en Bash, mais je me demande pourquoi cela n'a pas été proposé:

stdin=$(cat)

echo "$stdin"

Preuve à un revêtement que cela fonctionne pour moi:

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

4
C'est probablement parce que "read" est une commande bash, et cat est un binaire séparé qui sera lancé dans un sous-processus, il est donc moins efficace.
dj_segfault

10
Parfois, la simplicité et la clarté l'emportent sur l'efficacité :)
Rondo

5
certainement la réponse la plus simple
drwatsoncode

Le problème que j'ai rencontré avec cela est que si un tuyau n'est pas utilisé, le script se bloque.
Dale Anderson

@DaleA Bien sûr. Tout programme qui essaie de lire à partir d'une entrée standard "se bloquera" si rien n'y est introduit.
djanowski

27

bash4.2 introduit l' lastpipeoption, qui permet à votre code de fonctionner tel qu'il est écrit, en exécutant la dernière commande dans un pipeline dans le shell actuel, plutôt qu'un sous-shell.

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

2
ah! si bon, ça. si test dans un shell interactif, aussi: "set + m" (non requis dans un script .sh)
XXL

14

La syntaxe d'un canal implicite d'une commande shell dans une variable bash est

var=$(command)

ou

var=`command`

Dans vos exemples, vous redirigez des données vers une instruction d'affectation, qui n'attend aucune entrée.


4
Parce que $ () peut être imbriqué facilement. Pensez dans JAVA_DIR = $ (dirname $ (readlink -f $ (which java))), et essayez-le avec `. Vous devrez vous échapper trois fois!
albfan

8

La première tentative a été assez serrée. Cette variation devrait fonctionner:

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

et la sortie est:

test = bonjour le monde

Vous avez besoin d'accolades après le tuyau pour entourer l'affectation à tester et l'écho.

Sans les accolades, l'affectation à tester (après le tube) est dans un shell, et l'écho "test = $ test" est dans un shell séparé qui ne connaît pas cette affectation. C'est pourquoi vous obteniez "test =" dans la sortie au lieu de "test = hello world".


8

À mes yeux, la meilleure façon de lire depuis stdin dans bash est la suivante, qui vous permet également de travailler sur les lignes avant la fin de l'entrée:

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

1
Je suis presque devenu fou avant de trouver ça. Merci beaucoup pour le partage!
Samy Dindane

6

Parce que je tombe amoureux d'elle, je voudrais laisser une note. J'ai trouvé ce fil, car je dois réécrire un ancien script sh pour qu'il soit compatible POSIX. Cela signifie essentiellement pour contourner le problème de pipe / sous-shell introduit par POSIX en réécrivant le code comme ceci:

some_command | read a b c

dans:

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

Et un code comme celui-ci:

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

dans:

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

Mais ce dernier ne se comporte pas de la même manière sur une entrée vide. Avec l'ancienne notation, la boucle while n'est pas entrée sur une entrée vide, mais en notation POSIX c'est le cas! Je pense que c'est dû à la nouvelle ligne avant l'EOF, qui ne peut pas être supprimée. Le code POSIX qui se comporte plus comme l'ancienne notation ressemble à ceci:

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

Dans la plupart des cas, cela devrait être suffisant. Mais malheureusement, cela ne se comporte toujours pas exactement comme l'ancienne notation si some_command imprime une ligne vide. Dans l'ancienne notation, le corps while est exécuté et dans la notation POSIX, nous nous cassons devant le corps.

Une approche pour résoudre ce problème pourrait ressembler à ceci:

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 intelligent qui peut à la fois lire les données de PIPE et les arguments de ligne de commande:

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

Production:

$ bash test arg1 arg2
ARGS=arg1 arg2

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

Explication: Lorsqu'un script reçoit des données via un canal, stdin / proc / self / fd / 0 sera un lien symbolique vers un canal.

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

Sinon, il pointera vers le terminal actuel:

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

L' [[ -poption bash peut vérifier si c'est un tuyau ou non.

cat -lit le de stdin.

Si nous utilisons cat -quand il n'y en a pas stdin, il attendra pour toujours, c'est pourquoi nous le mettons à l'intérieur de la ifcondition.


Vous pouvez également utiliser /dev/stdince qui est un lien vers/proc/self/fd/0
Elie G.

3

Pipeter quelque chose dans une expression impliquant une affectation ne se comporte pas comme ça.

Essayez plutôt:

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

2

Le code suivant:

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

fonctionnera aussi, mais il ouvrira un autre nouveau sous-shell après le tuyau, où

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

habitude.


J'ai dû désactiver le contrôle des tâches pour utiliser la méthode de chepnars ( j'exécutais cette commande depuis le terminal):

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

Le manuel de Bash dit :

lastpipe

S'il est défini et que le contrôle des travaux n'est pas actif , le shell exécute la dernière commande d'un pipeline non exécuté en arrière-plan dans l'environnement shell actuel.

Remarque: le contrôle des travaux est désactivé par défaut dans un shell non interactif et vous n'avez donc pas besoin de l' set +mintérieur d'un script.


1

Je pense que vous essayez d'écrire un script shell qui pourrait recevoir des entrées de stdin. mais pendant que vous essayez de le faire en ligne, vous vous êtes perdu en essayant de créer cette variable test =. Je pense que cela n'a pas beaucoup de sens de le faire en ligne, et c'est pourquoi cela ne fonctionne pas comme vous l'attendez.

J'essayais de réduire

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

pour obtenir une ligne spécifique à partir de diverses entrées. pour que je puisse taper ...

cat program_file.c | line 34

j'ai donc besoin d'un petit programme shell capable de lire depuis stdin. comme vous le faites.

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

Voilà.


-1

Que dis-tu de ça:

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

Fondamentalement, un dupe de ma réponse ci-dessous.
djanowski

Peut-être, je dirais que le mien est légèrement plus propre et plus proche du code publié dans la question d'origine.
bingles
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.