Loop pelo conteúdo de um arquivo no Bash

1389

Como faço para percorrer cada linha de um arquivo de texto com o Bash ?

Com este script:

echo "Start!"
for p in (peptides.txt)
do
    echo "${p}"
done

Eu recebo esta saída na tela:

Start!
./runPep.sh: line 3: syntax error near unexpected token `('
./runPep.sh: line 3: `for p in (peptides.txt)'

(Mais tarde, quero fazer algo mais complicado do $pque apenas exibir na tela.)


A variável de ambiente SHELL é (de env):

SHELL=/bin/bash

/bin/bash --version resultado:

GNU bash, version 3.1.17(1)-release (x86_64-suse-linux-gnu)
Copyright (C) 2005 Free Software Foundation, Inc.

cat /proc/version resultado:

Linux version 2.6.18.2-34-default (geeko@buildhost) (gcc version 4.1.2 20061115 (prerelease) (SUSE Linux)) #1 SMP Mon Nov 27 11:46:27 UTC 2006

O arquivo peptides.txt contém:

RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Peter Mortensen
fonte
19
Oh, vejo muitas coisas aconteceram aqui: todos os comentários foram excluídos e a pergunta foi reaberta. Apenas para referência, a resposta aceita em Ler um arquivo linha por linha atribuindo o valor a uma variável aborda o problema de maneira canônica e deve ser preferida à aceita aqui.
fedorqui 'SO stop prejudicar'

Respostas:

2096

Uma maneira de fazer isso é:

while read p; do
  echo "$p"
done <peptides.txt

Conforme apontado nos comentários, isso tem os efeitos colaterais de aparar espaços em branco à esquerda, interpretar seqüências de barra invertida e pular a última linha se estiver faltando um feed de linha final. Se estas são preocupações, você pode fazer:

while IFS="" read -r p || [ -n "$p" ]
do
  printf '%s\n' "$p"
done < peptides.txt

Excepcionalmente, se o corpo do loop puder ler da entrada padrão , você poderá abrir o arquivo usando um descritor de arquivo diferente:

while read -u 10 p; do
  ...
done 10<peptides.txt

Aqui, 10 é apenas um número arbitrário (diferente de 0, 1, 2).

Bruno De Fraine
fonte
7
Como devo interpretar a última linha? O arquivo peptides.txt é redirecionado para a entrada padrão e de alguma forma para o bloco while?
22640 Peter Mortensen
11
"Coloque o peptides.txt neste loop while, para que o comando 'read' tenha algo a consumir." Meu método "cat" é semelhante, enviando a saída de um comando para o bloco while para consumo por 'read', também, apenas ele lança outro programa para concluir o trabalho.
5119 Warren Young
8
Este método parece pular a última linha de um arquivo.
Xstor #
5
Cite duas vezes as linhas !! eco "$ p" e o arquivo .. confie em mim, ele vai morder se você não !!! EU SEI! lol
Mike Q
5
Ambas as versões falham ao ler uma linha final se não terminar com uma nova linha. Sempre usewhile read p || [[ -n $p ]]; do ...
dawg
448
cat peptides.txt | while read line 
do
   # do something with $line here
done

e a variante de uma linha:

cat peptides.txt | while read line; do something_with_$line_here; done

Essas opções ignoram a última linha do arquivo se não houver avanço de linha à direita.

Você pode evitar isso da seguinte maneira:

cat peptides.txt | while read line || [[ -n $line ]];
do
   # do something with $line here
done
Warren Young
fonte
68
Em geral, se você estiver usando "gato" com apenas um argumento, estará fazendo algo errado (ou abaixo do ideal).
JesperE 05/10/09
27
Sim, não é tão eficiente quanto o de Bruno, porque lança outro programa desnecessariamente. Se a eficiência importa, faça do jeito de Bruno. Lembro-me do meu caminho, porque você pode usá-lo com outros comandos, onde a sintaxe "redirecionar de" não funciona.
5119 Warren Young
74
Há outro problema mais sério com isso: como o loop while faz parte de um pipeline, ele é executado em um subshell e, portanto, qualquer variável definida dentro do loop é perdida quando sai (consulte bash-hackers.org/wiki/doku. php / mirroring / bashfaq / 024 ). Isso pode ser muito irritante (dependendo do que você está tentando fazer no loop).
9789 Gordon Davisson
25
Eu uso "arquivo de gato |" como o início de muitos dos meus comandos apenas porque geralmente protótipo com "arquivo de cabeça |"
22614 Mathewel -
62
Isso pode não ser tão eficiente, mas é muito mais legível do que outras respostas.
Savage Leitor
144

Opção 1a: loop While: linha única de cada vez: redirecionamento de entrada

#!/bin/bash
filename='peptides.txt'
echo Start
while read p; do 
    echo $p
done < $filename

Opção 1b: Loop while: Linha única de cada vez:
Abra o arquivo, leia a partir de um descritor de arquivo (neste caso, o descritor de arquivo nº 4).

#!/bin/bash
filename='peptides.txt'
exec 4<$filename
echo Start
while read -u4 p ; do
    echo $p
done
Stan Graves
fonte
Para a opção 1b: o descritor de arquivo precisa ser fechado novamente? Por exemplo, o loop pode ser um loop interno.
31540 Peter Mortensen
3
O descritor de arquivo será limpo com as saídas do processo. Um fechamento explícito pode ser feito para reutilizar o número fd. Para fechar um fd, use outro exec com o & - sintaxe, como este: exec 4 <& -
Stan Graves
1
Obrigado pela opção 2. Tive muitos problemas com a opção 1 porque precisava ler do stdin dentro do loop; nesse caso, a opção 1 não funcionará.
#
4
Você deve indicar mais claramente que a opção 2 é fortemente desencorajada . @masgo A opção 1b deve funcionar nesse caso e pode ser combinada com a sintaxe de redirecionamento de entrada da opção 1a substituindo done < $filenamepor done 4<$filename(o que é útil se você quiser ler o nome do arquivo a partir de um parâmetro de comando, nesse caso, basta substituir $filenamepor $1)
Egor Hans
Eu preciso fazer um loop sobre o conteúdo do arquivo, como tail -n +2 myfile.txt | grep 'somepattern' | cut -f3, durante a execução de comandos ssh dentro do loop (consome stdin); opção 2 aqui parece ser o único caminho?
user5359531
85

Isso não é melhor do que outras respostas, mas é mais uma maneira de realizar o trabalho em um arquivo sem espaços (consulte os comentários). Acho que geralmente preciso de uma linha para vasculhar listas em arquivos de texto sem a etapa extra de usar arquivos de script separados.

for word in $(cat peptides.txt); do echo $word; done

Esse formato me permite colocar tudo em uma linha de comando. Altere a parte "echo $ word" para o que você quiser e poderá emitir vários comandos separados por ponto e vírgula. O exemplo a seguir usa o conteúdo do arquivo como argumentos em outros dois scripts que você pode ter escrito.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done

Ou, se você pretende usá-lo como um editor de fluxo (learn sed), pode despejar a saída em outro arquivo da seguinte maneira.

for word in $(cat peptides.txt); do cmd_a.sh $word; cmd_b.py $word; done > outfile.txt

Eu usei isso como escrito acima, porque usei arquivos de texto onde os criei com uma palavra por linha. (Ver comentários) Se você possui espaços que não deseja dividir suas palavras / linhas, fica um pouco mais feio, mas o mesmo comando ainda funciona da seguinte maneira:

OLDIFS=$IFS; IFS=$'\n'; for line in $(cat peptides.txt); do cmd_a.sh $line; cmd_b.py $line; done > outfile.txt; IFS=$OLDIFS

Isso apenas diz ao shell para dividir apenas em novas linhas, não em espaços, depois retorna o ambiente ao que era anteriormente. Neste ponto, você pode considerar colocar tudo isso em um script shell, em vez de espremer tudo em uma única linha.

Boa sorte!

mightypile
fonte
6
O bash $ (<peptides.txt) é talvez mais elegante, mas ainda está errado, o que João disse correto, você está executando uma lógica de substituição de comando em que espaço ou nova linha é a mesma coisa. Se uma linha tiver um espaço, o loop executará DUAS VEZES ou mais para essa linha. Portanto, seu código deve ler corretamente: por palavra em $ (<peptides.txt); do .... Se você sabe que não há espaços, uma linha é igual a uma palavra e você está bem.
Maxpolk
2
@ JoaoCosta, maxpolk: Bons pontos que eu não tinha considerado. Eu editei a postagem original para refleti-las. Obrigado!
Mightypile
2
O uso fortorna os tokens / linhas de entrada sujeitos a expansões de shell, o que geralmente é indesejável; tente o seguinte: for l in $(echo '* b c'); do echo "[$l]"; done+ como você verá, o *+ mesmo que originalmente um literal citado + se expanda para os arquivos no diretório atual.
mklement0
2
@ dblanchard: O último exemplo, usando $ IFS, deve ignorar espaços. Você já tentou essa versão?
Mightypile #
4
A maneira como esse comando se torna muito mais complexo à medida que problemas cruciais são corrigidos apresenta muito bem o motivo pelo qual usar forpara iterar linhas de arquivo é uma má idéia. Além disso, o aspecto de expansão mencionado por @ mklement0 (embora isso provavelmente possa ser contornado com a inclusão de aspas escapadas, o que novamente torna as coisas mais complexas e menos legíveis).
Egor Hans
69

Mais algumas coisas não cobertas por outras respostas:

Lendo de um arquivo delimitado

# ':' is the delimiter here, and there are three fields on each line in the file
# IFS set below is restricted to the context of `read`, it doesn't affect any other code
while IFS=: read -r field1 field2 field3; do
  # process the fields
  # if the line has less than three fields, the missing fields will be set to an empty string
  # if the line has more than three fields, `field3` will get all the values, including the third field plus the delimiter(s)
done < input.txt

Leitura da saída de outro comando, usando substituição de processo

while read -r line; do
  # process the line
done < <(command ...)

Essa abordagem é melhor do que command ... | while read -r line; do ... porque o loop while é executado no shell atual em vez de um subshell, como no caso do último. Veja o post relacionado Uma variável modificada dentro de um loop while não é lembrada .

Lendo de uma entrada delimitada nula, por exemplo find ... -print0

while read -r -d '' line; do
  # logic
  # use a second 'read ... <<< "$line"' if we need to tokenize the line
done < <(find /path/to/dir -print0)

Leitura relacionada: BashFAQ / 020 - Como posso encontrar e lidar com segurança com nomes de arquivos que contenham novas linhas, espaços ou ambos?

Lendo mais de um arquivo por vez

while read -u 3 -r line1 && read -u 4 -r line2; do
  # process the lines
  # note that the loop will end when we reach EOF on either of the files, because of the `&&`
done 3< input1.txt 4< input2.txt

Com base na resposta de @ chepner aqui :

-ué uma extensão do bash. Para compatibilidade com POSIX, cada chamada seria algo comoread -r X <&3 .

Lendo um arquivo inteiro em uma matriz (versões do Bash anteriores a 4)

while read -r line; do
    my_array+=("$line")
done < my_file

Se o arquivo terminar com uma linha incompleta (nova linha ausente no final), então:

while read -r line || [[ $line ]]; do
    my_array+=("$line")
done < my_file

Lendo um arquivo inteiro em uma matriz (versões Bash 4x e posteriores)

readarray -t my_array < my_file

ou

mapfile -t my_array < my_file

E depois

for line in "${my_array[@]}"; do
  # process the lines
done

Mensagens relacionadas:

codeforester
fonte
nota que, em vez de command < input_filename.txtvocê sempre pode fazer input_generating_command | commandoucommand < <(input_generating_command)
masterxilo
1
Obrigado por ler o arquivo na matriz. Exatamente o que eu preciso, porque preciso que cada linha analise duas vezes, adicione novas variáveis, faça algumas validações etc.
frank_108
45

Use um loop while, assim:

while IFS= read -r line; do
   echo "$line"
done <file

Notas:

  1. Se você não definir IFScorretamente, perderá o recuo.

  2. Você quase sempre deve usar a opção -r com read.

  3. Não leia linhas com for

Jahid
fonte
2
Por que a -ropção?
David C. Rankin
2
@ DavidC.Rankin A opção -r impede a interpretação da barra invertida. Note #2é um link onde é descrito em detalhes ...
Jahid
Combine isso com a opção "read -u" em outra resposta e, em seguida, é perfeito.
Florin Andrei
@FlorinAndrei: O exemplo acima não precisa da -uopção, você está falando de outro exemplo -u?
Jahid
Examinou seus links e ficou surpreso por não haver uma resposta que simplesmente vincule seu link na Nota 2. Essa página fornece tudo o que você precisa saber sobre esse assunto. Ou as respostas somente de link são desencorajadas ou algo assim?
Egor Hans
14

Suponha que você tenha este arquivo:

$ cat /tmp/test.txt
Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR

Existem quatro elementos que alterarão o significado da saída do arquivo lida por muitas soluções Bash:

  1. A linha em branco 4;
  2. Espaços iniciais ou finais em duas linhas;
  3. Manter o significado de linhas individuais (ou seja, cada linha é um registro);
  4. A linha 6 não terminou com um CR.

Se você deseja que o arquivo de texto linha por linha, incluindo linhas em branco e linhas de terminação sem CR, use um loop while e faça um teste alternativo para a linha final.

Aqui estão os métodos que podem alterar o arquivo (em comparação com o que catretorna):

1) Perca a última linha e os espaços à esquerda e à direita:

$ while read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'

(Se while IFS= read -r p; do printf "%s\n" "'$p'"; done </tmp/test.txtpreferir, você preserva os espaços à esquerda e à direita, mas ainda perde a última linha se não terminar com CR)

2) O uso da substituição de processo com o catwill lê o arquivo inteiro de uma só vez e perde o significado de linhas individuais:

$ for p in "$(cat /tmp/test.txt)"; do printf "%s\n" "'$p'"; done
'Line 1
    Line 2 has leading space
Line 3 followed by blank line

Line 5 (follows a blank line) and has trailing space    
Line 6 has no ending CR'

(Se você remover a "partir de $(cat /tmp/test.txt)você ler a palavra arquivo por palavra, em vez de um só gole. Além disso, provavelmente, não o que se pretende ...)


A maneira mais robusta e simples de ler um arquivo linha por linha e preservar todo o espaçamento é:

$ while IFS= read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'    Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space    '
'Line 6 has no ending CR'

Se você deseja remover os espaços de liderança e negociação, remova a IFS=peça:

$ while read -r line || [[ -n $line ]]; do printf "'%s'\n" "$line"; done </tmp/test.txt
'Line 1'
'Line 2 has leading space'
'Line 3 followed by blank line'
''
'Line 5 (follows a blank line) and has trailing space'
'Line 6 has no ending CR'

(Um arquivo de texto sem terminação \n, embora bastante comum, é considerado quebrado no POSIX. Se você puder contar com a trilha \nque não precisa, || [[ -n $line ]]nowhile loop.)

Mais no FAQ do BASH

dawg
fonte
13

Se você não deseja que sua leitura seja interrompida pelo caractere de nova linha, use -

#!/bin/bash
while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "$line"
done < "$1"

Em seguida, execute o script com o nome do arquivo como parâmetro.

Anjul Sharma
fonte
4
#!/bin/bash
#
# Change the file name from "test" to desired input file 
# (The comments in bash are prefixed with #'s)
for x in $(cat test.txt)
do
    echo $x
done
Seno
fonte
7
Essa resposta precisa das advertências mencionadas na resposta do mightypile e pode falhar muito se alguma linha contiver metacaracteres de shell (devido ao "$ x" não citado).
perfil completo de Toby Speight
7
Eu estou realmente surpreso pessoas ainda não veio para cima com as habituais linhas não lêem com para ...
Egor Hans
3

Aqui está o meu exemplo da vida real de como fazer loop de linhas de outra saída de programa, procurar substrings, remover aspas duplas da variável, usar essa variável fora do loop. Eu acho que muitos estão fazendo essas perguntas mais cedo ou mais tarde.

##Parse FPS from first video stream, drop quotes from fps variable
## streams.stream.0.codec_type="video"
## streams.stream.0.r_frame_rate="24000/1001"
## streams.stream.0.avg_frame_rate="24000/1001"
FPS=unknown
while read -r line; do
  if [[ $FPS == "unknown" ]] && [[ $line == *".codec_type=\"video\""* ]]; then
    echo ParseFPS $line
    FPS=parse
  fi
  if [[ $FPS == "parse" ]] && [[ $line == *".r_frame_rate="* ]]; then
    echo ParseFPS $line
    FPS=${line##*=}
    FPS="${FPS%\"}"
    FPS="${FPS#\"}"
  fi
done <<< "$(ffprobe -v quiet -print_format flat -show_format -show_streams -i "$input")"
if [ "$FPS" == "unknown" ] || [ "$FPS" == "parse" ]; then 
  echo ParseFPS Unknown frame rate
fi
echo Found $FPS

Declarar a variável fora do loop, definir valor e usá-lo fora do loop requer que seja feito <<< "$ (...)" sintaxe . O aplicativo precisa ser executado dentro de um contexto do console atual. As aspas ao redor do comando mantêm novas linhas de fluxo de saída.

A correspondência de loop para substrings lê o par nome = valor , divide a parte direita do último caractere = , solta a primeira citação, solta a última citação, temos um valor limpo para ser usado em outro lugar.

Quem eu
fonte
3
Enquanto a resposta está correta, eu entendo como acabou aqui. O método essencial é o mesmo proposto por muitas outras respostas. Além disso, ele se afoga completamente no seu exemplo de FPS.
Egor Hans
0

Isso está chegando muito tarde, mas com o pensamento de que isso pode ajudar alguém, estou adicionando a resposta. Além disso, este pode não ser o melhor caminho. headO comando pode ser usado com -nargumento para ler n linhas desde o início do arquivo e o tailcomando da mesma forma pode ser usado para ler de baixo. Agora, para buscar a enésima linha do arquivo, dirigimos n linhas , canalizamos os dados para reduzir apenas 1 linha dos dados canalizados.

   TOTAL_LINES=`wc -l $USER_FILE | cut -d " " -f1 `
   echo $TOTAL_LINES       # To validate total lines in the file

   for (( i=1 ; i <= $TOTAL_LINES; i++ ))
   do
      LINE=`head -n$i $USER_FILE | tail -n1`
      echo $LINE
   done
madD7
fonte
1
Não faça isso. Fazer um loop sobre os números de linhas e buscar cada linha individual por meio de sedou head+ tailé incrivelmente ineficiente e, é claro, levanta a questão de por que você simplesmente não usa uma das outras soluções aqui. Se você precisar saber o número da linha, adicione um contador ao seu while read -rloop ou use nl -bapara adicionar um prefixo de número de linha a cada linha antes do loop.
tripleee
-1

@ Peter: Isso pode funcionar para você-

echo "Start!";for p in $(cat ./pep); do
echo $p
done

Isso retornaria a saída

Start!
RKEKNVQ
IPKKLLQK
QYFHQLEKMNVK
IPKKLLQK
GDLSTALEVAIDCYEK
QYFHQLEKMNVKIPENIYR
RKEKNVQ
VLAKHGKLQDAIN
ILGFMK
LEDVALQILL
Alan Jebakumar
fonte
11
Isso é muito ruim! Por que você não lê linhas com "for" .
fedorqui 'Então, pare de prejudicar'
3
Esta resposta está derrotando todos os princípios estabelecidos pelas boas respostas acima!
codeforester 14/01
3
Por favor, apague esta resposta.
dawg
3
Agora pessoal, não exagere. A resposta é ruim, mas parece funcionar, pelo menos para casos de uso simples. Desde que seja fornecida, ser uma resposta ruim não tira o direito de existir.
Egor Hans
3
@ EgorHans, discordo veementemente: o objetivo das respostas é ensinar às pessoas como escrever software. Ensinar as pessoas a fazer as coisas de uma maneira que você sabe que é prejudicial para elas e as pessoas que usam seu software (introdução de bugs / comportamentos inesperados / etc) estão prejudicando conscientemente outras pessoas. Uma resposta conhecida como prejudicial não tem "direito de existir" em um recurso de ensino bem curado (e curadoria é exatamente o que nós, as pessoas que votamos e denunciamos, devemos fazer aqui).
Charles Duffy