sed: lê o arquivo inteiro no espaço padrão sem falhar na entrada de linha única

9

A leitura de um arquivo inteiro no espaço padrão é útil para substituir novas linhas, etc. e há muitos casos aconselhando o seguinte:

sed ':a;N;$!ba; [commands...]'

No entanto, ele falhará se a entrada contiver apenas uma linha.

Como exemplo, com entrada de duas linhas, todas as linhas estão sujeitas ao comando de substituição:

$ echo $'abc\ncat' | sed ':a;N;$!ba; s/a/xxx/g'
xxxbc
cxxxt

Mas, com entrada de linha única, nenhuma substituição é realizada:

$ echo 'abc' | sed ':a;N;$!ba; s/a/xxx/g'
abc

Como se escreve um sedcomando para ler todas as entradas de uma só vez e não ter esse problema?

dicktyr
fonte
Editei sua pergunta para que ela contenha uma pergunta real. Você pode esperar outras respostas, se quiser, mas eventualmente marcar a melhor resposta como aceita (consulte o botão de tubulação à esquerda da resposta, logo abaixo dos botões de seta para cima e para baixo).
precisa saber é o seguinte
@ John1024 Obrigado, bom ter um exemplo. Encontrar esse tipo de coisa tende a me lembrar que "está tudo errado", mas fico feliz por alguns de nós não desistirmos. :}
dicktyr 31/01
2
Há uma terceira opção! Use a sed -zopção do GNU . Se o seu arquivo não tiver nulo, ele será lido até o final do arquivo! Encontrado a partir desta: stackoverflow.com/a/30049447/582917
CMCDragonkai

Respostas:

13

Existem todos os tipos de razões pelas quais a leitura de um arquivo inteiro no espaço padrão pode dar errado. O problema lógico na pergunta em torno da última linha é comum. Está relacionado ao sedciclo de linhas - quando não há mais linhas e sedencontra EOF em que está terminado - ele encerra o processamento. E assim, se você estiver na última linha e instruir sedpara conseguir outra, ela vai parar ali e não fazer mais.

Dito isto, se você realmente precisar ler um arquivo inteiro no espaço padrão, provavelmente vale a pena considerar outra ferramenta. O fato é que sedé o editor de fluxo de maneira homogênea - ele é projetado para trabalhar uma linha - ou um bloco de dados lógicos - de cada vez.

Existem muitas ferramentas semelhantes que estão melhor equipadas para lidar com blocos de arquivos completos. ede ex, por exemplo, podem fazer muito do que sedpodem fazer e com sintaxe semelhante - e muito mais - mas, em vez de operar apenas em um fluxo de entrada enquanto o transformam em saída sed, eles também mantêm arquivos de backup temporários no sistema de arquivos . O trabalho deles é armazenado em buffer no disco, conforme necessário, e eles não são encerrados abruptamente no final do arquivo (e tendem a implodir com muito menos frequência sob tensão do buffer) . Além disso, eles oferecem muitas funções úteis que sednão fazem - do tipo que simplesmente não faz sentido em um contexto de fluxo - como marcas de linha, desfazer, buffers nomeados, junção e muito mais.

sedA força principal de sua capacidade é processar dados assim que os lê - de maneira rápida, eficiente e em fluxo. Quando você copia um arquivo, joga isso fora e tende a encontrar dificuldades de casos extremos, como o último problema de linha que você mencionou, excedentes de buffer e desempenho péssimo - à medida que os dados que ele analisa aumentam no tempo de processamento de um mecanismo de expressão regular ao enumerar correspondências aumenta exponencialmente .

A respeito desse último ponto, a propósito: embora eu entenda que o s/a/A/gcaso de exemplo é muito provavelmente apenas um exemplo ingênuo e provavelmente não é o script real para o qual você deseja reunir uma entrada, você pode achar que vale a pena se familiarizar com y///. Se você costuma gsubstituir globalmente um caractere por outro, ypode ser muito útil para você. É uma transformação em oposição a uma substituição e é muito mais rápida, pois não implica uma regexp. Esse último ponto também pode torná-lo útil ao tentar preservar e repetir //endereços vazios , pois não os afeta, mas pode ser afetado por eles. De qualquer forma, y/a/A/é um meio mais simples de realizar o mesmo - e os swaps são possíveis, assim como:y/aA/Aa/ que trocaria todas as maiúsculas / minúsculas como em uma linha entre si.

Você também deve observar que o comportamento que você descreve não é realmente o que deveria acontecer de qualquer maneira.

Dos GNUs info sedna seção BUGS RELATADOS COMUNS :

  • N comando na última linha

    • A maioria das versões de sedsaída sem imprimir nada quando o Ncomando é emitido na última linha de um arquivo. O GNU sedimprime o espaço do padrão antes de sair, a menos que a -nopção de comando tenha sido especificada. Essa escolha é por design.

    • Por exemplo, o comportamento de sed N foo bardepende de foo ter um número par ou ímpar de linhas. Ou, ao escrever um script para ler as próximas linhas após uma correspondência de padrão, as implementações tradicionais de sedforçariam você a escrever algo como, em /foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }vez de apenas /foo/{ N;N;N;N;N;N;N;N;N; }.

    • Em qualquer caso, a solução mais simples é usar $d;Nem scripts que dependem do comportamento tradicional ou definir a POSIXLY_CORRECTvariável como um valor não vazio.

A POSIXLY_CORRECTvariável de ambiente é mencionada porque o POSIX especifica que, se sedencontrar EOF ao tentar um, Nele deve sair sem saída, mas a versão GNU rompe intencionalmente com o padrão nesse caso. Observe também que, mesmo que o comportamento seja justificado acima, pressupõe-se que o caso de erro seja de edição de fluxo - não colocando um arquivo inteiro na memória.

O padrão define No comportamento da seguinte forma:

  • N

    • Anexe a próxima linha de entrada, menos sua linha de \new final , ao espaço do padrão, usando uma \nlinha de ew incorporada para separar o material anexado do material original. Observe que o número da linha atual é alterado.

    • Se nenhuma próxima linha de entrada estiver disponível, o Nverbo de comando deverá se ramificar no final do script e sair sem iniciar um novo ciclo ou copiar o espaço do padrão para a saída padrão.

Nessa nota, existem alguns outros GNU-ismos demonstrados na pergunta - particularmente o uso do :rótulo, brancho e {colchetes de contexto de função }. Como regra geral, qualquer sedcomando que aceite um parâmetro arbitrário delimita em uma linha de \new no script. Então os comandos ...

:arbitrary_label_name; ...
b to_arbitrary_label_name; ...
//{ do arbitrary list of commands } ...

... é provável que todos tenham um desempenho irregular, dependendo da sedimplementação que os lê. Portably eles devem ser escritos:

...;:arbitrary_label_name
...;b to_arbitrary_label_name
//{ do arbitrary list of commands
}

O mesmo vale para r, w, t, a, i, e c (e, possivelmente, um pouco mais que eu estou esquecendo no momento) . Em quase todos os casos, eles também podem ser escritos:

sed -e :arbitrary_label_name -e b\ to_arbitary_label_name -e \
    "//{ do arbitrary list of commands" -e \}

... onde a nova -einstrução xecution representa o \ndelimitador de linha de ew. Portanto, onde o infotexto GNU sugere que uma implementação tradicional sedforçaria você a fazer :

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }

... deveria ser ...

/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N
}

... é claro, isso também não é verdade. Escrever o script dessa maneira é um pouco bobo. Existem meios muito mais simples de fazer o mesmo, como:

printf %s\\n foo . . . . . . |
sed -ne 'H;/foo/h;x;//s/\n/&/3p;tnd
         //!g;x;$!d;:nd' -e 'l;$a\' \
     -e 'this is the last line' 

... que imprime:

foo
.
.
.
foo\n.\n.\n.$
.$
this is the last line

... porque o tcomando est - como a maioria dos sedcomandos - depende do ciclo da linha para atualizar seu registro de retorno e aqui o ciclo da linha é permitido para executar a maior parte do trabalho. Essa é outra desvantagem que você faz quando inverte um arquivo - o ciclo da linha não é atualizado novamente e muitos testes se comportam de maneira anormal.

O comando acima não corre o risco de exceder a entrada, porque apenas faz alguns testes simples para verificar o que lê enquanto lê. Com o Hantigo, todas as linhas são anexadas ao espaço de espera, mas se uma linha corresponder /foo/, substituirá o hespaço antigo. Os buffers são os próximos e xalterados e uma s///substituição condicional é tentada se o conteúdo do buffer corresponder ao //último padrão endereçado. Em outras palavras, //s/\n/&/3ptenta substituir a terceira nova linha no espaço de espera e imprimir os resultados se o espaço de espera corresponder no momento /foo/. Se isso for tbem-sucedido, o script se ramifica para o rótulo not delete - o que faz um ok le envolve o script.

No /foo/entanto, se uma e a terceira nova linha não puderem ser combinadas no espaço de espera, //!gelas substituirão o buffer se /foo/não forem correspondidas ou, se forem correspondentes, substituirão o buffer se uma linha de \new não corresponder (substituindo assim /foo/por próprio) . Esse pequeno teste sutil evita que o buffer seja preenchido desnecessariamente por longos períodos sem /foo/e garante que o processo permaneça rápido porque a entrada não se acumula. Em um caso de não /foo/ou //s/\n/&/3pfalha, os buffers são novamente trocados e todas as linhas, exceto a última, são excluídas.

Essa última - a última linha $!d- é uma demonstração simples de como um sedscript de cima para baixo pode ser feito para lidar com vários casos facilmente. Quando o seu método geral é remover os casos indesejados, começando pelos mais gerais e trabalhando pelos mais específicos, os casos extremos podem ser mais facilmente tratados, porque eles simplesmente podem passar até o final do script com seus outros dados desejados e quando tudo isso envolve apenas os dados que você deseja. Porém, ter que buscar esses casos extremos de um loop fechado pode ser muito mais difícil.

E aqui está a última coisa que tenho a dizer: se você realmente precisa extrair um arquivo inteiro, pode trabalhar um pouco menos, confiando no ciclo da linha para fazer isso por você. Normalmente você usaria Next e next para lookahead - porque eles avançam à frente do ciclo da linha. Em vez de implementar redundantemente um loop fechado dentro de um loop - como o sedciclo da linha é apenas um loop de leitura simples - se seu objetivo é apenas coletar informações indiscriminadamente, provavelmente é mais fácil:

sed 'H;1h;$!d;x;...'

... que reunirá todo o arquivo ou será estourado.


uma observação lateral Ne comportamento de última linha ...

Embora eu não tenha as ferramentas disponíveis para testar, considere que Nao ler e editar no local se comporta de maneira diferente se o arquivo editado for o arquivo de script da próxima leitura.

mikeserv
fonte
1
Colocar o incondicional em Hprimeiro lugar é adorável.
jthill
Obrigado por sua contribuição. Eu posso ver um benefício potencial em manter o ciclo da linha, mas como é menos trabalho?
dicktyr
@ dicktyr bem, a sintaxe requer alguns atalhos, :a;$!{N;ba}como mencionei acima - é mais fácil usar o formulário padrão a longo prazo quando você tenta executar regexps em sistemas desconhecidos. Mas não foi exatamente isso que eu quis dizer: você implementa um loop fechado - você não pode entrar no meio com tanta facilidade quando quiser, como deseja ramificando - removendo dados indesejados - e deixando o ciclo acontecer. É como uma coisa de cima para baixo - tudo o que sedfaz é um resultado direto do que acabou de fazer. Talvez você o veja de maneira diferente - mas se você tentar, pode achar que o script é mais fácil.
Mikeerv
11

Ele falha porque o Ncomando vem antes da correspondência do padrão $!(não da última linha) e o sed sai antes de executar qualquer trabalho:

N

Adicione uma nova linha ao espaço do padrão e anexe a próxima linha de entrada ao espaço do padrão. Se não houver mais entrada, o sed sai sem processar mais nenhum comando .

Isso pode ser facilmente corrigido para funcionar também com entrada de linha única (e, de fato, para ser mais claro em qualquer caso), basta agrupar os comandos Ne bapós o padrão:

sed ':a;$!{N;ba}; [commands...]'

Funciona da seguinte maneira:

  1. :a crie um rótulo chamado 'a'
  2. $! se não for a última linha, então
  3. Nanexe a próxima linha ao espaço do padrão (ou saia se não houver uma linha seguinte) e o baramo (vá para) rotule 'a'

Infelizmente, não é portátil (pois depende de extensões GNU), mas a seguinte alternativa (sugerida por @mikeserv) é portátil:

sed 'H;1h;$!d;x; [commands...]'
dicktyr
fonte
Publiquei isso aqui porque não encontrei as informações em outro lugar e queria disponibilizá-las para que outros possam evitar problemas com a disseminação :a;N;$!ba;.
dicktyr
Obrigado por publicar! Lembre-se de que aceitar sua própria resposta também é bom. Você só precisa esperar um pouco antes de o sistema permitir.
terdon