Ler valores em uma variável shell de um pipe


205

Estou tentando obter bash para processar dados de stdin que é canalizado, mas sem sorte. O que quero dizer é que nenhum dos seguintes trabalhos:

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=

onde eu quero que a saída esteja test=hello world. Eu tentei colocar "" aspas "$test"que também não funcionam.


1
Seu exemplo .. eco "olá mundo" | teste de leitura; teste de eco = $ test funcionou bem para mim .. resultado: teste = olá mundo; Em que ambiente está executando isso? Estou usando o bash 4.2 ..
alex.pilon

Deseja várias linhas em uma única leitura? Seu exemplo mostra apenas uma linha, mas a descrição do problema não é clara.
Charles Duffy

2
@ alex.pilon, estou executando o Bash versão 4.2.25, e seu exemplo também não funciona para mim. Pode ser que seja uma questão de opção de tempo de execução do Bash ou variável de ambiente? Eu tenho o exemplo não funciona com Sh nem, então pode ser Bash pode tentar ser compatível com Sh?
Hibou57

2
@ Hibou57 - Tentei novamente no bash 4.3.25 e não funciona mais. Minha memória está confusa nisso e não tenho certeza do que posso ter feito para fazê-la funcionar.
22414 alex_pilon

2
@ Hibou57 @ alex.pilon o último cmd em um pipe deve afetar os vars no bash4> = 4.2 com shopt -s lastpipe- tldp.org/LDP/abs/html/bashver4.html#LASTPIPEOPT
imz - Ivan Zakharyaschev

Respostas:


162

Usar

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

Você pode enganar reada aceitação de um pipe como este:

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

ou mesmo escrever uma função como esta:

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

Mas não faz sentido - suas atribuições de variáveis ​​podem não durar! Um pipeline pode gerar um subshell, onde o ambiente é herdado por valor, não por referência. Isso é por queread isso não se incomoda com a entrada de um tubo - é indefinido.

FYI, http://www.etalabs.net/sh_tricks.html é uma coleção bacana do lixo necessário para combater as esquisitices e incompatibilidades das cascas de bourne, sh.


Você pode fazer a tarefa durar fazendo o seguinte: `test =` `echo" hello world "| {teste de leitura; eco $ test; } `` `
Compholio

2
Vamos tentar novamente (aparentemente escapar dos backticks nessa marcação é divertido):test=`echo "hello world" | { read test; echo $test; }`
Compholio

posso perguntar por que você usou em {}vez de ()agrupar esses dois comandos?
Jürgen Paul

4
O truque não está em fazer a readentrada do pipe, mas em usar a variável no mesmo shell que executa o read.
chepner

1
Tem bash permission deniedao tentar usar esta solução. Meu caso é bastante diferente, mas eu não poderia encontrar uma resposta para isso em qualquer lugar, o que funcionou para mim foi (um exemplo diferente mas o uso similar): pip install -U echo $(ls -t *.py | head -1). No caso, alguém já teve um problema semelhante e se deparou com essa resposta como eu.
ivan_bilan

111

se você quiser ler muitos dados e trabalhar em cada linha separadamente, poderá usar algo como isto:

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

se você quiser dividir as linhas em várias palavras, poderá usar várias variáveis ​​no lugar de x, assim:

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

alternativamente:

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

Mas assim que você começar a querer fazer algo realmente inteligente com esse tipo de coisa, é melhor usar uma linguagem de script como o perl, onde você pode tentar algo assim:

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

Há uma curva de aprendizado bastante íngreme com o perl (ou eu acho que qualquer uma dessas linguagens), mas você achará muito mais fácil a longo prazo se quiser fazer qualquer coisa, exceto o mais simples dos scripts. Eu recomendaria o Perl Cookbook e, é claro, a linguagem de programação Perl de Larry Wall et al.


8
"alternativamente" é o caminho correto. Nenhum UUoC e nenhum subshell. Consulte BashFAQ / 024 .
Pausado até novo aviso.

43

readnão lê de um tubo (ou possivelmente o resultado é perdido porque o tubo cria um subshell). No entanto, você pode usar uma string here no Bash:

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

Mas consulte a resposta do @ chepner para obter informações sobre lastpipe.


Simples e agradável, fácil de entender. Esta resposta precisa de mais votos.
David Parks

42

Essa é outra opção

$ read test < <(echo hello world)

$ echo $test
hello world

24
A vantagem significativa que <(..)resta $(..)é que <(..)retorna cada linha ao chamador assim que o comando que ele executa o disponibiliza. $(..), no entanto, aguarda o comando concluir e gerar toda sua saída antes de disponibilizar qualquer saída para o chamador.
Derek Mahar

37

Não sou especialista em Bash, mas me pergunto por que isso não foi proposto:

stdin=$(cat)

echo "$stdin"

Prova única de que funciona para mim:

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

4
Provavelmente porque "read" é ​​um comando bash, e cat é um binário separado que será lançado em um subprocesso, por isso é menos eficiente.
Dj_segfault

10
às vezes a simplicidade e a clareza superam a eficiência :)
Rondo

5
definitivamente a resposta mais direta
drwatsoncode 6/15/15

O problema que me deparei com isso é que, se um pipe não for usado, o script trava.
Dale Anderson

@DaleA Bem, é claro. Qualquer programa que tentar ler da entrada padrão "travará" se nada for alimentado.
djanowski

27

bash4.2 introduz a lastpipeopção, que permite que seu código funcione como gravado, executando o último comando em um pipeline no shell atual, em vez de em um subshell.

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

2
ah! tão bom isso. se estiver testando em um shell interativo, também: "set + m" (não é necessário em um script .sh)
XXL

14

A sintaxe de um canal implícito de um comando shell para uma variável bash é

var=$(command)

ou

var=`command`

Nos seus exemplos, você está canalizando dados para uma declaração de atribuição, que não espera nenhuma entrada.


4
Porque $ () pode ser aninhado facilmente. Pense em JAVA_DIR = $ (dirname $ (readlink -f $ (que java)))) e tente com `. Você precisará escapar três vezes!
Albfan #

8

A primeira tentativa foi bem próxima. Essa variação deve funcionar:

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

e a saída é:

test = olá mundo

Você precisa de chaves após o tubo para incluir a tarefa a ser testada e o eco.

Sem os chavetas, a tarefa a ser testada (após o pipe) está em um shell, e o eco "test = $ test" está em um shell separado que não conhece essa tarefa. É por isso que você estava recebendo "test =" na saída em vez de "test = hello world".


8

A meu ver, a melhor maneira de ler o stdin no bash é a seguinte, que também permite trabalhar nas linhas antes que a entrada termine:

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

1
Eu quase enlouqueci antes de encontrar isso. Muito obrigado por compartilhar!
Samy Dindane

6

Porque eu me apaixono por isso, gostaria de deixar uma nota. Encontrei este tópico, porque tenho que reescrever um script sh antigo para ser compatível com POSIX. Isso basicamente significa contornar o problema de pipe / subshell introduzido pelo POSIX, reescrevendo o código da seguinte maneira:

some_command | read a b c

para dentro:

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

E código como este:

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

para dentro:

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

Mas o último não se comporta da mesma maneira na entrada vazia. Com a notação antiga, o loop while não é inserido na entrada vazia, mas na notação POSIX, é! Eu acho que é devido à nova linha antes do EOF, que não pode ser omitida. O código POSIX que se comporta mais como a notação antiga é assim:

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

Na maioria dos casos, isso deve ser bom o suficiente. Infelizmente, isso ainda não se comporta exatamente como a notação antiga, se some_comand imprime uma linha vazia. Na notação antiga, o corpo while é executado e na notação POSIX, quebramos na frente do corpo.

Uma abordagem para corrigir isso pode ser assim:

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

Um script inteligente que pode ler dados do PIPE e dos argumentos da linha de comando:

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

Resultado:

$ bash test arg1 arg2
ARGS=arg1 arg2

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

Explicação: Quando um script recebe dados por canal, o stdin / proc / self / fd / 0 será um link simbólico para um canal.

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

Caso contrário, ele apontará para o terminal atual:

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

A [[ -popção bash pode verificar se é um tubo ou não.

cat -lê o de stdin.

Se usarmos cat -quando não houver stdin, ele esperará para sempre, é por isso que o colocamos dentro da ifcondição.


Você também pode usar o /dev/stdinque é um link para/proc/self/fd/0
Elie G.

3

Introduzir algo em uma expressão que envolva uma tarefa não se comporta assim.

Em vez disso, tente:

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

2

O código a seguir:

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

funcionará também, mas abrirá outra nova subcamada após o tubo, onde

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

não vai.


Eu tive que desativar o controle de tarefas para fazer uso do método chepnars (eu estava executando este comando no terminal):

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

O Bash Manual diz :

lastpipe

Se definido e o controle de tarefa não estiver ativo , o shell executará o último comando de um pipeline não executado em segundo plano no ambiente atual do shell.

Nota: o controle da tarefa é desativado por padrão em um shell não interativo e, portanto, você não precisa do set +mscript interno.


1

Eu acho que você estava tentando escrever um script de shell que poderia receber informações do stdin. mas enquanto você tenta fazê-lo em linha, você se perde tentando criar essa variável test =. Eu acho que não faz muito sentido fazê-lo em linha, e é por isso que não funciona da maneira que você espera.

Eu estava tentando reduzir

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

para obter uma linha específica de várias entradas. para que eu pudesse digitar ...

cat program_file.c | line 34

então eu preciso de um pequeno programa shell capaz de ler a partir de stdin. como você faz.

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

ai está.


-1

Que tal agora:

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

Basicamente, um engano da minha resposta abaixo.
djanowski

Talvez, eu diria que o meu é um pouco mais limpo e mais próximo do código postado na pergunta original.
Bingles
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.