Script de shell leu falta da última linha

Eu tenho um … estranho problema com um script de shell bash que eu estava esperando para obter alguma visão sobre.

Minha equipe está trabalhando em um script que percorre linhas em um arquivo e verifica o conteúdo em cada um. Nós tivemos um bug onde, quando executados através do processo automatizado que sequencia diferentes scripts juntos, a última linha não estava sendo vista.

O código usado para iterar as linhas no arquivo (o nome armazenado em DATAFILE era

 cat "$DATAFILE" | while read line 

Poderíamos executar o script a partir da linha de comando e veria todas as linhas no arquivo, incluindo a última, muito bem. No entanto, quando executado pelo processo automatizado (que executa o script que gera o DATAFILE logo antes do script em questão), a última linha nunca é vista.

Atualizamos o código para usar o seguinte para fazer uma iteração nas linhas e o problema foi resolvido:

 for line in `cat "$DATAFILE"` 

Nota: DATAFILE não possui uma nova linha escrita no final do arquivo.

Minha pergunta é de duas partes … Por que a última linha não seria vista pelo código original e por que isso mudaria de alguma forma?

Eu só pensei que eu poderia pensar em por que a última linha não seria vista:

  • O processo anterior, que grava o arquivo, dependia do processo para encerrar o descritor de arquivo.
  • O script do problema estava iniciando e abrindo o arquivo antes rápido o suficiente para que, enquanto o processo anterior tivesse “finalizado”, não tivesse “encerrado / limpo” o suficiente para o sistema fechar o descritor de arquivo automaticamente.

Dito isto, parece que, se você tiver 2 comandos em um script de shell, o primeiro deve ser completamente encerrado no momento em que o script executar o segundo.

Qualquer visão sobre as questões, especialmente a primeira, seria muito apreciada.

O padrão C diz que os arquivos de texto devem terminar com uma nova linha ou os dados após a última nova linha podem não ser lidos corretamente.

ISO / IEC 9899: 2011 §7.21.2 Transmissões

Um stream de texto é uma seqüência ordenada de caracteres compostos em linhas, cada linha consistindo em zero ou mais caracteres, além de um caractere de nova linha de terminação. Se a última linha requer um caractere de nova linha de terminação é definida pela implementação. Os caracteres podem ter que ser adicionados, alterados ou excluídos na input e na saída para estar em conformidade com as convenções diferentes para representar texto no ambiente host. Assim, não é necessário que haja uma correspondência de um para um entre os caracteres em um stream e aqueles na representação externa. Os dados lidos a partir de um stream de texto serão necessariamente comparados com os dados anteriormente gravados nesse stream apenas se: os dados consistirem apenas em caracteres de impressão e na guia horizontal e nova linha de caracteres de controle; nenhum caractere de nova linha é imediatamente precedido por caracteres espaciais; e o último caractere é um caractere de nova linha. Se os caracteres de espaço que são gravados imediatamente antes de um caractere de nova linha aparecer quando a leitura é definida pela implementação.

Eu não teria uma inesperada falta de nova linha no final do arquivo para causar problemas no bash (ou qualquer shell Unix), mas esse parece ser o problema reproduzível ( $ é o prompt nesta saída):

 $ echo xxx\\c xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y $ cat y abc def ghi xxx$ $ while read line; do echo $line; done < y abc def ghi $ bash -c 'while read line; do echo $line; done < y' abc def ghi $ ksh -c 'while read line; do echo $line; done < y' abc def ghi $ zsh -c 'while read line; do echo $line; done < y' abc def ghi $ for line in $( 

Também não está limitado a bash - Korn shell ( ksh ) e zsh se comportam assim também. Eu vivo, aprendo; obrigado por levantar a questão.

Como demonstrado no código acima, o comando cat lê o arquivo inteiro. A for line in `cat $DATAFILE` técnica for line in `cat $DATAFILE` coleta toda a saída e substitui sequências arbitrárias de espaço em branco por um único espaço em branco (concluo que cada linha no arquivo não contém espaços em branco).

Testado no Mac OS X 10.7.5.


O que o POSIX diz?

A especificação do comando POSIX read diz:

O utilitário de leitura deve ler uma única linha da input padrão.

Por padrão, a menos que a opção -r seja especificada, deve agir como um caractere de escape. Um sem escape preservará o valor literal do seguinte caractere, com exceção de um . Se uma seguir a , o utilitário de leitura interpretará isso como continuação de linha. O e a devem ser removidos antes de dividir a input nos campos. Todos os outros caracteres sem escape serão removidos após a divisão da input nos campos.

Se a input padrão for um dispositivo de terminal e o shell de chamada for interativo, read solicitará uma linha de continuação quando ele ler uma linha de input terminando com , a menos que a opção -r seja especificada.

A terminação (se houver) deve ser removida da input e os resultados devem ser divididos em campos como no shell para os resultados da expansão de parâmetro (consulte Divisão de campo); [...]

Note que '(se houver)' (ênfase adicionada na citação)! Parece-me que, se não houver nova linha, ainda deve ler o resultado. Por outro lado, também diz:

STDIN

A input padrão deve ser um arquivo de texto.

e então você volta ao debate sobre se um arquivo que não termina com uma nova linha é um arquivo de texto ou não.

No entanto, a justificativa na mesma página documenta:

Embora a input padrão seja necessária para ser um arquivo de texto e, portanto, sempre terminará com um (a menos que seja um arquivo vazio), o processamento de linhas de continuação quando a opção -r não for usada pode resultar na input não terminando com um . Isso ocorre se a última linha do arquivo de input terminar com um . É por esse motivo que "se houver" é usado em "A (se houver) deve ser removida da input" na descrição. Não é um relaxamento da exigência de input padrão para ser um arquivo de texto.

Esse raciocínio deve significar que o arquivo de texto deve terminar com uma nova linha.

A definição POSIX de um arquivo de texto é:

3.395 Arquivo de Texto

Um arquivo que contém caracteres organizados em zero ou mais linhas. As linhas não contêm caracteres NUL e nenhuma pode exceder {LINE_MAX} bytes de comprimento, incluindo o caractere . Embora POSIX.1-2008 não faça distinção entre arquivos de texto e arquivos binários (consulte o padrão ISO C), muitos utilitários só produzem resultados previsíveis ou significativos ao operar em arquivos de texto. Os utilitários padrão que possuem tais restrições sempre especificam "arquivos de texto" em suas seções STDIN ou INPUT FILES.

Isso não estipula 'termina com uma ' diretamente, mas adia para o padrão C.


Uma solução para o problema 'no new terminal'

Observe a resposta de Gordon Davisson . Um teste simples mostra que sua observação é precisa:

 $ while read line; do echo $line; done < y; echo $line abc def ghi xxx $ 

Portanto, sua técnica de:

 while read line || [ -n "$line" ]; do echo $line; done < y 

ou:

 cat y | while read line || [ -n "$line" ]; do echo $line; done 

irá trabalhar para arquivos sem uma nova linha no final (pelo menos na minha máquina).


Ainda estou surpreso ao descobrir que os shells soltam o último segmento (não pode ser chamado de linha porque não termina com uma nova linha) da input, mas pode haver justificativa suficiente no POSIX para fazer isso. E é claro que é melhor garantir que seus arquivos de texto realmente sejam arquivos de texto que terminem com uma nova linha.

De acordo com a especificação POSIX para o comando de leitura , ele deve retornar um status diferente de zero se “Fim de arquivo foi detectado ou ocorreu um erro”. Como o EOF é detectado quando ele lê a última “linha”, ele define $ line e, em seguida, retorna um status de erro, e o status do erro impede que o loop seja executado na última “linha”. A solução é fácil: faça o loop executar se o comando read for bem-sucedido OU se algo for lido em $ line.

 while read line || [ -n "$line" ]; do 

Adicionando algumas informações adicionais:

  1. Não há necessidade de usar o cat com loop while. while ...;do something;done é suficiente.
  2. Não leia as linhas com for .

Ao usar o loop while para ler linhas:

  1. Definir o IFS corretamente (você pode perder o recuo caso contrário).
  2. Você deve quase sempre usar a opção -r com leitura.

com o cumprimento dos requisitos acima, um loop while apropriado será semelhante a este:

 while IFS= read -r line; do ... done  

E para que funcione com arquivos sem uma nova linha no final (repostando minha solução daqui ):

 while IFS= read -r line || [ -n "$line" ]; do echo "$line" done  

Ou usando grep com loop while:

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

Eu suspeito que não ter nova linha na última linha do seu arquivo pode estar causando esse problema. Para testes você pode fazer pequenas modificações no seu script e ler DATAFILE assim:

 while read line do echo $line # do processing here done < "$DATAFILE" 

E veja se isso faz alguma diferença.

Use sed para corresponder à última linha de um arquivo, que então acrescentará uma nova linha se ela não existir e fizer uma substituição inline do arquivo:

sed -i '' -e '$a\' file

O código é deste link de stackexchange

Nota: Eu adicionei aspas simples vazias a -i '' porque, pelo menos no OS X, -i estava usando -e como uma extensão de arquivo para o arquivo de backup. Eu teria prazer em comentar sobre o post original, mas faltou 50 pontos. Talvez isso me ganhe alguns neste segmento, obrigado.

Eu testei isso na linha de comando

 # create dummy file. last line doesn't end with newline printf "%i\n%i\nNo-newline-here" >testing 

Teste com sua primeira forma (canalizando para while-loop)

 cat testing | while read line; do echo $line; done 

Isso erra a última linha, o que faz sentido, pois a read só recebe inputs que terminam com uma nova linha.


Teste com o seu segundo formulário (substituição de comando)

 for line in `cat testbed1` ; do echo $line; done 

Isso também pega a última linha


read only recebe input se for terminado por newline, é por isso que você erra a última linha.

Por outro lado, na segunda forma

 `cat testing` 

se expande para a forma de

 line1\nline2\n...lineM 

que é separado pelo shell em vários campos usando o IFS, para que você obtenha

 line1 line2 line3 ... lineM 

É por isso que você ainda recebe a última linha.

p / s: O que eu não entendo é como você consegue o primeiro formulário funcionando …

Como solução alternativa, antes de ler o arquivo de texto, uma nova linha pode ser anexada ao arquivo.

 echo "\n" >> $file_path 

Isso garantirá que todas as linhas que estavam anteriormente no arquivo serão lidas.

Eu tive uma questão semelhante. Eu estava fazendo um gato de um arquivo, canalizando-o para um tipo e, em seguida, canalizando o resultado para um ‘while read var1 var2 var3’. isto é: cat $ FILE | sort -k3 | while lido Count IP Name do O trabalho sob o “do” era uma declaração if que identificava a mudança de dados no campo $ Name e com base em alterações ou nenhuma mudança fazia sums de $ Count ou impressas a linha resumida para o relatório. Também corri para o problema em que não consegui imprimir a última linha no relatório. Eu fui com o expediente simples de redirect o cat / sort para um novo arquivo, ecoando uma nova linha para esse novo arquivo e então corri meu “while read Count IP Name” no novo arquivo com resultados bem sucedidos. ou seja: cat $ FILE | sort -k3> NEWFILE echo “\ n” >> NEWFILE cat NEWFILE | while lido Count IP Name do Às vezes o simples, deselegante é o melhor caminho a percorrer.