Confuso com a saída sed ao usar N. Alguém pode explicar esses resultados?

8

Estou aprendendo sed. Tudo parecia estar indo bem até eu encontrar o N (várias linhas a seguir). Eu criei este arquivo (guide.txt) para fins de prática / compreensão / contexto. Aqui está o conteúdo do referido arquivo ...

This guide is meant to walk you through a day as a Network
Administrator. By the end, hopefully you will be better
equipped to perform your duties as a Network Administrator
and maybe even enjoy being a Network Administrator that much more.
Network Administrator
Network Administrator
I'm a Network Administrator

Portanto, meu objetivo é substituir TODAS as instâncias do "Administrador de Rede" por "Usuário do Sistema". Como a primeira instância do "Administrador de rede" é separada por uma nova linha (\ n), preciso do próximo operador de várias linhas (N) para acrescentar a linha que começa com "Administrador" e a linha anterior que termina com "Rede \ n" . Sem problemas. Mas também quero capturar todas as outras instâncias de linha única "Administrador de rede".

Com minha pesquisa, aprendi que precisarei de dois comandos de substituição; um para a sequência separada por nova linha e um para os outros. Além disso, há alguma ação acontecendo por causa da última linha que contém a correspondência de substituição e a linha múltipla a seguir. Então eu faço isso ...

$ sed '
> s/Network Administrator/System User/
> N
> s/Network\nAdministrator/System\nUser/
> ' guide.txt

Isso retorna esses resultados ...

This guide is meant to walk you through a day as a System
User. By the end, hopefully you will be better
equipped to perform your duties as a System User
and maybe even enjoy being a Network Administrator that much more.
System User
Network Administrator
I'm a System User

Eu pensei que a substituição de linha única pegaria todas as instâncias "normais" de "Administrador de Rede" e a trocaria por "Usuário do sistema", enquanto a instrução de várias linhas funcionaria sua mágica na instância separada de nova linha, mas como você posso vê-lo retornado, o que considero, resultados inesperados.

Depois de algumas brincadeiras, eu cheguei aqui ...

$ sed '
> s/Network Administrator/System User/
> N
> s/Network\nAdministrator/System\nUser/
> s/Network Administrator/System User/
> ' guide.txt

E voilà, eu recebo a saída desejada de ...

This guide is meant to walk you through a day as a System
User. By the end, hopefully you will be better
equipped to perform your duties as a System User
and maybe even enjoy being a System User that much more.
System User
System User
I'm a System User

Por que isso funciona e o script sed original não? Eu realmente quero entender isso.

Agradecemos antecipadamente por qualquer ajuda.

dlowrie290
fonte
Bom em você por aprender Sed! Eu usei o Sed para resolver uma questão muito semelhante anteriormente neste site; pode ser do seu interesse.
Wildcard
E dois outros exemplos complicados Sed: unix.stackexchange.com/a/277375/135943 , unix.stackexchange.com/a/257913/135943
Wildcard

Respostas:

6

Como você está aprendendo sed, aproveitarei o tempo para adicionar a resposta de @ John1024:

1) Observe que você está usando \na string de substituição. Isso funciona no GNU sed, mas não faz parte do POSIX, portanto, ele insere uma barra invertida e um nem muitos outros seds (o uso \ndo padrão é portátil, btw).

Em vez disso, sugiro fazer s/Network\([[:space:]]\)Administrator/System\1Us‌​er/g: O [[:space:]]corresponderá a nova linha ou espaço em branco, para que você não precise de dois scomandos, mas combine-os em um. Ao cercá-lo, \(...\)você pode se referir a ele na substituição: O \1será substituído pelo que corresponder no primeiro par de \(\).

2) Para combinar corretamente os padrões em duas linhas, você deve conhecer o N;P;Dpadrão:

 sed '$!N;s/Network\([[:space:]]\)Administrator/System\1User/g;P;D'

O Né sempre acrescentar a próxima linha (exceto para a última linha, é por isso que é "dirigida" com $!(= se não for última linha, você deve sempre considerar a preceder Ncom $!para evitar que acidentalmente terminando o script) Em seguida, após a substituição dos. PSó imprime a primeira linha no espaço do padrão e Dexclui essa linha e inicia o próximo ciclo com os restos do espaço do padrão (sem ler a próxima linha), provavelmente o que você pretendia originalmente.

Lembre-se deste padrão, você frequentemente precisará dele.

3) Outro padrão útil para a edição de várias linhas, especialmente quando mais de duas linhas estão envolvidas: mantenha a coleta de espaço, como sugeri a John:

sed 'H;1h;$!d;g;s/Network\([[:space:]]\)Administrator/System\1Us‌​er/g'

Repito para explicar: Hacrescenta cada linha ao espaço de espera. Como isso resultaria em uma nova linha extra antes da primeira linha, a primeira linha precisa ser movida em vez de anexada 1h. O seguinte $!dsignifica "para todas as linhas, exceto a última, exclua o espaço do padrão e comece novamente". Assim, o restante do script é executado apenas para a última linha. Nesse ponto, o arquivo inteiro é coletado no espaço de espera (portanto, não use isso para arquivos muito grandes!) E o gmove para o espaço do padrão, para que você possa fazer todas as substituições de uma só vez, como pode com a -zopção de GNU sed.

Esse é outro padrão útil que sugiro ter em mente.

Philippos
fonte
Uau! Ótima explicação! Isso, juntamente com a resposta de John, realmente me deu uma visão melhor desse problema e sedou em geral. Parece que tenho muito mais a aprender. Eu gostaria de poder verificar as duas soluções como respostas. Muito obrigado por ambos os seus esforços. Eles são muito apreciados.
dlowrie290
7

Primeiro, observe que sua solução realmente não funciona. Considere este arquivo de teste:

$ cat test1
Network
Administrator Network
Administrator

E, em seguida, execute o comando:

$ sed '
 s/Network Administrator/System User/
 N
 s/Network\nAdministrator/System\nUser/
 s/Network Administrator/System User/
 ' test1
System
User Network
Administrator

O problema é que o código não substitui o último Network\nAdministrator.

Esta solução funciona:

$ sed ':a; /Network$/{$!{N;ba}}; s/Network\nAdministrator/System\nUser/g; s/Network Administrator/System User/g' test1
System
User System
User

Também podemos aplicar isso ao seu guide.txt:

$ sed ':a; /Network$/{$!{N;ba}}; s/Network\nAdministrator/System\nUser/g; s/Network Administrator/System User/g' guide.txt 
This guide is meant to walk you through a day as a System
User. By the end, hopefully you will be better
equipped to perform your duties as a System User
and maybe even enjoy being a System User that much more.
System User
System User
I'm a System User

A chave é continuar lendo as linhas até encontrar uma que não termine com Network. Quando isso é feito, as substituições podem ser feitas.

Nota de compatibilidade: Todos os itens acima são usados \nno texto de substituição. Isso requer GNU sed. Não funcionará no BSD / OSX sed.

[Gorjeta de chapéu para Philippos .]

Versão multilinha

Se isso ajudar a esclarecer, aqui está o mesmo comando dividido em várias linhas:

$ sed ':a
    /Network$/{
       $!{
           N
           ba
       }
    }
    s/Network\nAdministrator/System\nUser/g
    s/Network Administrator/System User/g
    ' filename

Como funciona

  1. :a

    Isso cria um rótulo a.

  2. /Network$/{ $!{N;ba} }

    Se esta linha terminar com Network, então, se esta não for a última linha ( $!), leia e acrescente a próxima linha ( N) e ramifique novamente para o rótulo a( ba).

  3. s/Network\nAdministrator/System\nUser/g

    Faça a substituição com a nova linha intermediária.

  4. s/Network Administrator/System User/g

    Faça a substituição com o espaço em branco intermediário.

Solução mais simples (apenas GNU)

Com o GNU sed ( não o BSD / OSX), precisamos apenas de um comando substituto:

$ sed -zE 's/Network([[:space:]]+)Administrator/System\1User/g' test1
System
User System
User

E no guide.txtarquivo:

$ sed -zE 's/Network([[:space:]]+)Administrator/System\1User/g' guide.txt 
This guide is meant to walk you through a day as a System
User. By the end, hopefully you will be better
equipped to perform your duties as a System User
and maybe even enjoy being a System User that much more.
System User
System User
I'm a System User

Nesse caso, -zdiz ao sed para ler até o primeiro caractere NUL. Como os arquivos de texto nunca têm um caractere nulo, isso tem o efeito de ler o arquivo inteiro de uma só vez. Podemos então fazer a substituição sem nos preocupar em perder uma linha.

Esse método não é bom se o arquivo for enorme (geralmente significa gigabytes). Se for muito grande, a leitura de tudo de uma só vez pode sobrecarregar a RAM do sistema.

Solução que funciona tanto no GNU quanto no BSD sed

Conforme sugerido por Phillipos , a seguir é uma solução portátil:

sed 'H;1h;$!d;x;s/Network\([[:space:]]\)Administrator/System\1Us‌​er/g'
John1024
fonte
1
Excelente informação, John! Obrigado por esclarecer isso e sua solução alternativa é muito boa. Dito isto, ainda não entendo por que minha solução não é uma solução. Parece funcionar, mas com o seu arquivo test.txt não funciona. Por que minha solução parece funcionar, mas realmente não? Muito obrigado pela ajuda.
dlowrie290
1
@ dlowrie290 Sua solução lê em linhas em pares. Se Network Administratorestiver dividido entre a primeira e a segunda linha desse par, sua solução fará a substituição com êxito. Em seguida, imprime essas duas linhas e lê o próximo par. Se, no entanto, a segunda linha do primeiro par terminar com Networke a primeira linha do segundo par começar Administrator, o código estará ausente. Meu código evita isso lendo linhas, até encontrar um que não termine com Network.
John1024
2
Observe que sua primeira solução multilinha também depende das extensões GNU para sed: A \nsubstituição não está definida no padrão. sed 'H;1h;$!d;x;s/Network\([[:space:]]\)Administrator/System\1User/g'é uma maneira portátil de fazer isso.
Philippos
@ Phillippos Excelentes pontos. Resposta atualizada para incluir a solução portátil.
John1024
1
Obrigado pelo esclarecimento, John! Mais uma vez, grandes coisas e seu tempo / esforços são muito apreciados!
dlowrie290