Como pesquisar texto de arquivo para um padrão e substituí-lo por um determinado valor

Eu estou procurando um script para procurar um arquivo (ou lista de arquivos) para um padrão e, se encontrado, replace esse padrão com um determinado valor.

Pensamentos?

Aqui está uma maneira rápida e rápida de fazer isso.

 file_names = ['foo.txt', 'bar.txt'] file_names.each do |file_name| text = File.read(file_name) new_contents = text.gsub(/search_regexp/, "replacement string") # To merely print the contents of the file, use: puts new_contents # To write changes to the file, use: File.open(file_name, "w") {|file| file.puts new_contents } end 

Na verdade, o Ruby tem um recurso de edição no local. Como o Perl, você pode dizer

 ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt 

Isso aplicará o código entre aspas duplas para todos os arquivos no diretório atual cujos nomes terminam com “.txt”. Cópias de backup de arquivos editados serão criadas com uma extensão “.bak” (“foobar.txt.bak”, eu acho).

NOTA: isso não parece funcionar para pesquisas de múltiplas linhas. Para aqueles, você tem que fazer do outro jeito menos bonito, com um script wrapper em torno do regex.

Tenha em mente que, quando você fizer isso, o sistema de arquivos pode estar sem espaço e você pode criar um arquivo de comprimento zero. Isso é catastrófico se você estiver fazendo algo como escrever arquivos / etc / passwd como parte do gerenciamento da configuração do sistema.

[EDIT: observe que a edição de arquivos no local, como na resposta aceita, sempre truncará o arquivo e gravará o novo arquivo sequencialmente. Sempre haverá uma condição de corrida em que os leitores concorrentes verão um arquivo truncado ou parcialmente truncado, que pode ser catastrófico. Por essa razão, acho que a resposta aceita provavelmente não seria a resposta aceita. ]

Você precisa usar um algoritmo que:

  1. lê o arquivo antigo e grava no novo arquivo. (Você precisa ter cuidado ao chupar arquivos inteiros na memory).

  2. fecha explicitamente o novo arquivo temporário, que é onde você pode lançar uma exceção porque os buffers de arquivo não podem ser gravados no disco porque não há espaço. (Pegue isso e limpe o arquivo temporário, se quiser, mas é preciso relançar algo ou falhar bastante nesse ponto.

  3. Corrige as permissions e modos de arquivo no novo arquivo.

  4. renomeia o novo arquivo e o coloca no lugar.

Com os filesystems ext3, é garantido que os metadados gravados para mover o arquivo para o lugar não serão reorganizados pelo sistema de arquivos e gravados antes que os buffers de dados para o novo arquivo sejam gravados, portanto, isso deve ser bem-sucedido ou falhar. O sistema de arquivos ext4 também foi corrigido para suportar esse tipo de comportamento. Se você é muito paranóico, você deve chamar a chamada do sistema fdatasync() como uma etapa 3.5 antes de mover o arquivo para o lugar.

Independentemente da linguagem, essa é a melhor prática. Em linguagens onde chamar close() não lança uma exceção (Perl ou C) você deve explicitamente verificar o retorno de close() e lançar uma exceção se falhar.

A sugestão acima para simplesmente inserir o arquivo na memory, manipulá-lo e escrevê-lo no arquivo será garantida para produzir arquivos de comprimento zero em um sistema de arquivos completo. Você precisa sempre usar o FileUtils.mv para mover um arquivo temporário totalmente escrito para o lugar.

Uma consideração final é o posicionamento do arquivo temporário. Se você abrir um arquivo em / tmp, terá que considerar alguns problemas:

  • Se / tmp estiver montado em um sistema de arquivos diferente, você poderá executar / tmp falta de espaço antes de ter gravado o arquivo que, de outra forma, seria implementado no destino do arquivo antigo.
  • Provavelmente, mais importante, quando você tenta mover o arquivo através de uma assembly de dispositivo, você será convertido de forma transparente para o comportamento de cp . O arquivo antigo será aberto, o arquivo antigo inode será preservado e reaberto e o conteúdo do arquivo será copiado. Isso provavelmente não é o que você deseja, e você pode encontrar erros de “arquivo de texto ocupado” se tentar editar o conteúdo de um arquivo em execução. Isso também anula o propósito de usar os comandos mv do sistema de arquivos e você pode executar o espaço de arquivos de destino com apenas um arquivo parcialmente gravado.

    Isso também não tem nada a ver com a implementação do Ruby. Os comandos mv e cp sistema se comportam de maneira semelhante.

O que é mais preferível é abrir um arquivo Temp no mesmo diretório que o arquivo antigo. Isso garante que não haverá problemas de movimentação entre dispositivos. O próprio mv nunca deve falhar, e você deve sempre obter um arquivo completo e não-truncado. Quaisquer falhas, como espaço no dispositivo, erros de permissão, etc., devem ser encontradas durante a gravação do Tempfile.

As únicas desvantagens da abordagem de criação do Tempfile no diretório de destino são:

  • às vezes você pode não ser capaz de abrir um Tempfile lá, como se você estivesse tentando ‘editar’ um arquivo em / proc por exemplo. Por essa razão, você pode querer retroceder e tentar / tmp se a abertura do arquivo no diretório de destino falhar.
  • você deve ter espaço suficiente na partição de destino para manter o arquivo antigo completo e o novo. No entanto, se você tiver espaço insuficiente para armazenar as duas cópias, provavelmente terá pouco espaço em disco eo risco real de gravar um arquivo truncado será muito maior, por isso argumentaria que esse é um tradeoff muito ruim, não muito restrito (e bem monitorados).

Aqui está um código que implementa o algoritmo completo (o código do Windows não foi testado e está inacabado):

 #!/usr/bin/env ruby require 'tempfile' def file_edit(filename, regexp, replacement) tempdir = File.dirname(filename) tempprefix = File.basename(filename) tempprefix.prepend('.') unless RUBY_PLATFORM =~ /mswin|mingw|windows/ tempfile = begin Tempfile.new(tempprefix, tempdir) rescue Tempfile.new(tempprefix) end File.open(filename).each do |line| tempfile.puts line.gsub(regexp, replacement) end tempfile.fdatasync unless RUBY_PLATFORM =~ /mswin|mingw|windows/ tempfile.close unless RUBY_PLATFORM =~ /mswin|mingw|windows/ stat = File.stat(filename) FileUtils.chown stat.uid, stat.gid, tempfile.path FileUtils.chmod stat.mode, tempfile.path else # FIXME: apply perms on windows end FileUtils.mv tempfile.path, filename end file_edit('/tmp/foo', /foo/, "baz") 

E aqui está uma versão um pouco mais restrita que não se preocupa com todos os casos possíveis (se você está no Unix e não se importa em escrever para / proc):

 #!/usr/bin/env ruby require 'tempfile' def file_edit(filename, regexp, replacement) Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile| File.open(filename).each do |line| tempfile.puts line.gsub(regexp, replacement) end tempfile.fdatasync tempfile.close stat = File.stat(filename) FileUtils.chown stat.uid, stat.gid, tempfile.path FileUtils.chmod stat.mode, tempfile.path FileUtils.mv tempfile.path, filename end end file_edit('/tmp/foo', /foo/, "baz") 

O caso de uso realmente simples, quando você não se importa com as permissions do sistema de arquivos (você não está executando como root ou está executando como root e o arquivo é de propriedade raiz):

 #!/usr/bin/env ruby require 'tempfile' def file_edit(filename, regexp, replacement) Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile| File.open(filename).each do |line| tempfile.puts line.gsub(regexp, replacement) end tempfile.close FileUtils.mv tempfile.path, filename end end file_edit('/tmp/foo', /foo/, "baz") 

TL; DR: Deve ser usado em vez da resposta aceita, no mínimo, em todos os casos, para garantir que a atualização seja atômica e os leitores concorrentes não verão arquivos truncados. Como mencionei acima, criar o Tempfile no mesmo diretório que o arquivo editado é importante aqui para evitar que operações mv entre dispositivos sejam traduzidas em operações cp se / tmp for montado em um dispositivo diferente. Chamar o fdatasync é uma camada adicional de paranóia, mas ele causará um impacto no desempenho, por isso omiti esse exemplo, já que ele não é comumente praticado.

Não há realmente uma maneira de editar arquivos no local. O que você normalmente faz quando consegue se safar (ou seja, se os arquivos não são muito grandes) é, você lê o arquivo na memory ( File.read ), executa suas substituições na string de leitura ( String#gsub ) e então escreve a string alterada de volta para o arquivo ( File.open , File#write ).

Se os arquivos são grandes o suficiente para que isso seja inviável, o que você precisa fazer é ler o arquivo em pedaços (se o padrão que você deseja replace não ocupar várias linhas, então um pedaço geralmente significa uma linha – você pode usar File.foreach para ler um arquivo linha por linha), e para cada pedaço executar a substituição e anexá-lo a um arquivo temporário. Quando terminar de iterar sobre o arquivo de origem, feche-o e use FileUtils.mv para substituí-lo pelo arquivo temporário.

Outra abordagem é usar a edição no local dentro do Ruby (não da linha de comando):

 #!/usr/bin/ruby def inplace_edit(file, bak, &block) old_stdout = $stdout argf = ARGF.clone argf.argv.replace [file] argf.inplace_mode = bak argf.each_line do |line| yield line end argf.close $stdout = old_stdout end inplace_edit 'test.txt', '.bak' do |line| line = line.gsub(/search1/,"replace1") line = line.gsub(/search2/,"replace2") print line unless line.match(/something/) end 

Se você não quiser criar um backup, altere ‘.bak’ para ”.

Aqui está uma solução para localizar / replace em todos os arquivos de um determinado diretório. Basicamente peguei a resposta fornecida pelo sepp2k e a ampliei.

 # First set the files to search/replace in files = Dir.glob("/PATH/*") # Then set the variables for find/replace @original_string_or_regex = /REGEX/ @replacement_string = "STRING" files.each do |file_name| text = File.read(file_name) replace = text.gsub!(@original_string_or_regex, @replacement_string) File.open(file_name, "w") { |file| file.puts replace } end 

Isso funciona para mim:

 filename = "foo" text = File.read(filename) content = text.gsub(/search_regexp/, "replacestring") File.open(filename, "w") { |file| file << content } 
 require 'trollop' opts = Trollop::options do opt :output, "Output file", :type => String opt :input, "Input file", :type => String opt :ss, "String to search", :type => String opt :rs, "String to replace", :type => String end text = File.read(opts.input) text.gsub!(opts.ss, opts.rs) File.open(opts.output, 'w') { |f| f.write(text) } 

Aqui uma alternativa para o forro de um jim, desta vez em um script

 ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))} 

Salve em um script, por exemplo, replace.rb

Você começa na linha de comando com

 replace.rb *.txt   

* .txt pode ser substituído por outra seleção ou com alguns nomes de arquivos ou caminhos

dividido para que eu possa explicar o que está acontecendo, mas ainda executável

 # ARGV is an array of the arguments passed to the script. ARGV[0..-3].each do |f| # enumerate the arguments of this script from the first to the last (-1) minus 2 File.write(f, # open the argument (= filename) for writing File.read(f) # open the argument (= filename) for reading .gsub(ARGV[-2],ARGV[-1])) # and replace all occurances of the beforelast with the last argument (string) end 

Se você precisar fazer substituições entre limites de linha, usar ruby -pi -e não funcionará porque o p processa uma linha por vez. Em vez disso, recomendo o seguinte, embora possa falhar com um arquivo de vários GB:

 ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))" 

O está procurando espaço em branco (potencialmente incluindo novas linhas) seguido por uma citação, caso em que se livrar do espaço em branco. O %q(') é apenas uma maneira sofisticada de citar o caractere de aspas.