Como ler um arquivo ou STDIN no Bash?

244

O seguinte script Perl ( my.pl) pode ser lido no arquivo args da linha de comando ou no STDIN:

while (<>) {
   print($_);
}

perl my.pllerá de STDIN, enquanto perl my.pl a.txtlerá de a.txt. Isso é muito conveniente.

Quer saber se há um equivalente no Bash?

Dagang
fonte

Respostas:

409

A solução a seguir lê de um arquivo se o script for chamado com um nome de arquivo como o primeiro parâmetro, $1caso contrário, da entrada padrão.

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

A substituição ${1:-...}leva, $1se definida, caso contrário, o nome do arquivo da entrada padrão do próprio processo é usado.

Fritz G. Mehner
fonte
1
Bom, funciona. Outra pergunta é por que você adiciona uma cotação para isso? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang
15
O nome do arquivo que você fornece na linha de comando pode ter espaços em branco.
Fritz G. Mehner
3
Existe alguma diferença entre usar /proc/$$/fd/0e /dev/stdin? Notei que o último parece ser mais comum e parece mais direto.
knowah
19
Melhor adicionar -rao seu readcomando, para que ele não coma acidentalmente \ caracteres; use while IFS= read -r linepara preservar os espaços em branco iniciais e finais.
mklement0
1
@ NeDark: Isso é curioso; Acabei de verificar que ele funciona nessa plataforma, mesmo ao usar /bin/sh- você está usando um shell diferente de bashou sh?
precisa saber é o seguinte
119

Talvez a solução mais simples seja redirecionar stdin com um operador de redirecionamento de mesclagem:

#!/bin/bash
less <&0

Stdin é o descritor de arquivo zero. O exemplo acima envia a entrada canalizada para o seu script bash para o stdin de less.

Leia mais sobre o redirecionamento de descritor de arquivo .

Ryan Ballantyne
fonte
1
Eu gostaria de ter mais votos positivos para dar a você, estou procurando isso há anos.
Marcus Downing
13
Não há nenhum benefício em usar <&0nesta situação - seu exemplo funcionará da mesma forma com ou sem ela - aparentemente, as ferramentas que você chama de dentro de um script bash por padrão veem o mesmo stdin que o próprio script (a menos que o script o consuma primeiro).
mklement0
@ mkelement0 Então, se uma ferramenta lê metade do buffer de entrada, a próxima ferramenta que eu chamo fica com o resto?
Asad Saeeduddin
"Faltando nome do arquivo (" less --help "for help)" quando faço isso ... Ubuntu 16.04
OmarOthman
5
onde está a parte "ou do arquivo" nesta resposta?
Sebastian
84

Aqui está a maneira mais simples:

#!/bin/sh
cat -

Uso:

$ echo test | sh my_script.sh
test

Para atribuir stdin à variável, você pode usar: STDIN=$(cat -)ou simplesmente STDIN=$(cat)como o operador não é necessário (conforme o comentário @ mklement0 ).


Para analisar cada linha da entrada padrão , tente o seguinte script:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

Para ler do arquivo ou stdin (se o argumento não estiver presente), você pode estendê-lo para:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

Notas:

- read -r- Não trate um caractere de barra invertida de maneira especial. Considere cada barra invertida como parte da linha de entrada.

- Sem configuração IFS, por padrão, as seqüências Spacee Tabno início e no final das linhas são ignoradas (aparadas).

- Use em printfvez de echopara evitar a impressão de linhas vazias quando a linha consistir em uma única -e, -nou -E. No entanto, há uma solução alternativa usando o env POSIXLY_CORRECT=1 echo "$line"que executa seu GNU externoecho que o suporta. Veja: Como eco "-e"?

Veja: Como ler stdin quando nenhum argumento é passado?no stackoverflow SE

kenorb
fonte
Você poderia simplificar [ "$1" ] && FILE=$1 || FILE="-"para FILE=${1:--}. (Quibble: melhor evitar todos os-maiúsculas shell variáveis para colisões de nomes evitar com ambiente variáveis.)
mklement0
O prazer é meu; na verdade, ${1:--} é compatível com POSIX, portanto deve funcionar em todos os shells semelhantes a POSIX. O que não funcionará em todos esses shells é a substituição do processo ( <(...)); funcionará no bash, ksh, zsh, mas não no traço, por exemplo. Além disso, é melhor adicionar -rao seu readcomando, para que ele não coma acidentalmente \ caracteres; preceda IFS= para preservar os espaços em branco iniciais e finais.
mklement0
4
Na verdade, seu código ainda quebra por causa de echo: se uma linha consiste em -e, -nou -E, ela não será mostrada. Para corrigir isso, você deve usar printf: printf '%s\n' "$line". Eu não o incluí na minha edição anterior ... muitas vezes minhas edições são revertidas quando eu corrigo esse erro :(.
58550 Gnourf_gniourf #
1
Não, não falha. E o --é inútil se primeiro argumento é'%s\n'
gniourf_gniourf
1
Sua resposta é ótima para mim (quero dizer, não há bugs ou recursos indesejados dos quais conheço mais) - embora ela não trate vários argumentos como o Perl. De fato, se você quiser lidar com vários argumentos, acabará escrevendo a excelente resposta de Jonathan Leffler - na verdade, a sua seria melhor, pois você usaria IFS=com reade printfnão echo. :).
gniourf_gniourf
19

Eu acho que este é o caminho direto:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5
Amir Mehler
fonte
4
Isso não se encaixa no requisito do pôster para ler stdin ou um argumento de arquivo, apenas lê stdin.
Nash
2
Deixando @ objeção válida de Nash aparte: readlê de stdin por padrão , então não há nenhuma necessidade para < /dev/stdin.
mklement0
13

A echosolução adiciona novas linhas sempre que IFSinterrompe o fluxo de entrada. A resposta do @ fgm pode ser modificada um pouco:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
David Souther
fonte
Você poderia explicar o que quer dizer com "a solução echo adiciona novas linhas sempre que o IFS interrompe o fluxo de entrada"? Em caso você estava referindo-se a read's comportamento: enquanto read que potencialmente dividida em várias fichas pelos caracteres. contido em $IFS, ele retornará apenas um único token se você especificar apenas um único nome de variável (mas, por padrão, apara e espaços em branco à esquerda e à direita).
mklement0
@ mklement0 Eu concordo 100% com você sobre o comportamento de reade $IFS- echoele próprio adiciona novas linhas sem a -nbandeira. "O utilitário echo grava todos os operandos especificados, separados por caracteres em branco (` ') e seguidos por um caractere de nova linha (`\ n') na saída padrão."
David Souther
Entendi. No entanto, para emular o loop Perl, você precisa da trilha \nadicionada por echo: O Perl $_ inclui a linha que termina \nna linha lida, enquanto o bash readnão. (No entanto, como @gniourf_gniourf aponta em outro lugar, a abordagem mais robusta é usar printf '%s\n'em vez disso echo).
precisa saber é o seguinte
8

O loop Perl na pergunta lê de todos os os argumentos de nome de arquivo na linha de comando ou da entrada padrão se nenhum arquivo for especificado. As respostas que vejo parecem processar um único arquivo ou entrada padrão se não houver um arquivo especificado.

Embora muitas vezes ridicularizado com precisão como UUOC (uso inútil de cat), há momentos em que caté a melhor ferramenta para o trabalho e é discutível que este seja um deles:

cat "$@" |
while read -r line
do
    echo "$line"
done

A única desvantagem disso é que ele cria um pipeline em execução em um sub-shell, para que coisas como atribuições de variáveis ​​no whileloop não sejam acessíveis fora do pipeline. A bashmaneira de contornar isso é Substituição de Processo :

while read -r line
do
    echo "$line"
done < <(cat "$@")

Isso deixa o whileloop em execução no shell principal, para que as variáveis ​​definidas no loop sejam acessíveis fora do loop.

Jonathan Leffler
fonte
1
Excelente ponto sobre vários arquivos. Não sei quais seriam as implicações de recursos e desempenho, mas se você não estiver no bash, ksh ou zsh e, portanto, não puder usar a substituição de processos, tente um documento aqui com substituição de comandos (distribuído por 3 linhas) >>EOF\n$(cat "$@")\nEOF. Finalmente, uma queixa: while IFS= read -r lineé uma melhor aproximação do que while (<>)faz no Perl (preserva os espaços em branco à esquerda e à direita - embora o Perl também mantenha a trilha \n).
mklement0
4

O comportamento de Perl, com o código fornecido no OP, pode receber nenhum ou vários argumentos, e se um argumento é um único hífen, -isso é entendido como stdin. Além disso, sempre é possível ter o nome do arquivo $ARGV. Nenhuma das respostas dadas até agora realmente imita o comportamento de Perl nesses aspectos. Aqui está uma possibilidade pura do Bash. O truque é usar execadequadamente.

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

O nome do arquivo está disponível em $1.

Se nenhum argumento for fornecido, definimos artificialmente -como o primeiro parâmetro posicional. Em seguida, fazemos um loop nos parâmetros. Se um parâmetro não for -, redirecionamos a entrada padrão do nome do arquivo com exec. Se esse redirecionamento for bem-sucedido, fazemos um loop com um whileloop. Estou usando a REPLYvariável padrão e, neste caso, você não precisa redefinir IFS. Se você quiser outro nome, deverá redefinir IFSassim (a menos, é claro, que não queira e saiba o que está fazendo):

while IFS= read -r line; do
    printf '%s\n' "$line"
done
gniourf_gniourf
fonte
2

Mais precisamente...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
sorpigal
fonte
2
Presumo que este seja essencialmente um comentário em stackoverflow.com/a/6980232/45375 , não uma resposta. Para tornar o comentário explícito: adicionar IFS=e -r ao readcomando garante que cada linha seja lida sem modificação (incluindo espaços em branco à esquerda e à direita).
mklement0
2

Por favor, tente o seguinte código:

while IFS= read -r line; do
    echo "$line"
done < file
Webthusiast
fonte
1
Observe que, mesmo alterado, isso não será lido da entrada padrão ou de vários arquivos, portanto, não é uma resposta completa para a pergunta. (É também surpreendente ver duas edições em questão de minutos, mais de 3 anos após a resposta foi submetida em primeiro lugar.)
Jonathan Leffler
@ JonathanLeffler, desculpe-me por editar uma resposta tão antiga (e não muito boa) ... mas eu não aguentava ver esse pobre readsem IFS=e -r, e o pobre $linesem suas citações saudáveis.
Gnourf_gniourf
1
@gniourf_gniourf: Não gosto da read -rnotação. OMI, POSIX entendeu errado; a opção deve ativar o significado especial para as barras invertidas à direita, não desativá-lo - para que os scripts existentes (antes da existência do POSIX) não fossem interrompidos porque o -rfoi omitido. Observo, no entanto, que fazia parte da IEEE 1003.2 1992, que foi a versão mais antiga do padrão de shell e utilitários POSIX, mas foi marcada como uma adição até então, portanto, isso é uma chatice sobre oportunidades antigas. Eu nunca tive problemas porque meu código não usa -r; Eu devo ter sorte. Me ignore nisso.
Jonathan Leffler
1
@JonathanLeffler Eu realmente concordo que isso -rdeve ser padrão. Concordo que é improvável que ocorra nos casos em que não usá-lo gera problemas. Porém, código quebrado é código quebrado. Minha edição foi desencadeada pela $linevariável pobre que perdeu muito as aspas. Eu consertei o readtempo que estava nisso. Não corrigi o echoporque esse é o tipo de edição que é revertida. :(.
gniourf_gniourf
1

O código ${1:-/dev/stdin}entenderá apenas o primeiro argumento, então, que tal isso.

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done
Takahiro Onodera
fonte
1

Não acho nenhuma dessas respostas aceitável. Em particular, a resposta aceita apenas lida com o primeiro parâmetro da linha de comando e ignora o restante. O programa Perl que ele está tentando emular manipula todos os parâmetros da linha de comando. Portanto, a resposta aceita nem mesmo responde à pergunta. Outras respostas usam extensões bash, adicionam comandos desnecessários 'cat', funcionam apenas no caso simples de ecoar a entrada na saída ou são apenas desnecessariamente complicados.

No entanto, tenho que dar crédito a eles, porque eles me deram algumas idéias. Aqui está a resposta completa:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done
Gungwald
fonte
1

Combinei todas as respostas acima e criei uma função shell que atendesse às minhas necessidades. Isto é de um terminal cygwin das minhas 2 máquinas Windows10, onde eu tinha uma pasta compartilhada entre elas. Eu preciso ser capaz de lidar com o seguinte:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

Onde um nome de arquivo específico é especificado, eu preciso usar o mesmo nome de arquivo durante a cópia. Onde o fluxo de dados de entrada foi canalizado, então eu preciso gerar um nome de arquivo temporário com a hora minutos e segundos. A pasta principal compartilhada possui subpastas dos dias da semana. Isso é para fins organizacionais.

Eis o script final para minhas necessidades:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

Se houver alguma maneira de otimizar ainda mais isso, eu gostaria de saber.

truthadjustr
fonte
0

O seguinte funciona com o padrão sh(Testado com dashno Debian) e é bastante legível, mas isso é uma questão de gosto:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

Detalhes: se o primeiro parâmetro não estiver vazio, catesse arquivo será catinserido como padrão. Em seguida, a saída de toda a ifinstrução é processada pelo commands_and_transformations.

Não está na lista
fonte
IMHO a melhor resposta até porque aponta para a verdadeira solução: cat "${1:--}" | any_command. Ler as variáveis ​​shell e repeti-las pode funcionar para arquivos pequenos, mas não é tão dimensionável.
Andreas Spindler
O [ -n "$1" ]pode ser simplificado para [ "$1" ].
agc
0

Este é fácil de usar no terminal:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3
cmcginty
fonte
-1

E se

for line in `cat`; do
    something($line);
done
Charles Cooper
fonte
A saída de catserá colocada na linha de comando. A linha de comando tem um tamanho máximo. Além disso, isso não será lido linha por linha, mas palavra por palavra.
Notinlist