Existe um aplicativo utilitário de linha de comando que pode localizar um bloco específico de linhas em um arquivo de texto e substituí-lo?

7

UPDATE (veja o final da pergunta)

Os programas utilitários "pesquisar e substituir" de texto que vi parecem pesquisar apenas linha a linha ...

Existe uma ferramenta de linha de comando que pode localizar um bloco de linhas (em um arquivo de texto) e substituí-lo por outro bloco de linhas.

Por exemplo: o arquivo de teste contém este exact group de linhas:

'Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,  
And the mome raths outgrabe. 

'Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!'

Eu quero isso, para poder substituir várias linhas de texto em um arquivo e saber que não estou sobrescrevendo as linhas erradas.

Eu nunca substituiria "The Jabberwocky" (Lewis Carroll), mas é um novo exemplo:)

UPDATE :
.. (sub-update) Meu comentário a seguir sobre as razões quando não usam sed são somente no contexto de; não empurre nenhuma ferramenta muito além de sua intenção de design (eu uso sed com bastante frequência e a considero inestimável).

Acabei de encontrar uma página da Web interessante sobre sed e quando não usá-la.
Então, por causa de todas as sed respostas, eu vou postar o link .. é parte do sed FAQ no sourceforge

Além disso, tenho certeza de que existe uma maneira de diff fazer o trabalho de localizar o bloco de texto (uma vez localizado, a substituição é bem direta; usando head e tail ) ... 'diff' descarta todos os dados necessários, mas eu ainda não descobri como filtrá-lo, ... (Ainda estou trabalhando nisso)

    
por Peter.O 08.01.2011 / 20:11

5 respostas

7

Este script python simples deve executar a tarefa:


#!/usr/bin/env python

# Syntax: multiline-replace.py input.txt search.txt replacement.txt

import sys

inp = open(sys.argv[1]).read()
needle = open(sys.argv[2]).read()
replacement = open(sys.argv[3]).read()

sys.stdout.write(inp.replace(needle,replacement))

Como a maioria das outras soluções, tem a desvantagem de que todo o arquivo é sugado para a memória de uma só vez. Para pequenos arquivos de texto, ele deve funcionar bem o suficiente, no entanto.

    
por loevborg 09.01.2011 / 18:57
3

Abordagem 1: alterar temporariamente as novas linhas para outra coisa

O snippet a seguir troca novas linhas com pipes, realiza a substituição e troca os separadores de volta. O utilitário pode sufocar se a linha for extremamente longa. Você pode escolher qualquer caractere para trocar, desde que não esteja na sua string de busca.

<old.txt tr '\n' '|' |
sed 's/\(|\|^\)'\''Twas … toves|Did … Bandersnatch!'\''|/new line 1|new line 2|/g' |
tr '|' '\n' >new.txt

Abordagem 2: altere o separador de registro do utilitário

O awk e o perl suportam a configuração de duas ou mais linhas em branco como o separador de registro. Com awk, passe -vRS= (vazio RS variable). Com Perl, passe -000 ("modo de parágrafo") ou defina $,="" . Isso não é útil aqui, já que você tem uma string de pesquisa com vários parágrafos.

O awk e o perl também suportam a configuração de qualquer string como separador de registro. Defina RS ou $, para qualquer string que não esteja na sua string de pesquisa.

<old.txt perl -pe '
    BEGIN {$, = "|"}
    s/^'\''Twas … toves\nDid … Bandersnatch!'\''$/new line 1\nnew line 2/mg
' >new.txt

Abordagem 3: trabalhe no arquivo inteiro

Alguns utilitários permitem que você leia todo o arquivo na memória e trabalhe nele.

<old.txt perl -0777 -pe '
    s/^'\''Twas … toves\nDid … Bandersnatch!'\''$/new line 1\nnew line 2/mg
' >new.txt

Abordagem 4: programa

Leia as linhas uma por uma. Comece com um buffer vazio. Se você vir a linha "Twas" e o buffer estiver vazio, coloque-o no buffer. Se você vir o "Did gyre" e houver uma linha no buffer, anexe a linha atual ao buffer e assim por diante. Se você acabou de acrescentar a "linha Bandersnatch", imprima o texto de substituição. Se a linha atual não entrou no buffer, imprima o conteúdo do buffer, imprima a linha atual e esvazie o buffer.

psusi mostra uma implementação sed. No sed, o conceito de buffer é embutido; é chamado de espaço de espera. Em awk ou perl, você usaria apenas uma variável (talvez duas, uma para o conteúdo do buffer e outra para o número de linhas).

    
por Gilles 08.01.2011 / 21:03
2

Eu tinha certeza que tinha que haver uma maneira de fazer isso com sed. Depois de alguns googling me deparei com isso:

link

Com base nisso, acabei escrevendo:

sed -n '1h;1!H;${;g;s/foo\nbar/jar\nhead/g;p;}' < x

Qual deles pegou corretamente o conteúdo de x:

foo bar

E cuspir:

jar cabeça

    
por psusi 08.01.2011 / 20:42
2

Mesmo que você não goste de hoary sed e perl , você ainda pode achar um gosto em awk . Esta resposta parece ser o que você está procurando. Eu reproduzo aqui. Digamos que você tenha três arquivos e queira substituir needle por replacement em haystack :


awk ' BEGIN { RS="" }
      FILENAME==ARGV[1] { s=$0 }
      FILENAME==ARGV[2] { r=$0 }
      FILENAME==ARGV[3] { sub(s,r) ; print }
    ' needle replacement haystack > output

Isso não envolve expressões regulares e suporta caracteres de nova linha. Parece funcionar com arquivos razoavelmente grandes. Isso envolve a inclusão de todo o arquivo na memória, por isso não funcionará com arquivos de tamanho arbitrário. Se você quiser mais elegante, você pode colocar todo o texto em um script bash ou transformá-lo em um script awk .

    
por loevborg 09.01.2011 / 13:40
2

UPDATE : o script python do loevborg é certamente a solução mais simples e melhor (não há dúvida sobre isso) e estou muito feliz com isso, mas gostaria de salientar que o script bash Eu apresentei (no final da questão) não é nem de longe tão complicado quanto parece .. Eu aparado toda a escória de depuração que eu usei para testá-lo .. e aqui está novamente sem o overburden (para quem visita esta página) .. É basicamente um sed one-liner, com pré e pós hex-conversões:

F=("$haystack"  "$needle"  "$replacement")
for f in "${F[@]}" ; do cat "$f" | hexdump -v -e '1/1 "%02x"' > "$f.hex" ; done
sed -i "s/$(cat "${F[1])}.hex")/$(cat "${F[2])}.hex")/p" "${F[0])}.hex"
cat "${F[0])}.hex" | xxd -r -p > "${F[0])}"
# delete the temp *.hex files.

Apenas para jogar meu chapéu no ringue, eu criei uma solução 'sed' que não vai ter problemas com caracteres regex especiais , porque não usa nem um! .. em vez disso, ele funciona em versões Hexdumped dos arquivos ...

Eu acho que é muito "top pesado", mas funciona, e aparentemente não é restrito por quaisquer limitações de tamanho. O GNU sed tem um tamanho de buffer padrão ilimitado, e é aí que o Hexdump bloco de linhas de pesquisa termina .. Então está tudo bem a esse respeito ...

Eu ainda estou procurando por uma solução diff , porque ela será mais flexível em relação ao espaço em branco (e eu esperaria; mais rápido) ... mas até então .. É o famoso Sr. Sed. :)

Este script está totalmente funcionando como está e é razoavelmente comentado ...
Parece maior que seja; Eu tenho apenas 7 linhas de código essencial.
Para um teste semi-realista, ele faz o download do livro "Alice Through the Looking Glass" do Project Gutenberg (363.1 KB) ... e substitui o poema original de Jabberwocky por uma versão invertida de si mesma. (Curiosamente, não é muito diferente lê-lo de trás para frente :)

PS. Eu só percebi que uma fraqueza nesse método é se o seu original usa \ r \ n (0xODOA) como sua nova linha, e seu "texto para correspondência" é salvo com \ n (0x0A) .. então este processo de correspondência está inativo no água ... ('diff' não tem esses problemas) ...

# In a text file, replace one block of lines with another block
#
# Keeping with the 'Jabberwocky' theme, 
#  and using 'sed' with 'hexdump', so 
#  there is no possible *special* char clash.
# 
# The current setup will replace only the first instance.
#   Using sed's 'g' command, it cah change all instances. 
#

  lookinglass="$HOME/Through the Looking-Glass by Lewis Carroll"
  jabberwocky="$lookinglass (jabberwocky)"
  ykcowrebbaj="$lookinglass (ykcowrebbaj)"

  ##### This section if FOR TEST PREPARATION ONLY
        fromURL="http://www.gutenberg.org/ebooks/12.txt.utf8"
        wget $fromURL -O "$lookinglass"
        if (($?==0))
        then  echo "Download OK"
        else  exit 1
        fi
        # Make a backup of the original (while testing)
        cp "$lookinglass" "$lookinglass(fromURL)"
        #
        # Extact the poem and write it to a file. (It runs from line 322-359)
        sed -n 322,359p "$lookinglass" > "$jabberwocky"
        cat "$jabberwocky"; read -p "This is the original.. (press Enter to continue)"
        #
        # Make a file containing a replacement block of lines
        tac "$jabberwocky" > "$ykcowrebbaj"
        cat "$ykcowrebbaj"; read -p "This is the REPLACEMENT.. (press Enter to continue)"
  ##### End TEST PREPARATION

# The main process
#
# Make 'hexdump' versions of the 3 files... source, expected, replacement 
  cat "$lookinglass" | hexdump -v -e '1/1 "%02x"' > "$lookinglass.xdig"
  cat "$jabberwocky" | hexdump -v -e '1/1 "%02x"' > "$jabberwocky.xdig"
  cat "$ykcowrebbaj" | hexdump -v -e '1/1 "%02x"' > "$ykcowrebbaj.xdig"
# Now use 'sed' in a safe (no special chrs) way.
# Note, all files are now each, a single line  ('\n' is now '0A')
  sed -i "s/$(cat "$jabberwocky.xdig")/$(cat "$ykcowrebbaj.xdig")/p" "$lookinglass.xdig"

  ##### This section if FOR CHECKING THE RESULTS ONLY
        # Check result 1
        read -p "About to test for the presence of  'jabberwocky.xdig'  within itself (Enter) "
        sed -n "/$(cat "$jabberwocky.xdig")/p"     "$jabberwocky.xdig"
        echo -e "\n\nA dump above this line, means: 'jabberwocky' is as expected\n" 
        # Check result 2
        read -p "About to test for the presence of  'ykcowrebbaj.xdig'  within itself (Enter) "
        sed -n "/$(cat "$ykcowrebbaj.xdig")/p"     "$ykcowrebbaj.xdig"
        echo -e "\n\nA dump above this line, means: 'ykcowrebbaj' is as expected\n" 
        # Check result 3
        read -p "About to test for the presence of  'lookinglass.xdig'  within itself (Enter) "
        sed -n "/$(cat "$ykcowrebbaj.xdig")/p"     "$lookinglass.xdig"
        echo -e "\n\nA dump above this line, means: 'lookinglass' is as expected\n" 
        # Check result 4
        read -p "About to test for the presence of  'lookinglass.xdig'  within itself (Enter) "
        sed -n "/$(cat "$jabberwocky.xdig")/p"     "$lookinglass.xdig"
        echo -e "\n\nNo dump above this line means: 'lookinglass' is as expected\n"
  ##### End of CHECKING THE RESULTS

# Now convert the hexdump to binary, and overwrite the original
  cat "$lookinglass.xdig" | xxd -r -p > "$lookinglass"
# Echo the "modified" poem to the screen
  sed -n 322,359p "$lookinglass"
  echo -e "\n\nYou are now looking at the REPLACEMENT text (dumped directly from the source 'book'"
    
por Peter.O 09.01.2011 / 15:05