Modo Slurp no awk?

16

Ferramentas como sed, awkou perl -nprocessar sua uma entrada registro de cada vez, registros sendo linhas por padrão.

Alguns, como awkcom RS, GNU sedcom -zou perlcom -0ooopossível alterar o tipo de registro, selecionando um separador de registro diferente.

perl -npode fazer da entrada inteira (cada arquivo individual quando passados ​​vários arquivos) um único registro com a -0777opção (ou -0seguida por qualquer número octal maior que 0377, sendo 777 o canônico). É assim que eles chamam de modo slurp .

Pode algo semelhante ser feito com awk's RSou qualquer outro mecanismo? Onde awkprocessa cada conteúdo do arquivo como um todo em ordem, em oposição a cada linha de cada arquivo?

Stéphane Chazelas
fonte

Respostas:

15

Você pode adotar diferentes abordagens, dependendo se awktrata RScomo um único caractere (como as awkimplementações tradicionais fazem) ou como uma expressão regular (como gawkou mawkfaz). Arquivos vazios também são difíceis de serem considerados, pois awktendem a ignorá-los.

gawk, mawkou outras awkimplementações em que RSpode ser uma regexp.

Nessas implementações (por favor mawk, cuidado com o fato de que alguns sistemas operacionais, como o Debian, enviam uma versão muito antiga em vez da moderna mantida por @ThomasDickey ), se RScontiver um único caractere, o separador de registros é esse caractere ou awkentra no modo de parágrafo quando RSestá vazio, ou trata RScomo uma expressão regular de outra forma.

A solução é usar uma expressão regular que não possa ser correspondida. Alguns vêm à mente como x^ou $x( xantes do início ou depois do fim). No entanto, alguns (particularmente com gawk) são mais caros que outros. Até agora, descobri que ^$é o mais eficiente. Ele pode corresponder apenas a uma entrada vazia, mas não haveria nada contra o que corresponder.

Para que possamos fazer:

awk -v RS='^$' '{printf "%s: <%s>\n", FILENAME, $0}' file1 file2...

Uma ressalva, porém, é que ele pula arquivos vazios (ao contrário de perl -0777 -n). Isso pode ser resolvido com o GNU awk, colocando o código em uma ENDFILEdeclaração. Mas também precisamos redefinir $0em uma instrução BEGINFILE, pois, caso contrário, não seria redefinida após o processamento de um arquivo vazio:

gawk -v RS='^$' '
   BEGINFILE{$0 = ""}
   ENDFILE{printf "%s: <%s>\n", FILENAME, $0}' file1 file2...

awkimplementações tradicionais , POSIXawk

Nesses, RSé apenas um caractere, eles não têm BEGINFILE/ ENDFILE, eles não têm a RTvariável, geralmente também não podem processar o caractere NUL.

Você pensaria que o uso RS='\0'poderia funcionar, pois, de qualquer maneira, eles não podem processar a entrada que contém o byte NUL, mas não, que RS='\0'nas implementações tradicionais é tratado como RS=, que é o modo de parágrafo.

Uma solução pode ser usar um caractere improvável de ser encontrado na entrada como \1. Nos códigos de idioma de caracteres com vários bytes, é possível fazer com que seja muito improvável que sequências de bytes ocorram, pois formam caracteres que não são atribuídos ou não como caracteres $'\U10FFFE'nos códigos de idioma UTF-8. Não é realmente infalível e você também tem um problema com arquivos vazios.

Outra solução pode ser armazenar toda a entrada em uma variável e processá-la na instrução END no final. Isso significa que você pode processar apenas um arquivo por vez:

awk '{content = content $0 RS}
     END{$0 = content
       printf "%s: <%s>\n", FILENAME, $0
     }' file

Isso é o equivalente a sed's:

sed '
  :1
  $!{
   N;b1
  }
  ...' file1

Outro problema com essa abordagem é que, se o arquivo não estava terminando com um caractere de nova linha (e não estava vazio), um ainda é adicionado arbitrariamente $0no final (com gawk, você contornaria isso usando em RTvez de RSno código acima). Uma vantagem é que você possui um registro do número de linhas no arquivo em NR/ FNR.

Stéphane Chazelas
fonte
quanto à última parte ("se o arquivo não estava terminando com um caractere de nova linha (e não estava vazio), um ainda é adicionado arbitrariamente em US $ 0 no final"): para arquivos de texto, eles deveriam ter um final nova linha. O vi adiciona um, por exemplo, e modifica o arquivo quando você o salva. Não ter uma nova linha de terminação faz com que alguns comandos descartem a última "linha" (ex: wc), mas outros ainda 'veem' a última linha ... ymmv. Sua solução é, portanto válido, imo, se você é suposto a arquivos de texto tratar (que é provavelmente o caso, como awk é bom para processamento de texto, mas não tão bom para os binários ^^)
Olivier Dulac
11
tentar slurp all in pode ter algumas limitações ... o awk tradicional aparentemente tinha (tem?) um limite de 99 campos em uma linha ... então você pode precisar usar um FS diferente também para evitar esse limite, mas pode também tem limites de quanto tempo pode ser o comprimento total de uma linha (ou a coisa toda, se você conseguir colocar tudo em uma linha)?
Olivier Dulac
finalmente: um hack (bobo ...) pode ser o primeiro a analisar o arquivo inteiro e procurar um caractere que não esteja lá, tr '\n' 'thatchar' o arquivo antes de enviá-lo para o awk e tr 'thatchar' \n'a saída? (pode ser necessário ainda acrescentar uma nova linha para garantir, como observei acima, que seu arquivo de entrada tenha uma nova linha final: { tr '\n' 'missingchar' < thefile ; printf "\n" ;} | awk ..... | { tr 'missingchar' '\n' }(mas que adiciona um '\ n' no final, do qual talvez você precise se livrar ... talvez adicionando um sed antes do tr final, se que tr aceita arquivos sem terminar novas linhas ...)?
Olivier Dulac
@OlivierDulac, o limite do número de campos só seria atingido se estivéssemos acessando a NF ou qualquer campo. awknão faz a divisão se não fizermos. Dito isto, nem mesmo o /bin/awkSolaris 9 (baseado no awk dos anos 70) teve essa limitação, então não tenho certeza de que possamos encontrar um que tenha (ainda possível, pois o oawk do SVR4 tinha um limite de 99 e nawk 199, então é provavelmente o aumento desse limite foi adicionado pela Sun e pode não ser encontrado em outros awks baseados em SVR4, você pode testar no AIX?).
Stéphane Chazelas