Leggi i valori in una variabile shell da una pipe


205

Sto cercando di ottenere bash per elaborare i dati dallo stdin che viene reindirizzato, ma senza fortuna. Quello che voglio dire è nessuno dei seguenti lavori:

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=

dove voglio che sia l'output test=hello world. Ho provato a mettere "" virgolette in giro "$test"che non funziona neanche.


1
Il tuo esempio .. echo "ciao mondo" | leggi il test; echo test = $ test ha funzionato bene per me .. risultato: test = ciao mondo; in quale ambiente viene eseguito questo? Sto usando bash 4.2 ..
alex.pilon

Vuoi più righe in una sola lettura? Il tuo esempio mostra solo una riga, ma la descrizione del problema non è chiara.
Charles Duffy,

2
@ alex.pilon, sto eseguendo la versione 4.2.25 di Bash e il suo esempio non funziona anche per me. Potrebbe essere una questione di un'opzione di runtime Bash o di una variabile d'ambiente? Ho l'esempio non funziona nemmeno con Sh, quindi potrebbe essere Bash può provare a essere compatibile con Sh?
Hibou57,

2
@ Hibou57 - Ho provato di nuovo in Bash 4.3.25 e non funziona più. La mia memoria è confusa su questo e non sono sicuro di cosa avrei potuto fare per farlo funzionare.
alex.pilon,

2
@ Hibou57 @ alex.pilon l'ultimo cmd in una pipe dovrebbe influenzare i var in bash4> = 4.2 con shopt -s lastpipe- tldp.org/LDP/abs/html/bashver4.html#LASTPIPEOPT
imz - Ivan Zakharyaschev

Risposte:


162

Uso

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

È possibile ingannare readad accettare da un tubo in questo modo:

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

o addirittura scrivere una funzione come questa:

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

Ma non ha senso: le assegnazioni delle variabili potrebbero non durare! Una pipeline può generare una subshell, in cui l'ambiente è ereditato dal valore, non dal riferimento. Ecco perchéread non si preoccupa dell'input da una pipe: non è definito.

Cordiali saluti, http://www.etalabs.net/sh_tricks.html è una raccolta nifty dell'innesto necessario per combattere le stranezze e incompatibilità di bourne shell, sh.


Puoi fare in modo che il compito duri nel modo seguente: `test =` `echo" ciao mondo "| {leggi test; $ test; } ``
Compholio

2
Proviamo di nuovo (apparentemente sfuggire ai backtick in questo markup è divertente):test=`echo "hello world" | { read test; echo $test; }`
Compholio

posso chiederti perché hai usato {}invece di ()raggruppare quei due comandi?
Jürgen Paul,

4
Il trucco non sta nel readprendere input dalla pipe, ma nell'usare la variabile nella stessa shell che esegue il file read.
chepner,

1
Ottenuto bash permission deniedquando si tenta di utilizzare questa soluzione. Il mio caso è molto diverso, ma non sono riuscito a trovare una risposta per esso da nessuna parte, che cosa ha funzionato per me era (un esempio diverso, ma l'utilizzo simile): pip install -U echo $(ls -t *.py | head -1). Nel caso, qualcuno abbia mai avuto un problema simile e si imbatte in questa risposta come me.
Ivan_bilan,

111

se vuoi leggere molti dati e lavorare su ciascuna riga separatamente puoi usare qualcosa del genere:

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

se vuoi dividere le righe in più parole puoi usare più variabili al posto di x in questo modo:

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

in alternativa:

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

Ma non appena inizi a voler fare qualcosa di veramente intelligente con questo genere di cose, farai meglio ad usare un linguaggio di scripting come il Perl, dove potresti provare qualcosa del genere:

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

C'è una curva di apprendimento piuttosto ripida con il perl (o immagino che una di queste lingue) ma a lungo andare sarà molto più semplice se vuoi fare altro che il più semplice degli script. Consiglierei il Perl Cookbook e, ovviamente, The Perl Programming Language di Larry Wall et al.


8
"alternativamente" è il modo corretto. Nessun UUoC e nessuna subshell. Vedi BashFAQ / 024 .
In pausa fino a nuovo avviso.

43

readnon leggerà da una pipe (o forse il risultato andrà perso perché la pipe crea una subshell). Puoi, tuttavia, usare una stringa qui in Bash:

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

Ma vedi la risposta di @ chepner per informazioni su lastpipe.


One-liner bello e semplice, facile da capire. Questa risposta richiede più voti.
David Parks,

42

Questa è un'altra opzione

$ read test < <(echo hello world)

$ echo $test
hello world

24
Il vantaggio significativo che <(..)ha $(..)è che <(..)restituisce ogni linea al chiamante non appena il comando che esegue lo rende disponibile. $(..), tuttavia, attende che il comando venga completato e generato tutto il suo output prima di rendere disponibile qualsiasi output al chiamante.
Derek Mahar,

37

Non sono un esperto di Bash, ma mi chiedo perché questo non sia stato proposto:

stdin=$(cat)

echo "$stdin"

Prova di una riga che funziona per me:

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

4
Questo probabilmente perché "read" è un comando bash e cat è un file binario separato che verrà avviato in un sottoprocesso, quindi è meno efficiente.
dj_segfault,

10
a volte semplicità e chiarezza vincono l'efficienza :)
Rondo,

5
sicuramente la risposta più diretta
drwatsoncode

Il problema con cui mi sono imbattuto in questo è che se non viene utilizzata una pipe, lo script si blocca.
Dale Anderson,

@DaleA Beh, certo. Qualsiasi programma che tenta di leggere dall'input standard si "bloccherà" se non gli viene fornito nulla.
Djanowski,

27

bash4.2 introduce l' lastpipeopzione, che consente al codice di funzionare come scritto, eseguendo l'ultimo comando in una pipeline nella shell corrente, anziché in una shell secondaria.

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

2
ah! così buono, questo. se si esegue il test in una shell interattiva, anche: "set + m" (non richiesto in uno script .sh)
XXL

14

La sintassi per una pipe implicita da un comando shell in una variabile bash è

var=$(command)

o

var=`command`

Nei tuoi esempi, stai eseguendo il piping dei dati a un'istruzione di assegnazione, che non prevede alcun input.


4
Perché $ () può essere nidificato facilmente. Pensa a JAVA_DIR = $ (dirname $ (readlink -f $ (quale java))) e provalo con `. Dovrai scappare tre volte!
albfan,

8

Il primo tentativo è stato abbastanza vicino. Questa variazione dovrebbe funzionare:

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

e l'output è:

test = ciao mondo

Sono necessarie parentesi graffe dopo il tubo per racchiudere il compito da testare e l'eco.

Senza le parentesi graffe, l'assegnazione da testare (dopo la pipe) è in una shell e l'eco "test = $ test" è in una shell separata che non conosce tale assegnazione. Ecco perché stavi ottenendo "test =" nell'output anziché "test = ciao mondo".


8

Ai miei occhi il modo migliore per leggere da stdin in bash è il seguente, che ti consente anche di lavorare sulle linee prima della fine dell'input:

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

1
Sono quasi impazzito prima di trovarlo. Grazie mille per la condivisione!
Samy Dindane,

6

Perché mi innamoro di questo, vorrei lasciare un messaggio. Ho trovato questo thread, perché devo riscrivere un vecchio script sh per essere compatibile con POSIX. Ciò significa sostanzialmente aggirare il problema pipe / subshell introdotto da POSIX riscrivendo il codice in questo modo:

some_command | read a b c

in:

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

E codice come questo:

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

in:

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

Ma quest'ultimo non si comporta allo stesso modo con input vuoti. Con la vecchia notazione il ciclo while non viene inserito su input vuoto, ma nella notazione POSIX lo è! Penso che sia dovuto alla nuova riga prima dell'EOF, che non può essere omessa. Il codice POSIX che si comporta più come la vecchia notazione è simile al seguente:

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

Nella maggior parte dei casi questo dovrebbe essere abbastanza buono. Ma sfortunatamente questo non si comporta esattamente come la vecchia notazione se some_command stampa una riga vuota. Nella vecchia notazione viene eseguito il corpo while e nella notazione POSIX ci rompiamo davanti al corpo.

Un approccio per risolvere questo problema potrebbe apparire così:

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

Uno script intelligente in grado di leggere sia i dati da PIPE sia gli argomenti della riga di comando:

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

Produzione:

$ bash test arg1 arg2
ARGS=arg1 arg2

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

Spiegazione: Quando uno script riceve dati tramite pipe, lo stdin / proc / self / fd / 0 sarà un collegamento simbolico a una pipe.

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

In caso contrario, punterà al terminale corrente:

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

L' [[ -popzione bash può verificare che sia pipe o no.

cat -legge il da stdin.

Se usiamo cat -quando non c'è stdin, aspetterà per sempre, ecco perché lo mettiamo nella ifcondizione.


Puoi anche usare /dev/stdinun link a/proc/self/fd/0
Elie G.

3

Il piping di qualcosa in un'espressione che coinvolge un compito non si comporta così.

Invece, prova:

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

2

Il seguente codice:

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

funzionerà anche, ma aprirà un'altra nuova shell secondaria dopo la pipe, dove

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

non lo faranno.


Ho dovuto disabilitare il controllo del lavoro per utilizzare il metodo chepnars (stavo eseguendo questo comando dal terminale):

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

Se impostato e il controllo del lavoro non è attivo , la shell esegue l'ultimo comando di una pipeline non eseguita in background nell'ambiente di shell corrente.

Nota: il controllo del lavoro è disattivato per impostazione predefinita in una shell non interattiva e quindi non è necessario l' set +minterno di uno script.


1

Penso che stavi provando a scrivere uno script di shell che potesse ricevere input dallo stdin. ma mentre lo stai provando a farlo in linea, ti sei perso cercando di creare quella variabile test =. Penso che non abbia molto senso farlo in linea, ed è per questo che non funziona come ti aspetti.

Stavo cercando di ridurre

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

per ottenere una riga specifica da vari input. così potrei digitare ...

cat program_file.c | line 34

quindi ho bisogno di un piccolo programma shell in grado di leggere da stdin. come fai tu.

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

ecco qua.


-1

Cosa ne pensi di questo:

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

Fondamentalmente un imbroglione dalla mia risposta qui sotto.
Djanowski,

Forse, direi che il mio è leggermente più pulito e più vicino al codice pubblicato nella domanda originale.
Bingles
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.