Estou procurando um script para pesquisar um arquivo (ou lista de arquivos) por um padrão e, se encontrado, substituir esse padrão por um determinado valor.
Pensamentos?
Estou procurando um script para pesquisar um arquivo (ou lista de arquivos) por um padrão e, se encontrado, substituir esse padrão por um determinado valor.
Pensamentos?
Respostas:
Isenção de responsabilidade: esta abordagem é uma ilustração ingênua dos recursos do Ruby, e não uma solução de nível de produção para substituir strings em arquivos. Está sujeito a vários cenários de falha, como perda de dados em caso de falha, interrupção ou disco cheio. Este código não serve para nada além de um script rápido e único em que é feito o backup de todos os dados. Por esse motivo, NÃO copie este código em seus programas.
Aqui está uma maneira rápida e curta 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
File.write(file_name, text.gsub(/regexp/, "replace")
Na verdade, Ruby tem um recurso de edição local. Como Perl, você pode dizer
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Isso aplicará o código entre aspas a todos os arquivos no diretório atual cujos nomes terminem com ".txt". As cópias de segurança dos arquivos editados serão criadas com uma extensão ".bak" ("foobar.txt.bak" eu acho).
NOTA: isso não parece funcionar para pesquisas em várias linhas. Para eles, você tem que fazer de outra maneira menos bonita, com um script wrapper em torno da regex.
<main>': undefined method
gsub 'para main: Object (NoMethodError)
-i
edita em vigor. .bak
é a extensão usada para um arquivo de backup (opcional). -p
é algo parecido while gets; <script>; puts $_; end
. ( $_
é a última linha lida, mas você pode atribuir a ela para algo como echo aa | ruby -p -e '$_.upcase!'
.)
Tenha em mente que, ao fazer isso, o sistema de arquivos pode ficar sem espaço e você pode criar um arquivo de comprimento zero. Isso é catastrófico se você estiver fazendo algo como gravar arquivos / etc / passwd como parte do gerenciamento de configuração do sistema.
Observe que a edição local do arquivo, 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 simultâneos verão um arquivo truncado. Se o processo for abortado por qualquer motivo (ctrl-c, OOM killer, falha do sistema, queda de energia, etc) durante a gravação, o arquivo truncado também será deixado, o que pode ser catastrófico. Este é o tipo de cenário de perda de dados que os desenvolvedores DEVEM considerar porque isso vai acontecer. Por esse motivo, acho que a resposta aceita provavelmente não deve ser a resposta aceita. No mínimo, grave em um arquivo temporário e mova / renomeie o arquivo no lugar como a solução "simples" no final desta resposta.
Você precisa usar um algoritmo que:
Lê o arquivo antigo e grava no novo arquivo. (Você precisa ter cuidado ao colocar arquivos inteiros na memória).
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 desejar, mas você precisa relançar algo ou falhar bastante neste ponto.
Corrige as permissões e modos de arquivo no novo arquivo.
Renomeia o novo arquivo e o coloca no lugar.
Com sistemas de arquivos ext3, você tem a garantia de que a gravação de metadados para mover o arquivo para o local não será reorganizada pelo sistema de arquivos e gravada antes que os buffers de dados para o novo arquivo sejam gravados, então 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, deve chamar a chamada de fdatasync()
sistema como uma etapa 3.5 antes de mover o arquivo para o lugar.
Independentemente do idioma, essa é a prática recomendada. Em linguagens em que a chamada close()
não lança uma exceção (Perl ou C), você deve verificar explicitamente o retorno de close()
e lançar uma exceção se ele falhar.
A sugestão acima de simplesmente engolir o arquivo na memória, manipulá-lo e gravá-lo no arquivo terá a garantia de produzir arquivos de comprimento zero em um sistema de arquivos completo. Você sempre precisa usar FileUtils.mv
para mover um arquivo temporário totalmente escrito para o lugar.
Uma consideração final é a colocação do arquivo temporário. Se você abrir um arquivo em / tmp, deverá considerar alguns problemas:
Se / tmp estiver montado em um sistema de arquivos diferente, você pode executar / tmp sem espaço antes de gravar o arquivo que, de outra forma, seria implantado no destino do arquivo antigo.
Provavelmente, o mais importante é que ao tentar mv
montar o arquivo em um dispositivo, você será convertido em cp
comportamento de forma transparente . O arquivo antigo será aberto, o inode dos arquivos antigos será preservado e reaberto e o conteúdo do arquivo será copiado. Provavelmente não é isso 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 mv
comandos do sistema de arquivos e você pode executar o sistema de arquivos de destino sem espaço com apenas um arquivo parcialmente escrito.
Isso também não tem nada a ver com a implementação de Ruby. O sistema mv
e os cp
comandos se comportam de maneira semelhante.
O que é mais preferível é abrir um arquivo Temp no mesmo diretório do arquivo antigo. Isso garante que não haverá problemas de movimentação entre dispositivos. O mv
próprio arquivo nunca deve falhar e você sempre deve obter um arquivo completo e não truncado. Quaisquer falhas, como dispositivo sem espaço, erros de permissão, etc., devem ser encontradas durante a gravação do arquivo Temp.
As únicas desvantagens para a abordagem de criação do Tempfile no diretório de destino são:
Aqui está um código que implementa o algoritmo completo (o código do Windows não foi testado e não foi concluído):
#!/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 extremos possíveis (se você estiver no Unix e não se importar em escrever em / 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, para quando você não se importa com as permissões do sistema de arquivos (ou você não está executando como root ou está executando como root e o arquivo é de propriedade do root):
#!/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 no mínimo em vez da resposta aceita, em todos os casos, a fim de garantir que a atualização seja atômica e que os leitores simultâneos não vejam os arquivos truncados. Como mencionei acima, criar o Tempfile no mesmo diretório do arquivo editado é importante aqui para evitar que as operações mv entre dispositivos sejam traduzidas em operações cp se / tmp for montado em um dispositivo diferente. Chamar fdatasync é uma camada adicional de paranóia, mas causará um impacto no desempenho, portanto, omiti neste exemplo, uma vez que não é comumente praticado.
Não há realmente uma maneira de editar arquivos no local. O que você geralmente faz quando consegue se safar (ou seja, se os arquivos não são muito grandes) é ler o arquivo na memória ( File.read
), realizar suas substituições na string de leitura ( String#gsub
) e, em seguida, gravar a string alterada de volta no arquivo ( File.open
, File#write
).
Se os arquivos forem 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 substituir não abranger 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, faça a substituição nele e anexe-o a um arquivo temporário. Quando terminar de iterar o arquivo de origem, feche-o e use FileUtils.mv
para substituí-lo pelo arquivo temporário.
Outra abordagem é usar a edição local dentro do Ruby (não na 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, mude '.bak'
para ''
.
read
) o arquivo. É escalonável e deve ser muito rápido.
Isso funciona para mim:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Aqui está uma solução para localizar / substituir em todos os arquivos de um determinado diretório. Basicamente, peguei a resposta fornecida por sepp2k e a expandi.
# 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
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) }
Se você precisar fazer substituições além dos limites das linhas, o uso ruby -pi -e
não funcionará porque os p
processos são feitos 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 por um espaço em branco (potencialmente incluindo novas linhas) seguido por uma citação, caso em que elimina o espaço em branco. Essa %q(')
é apenas uma maneira elegante de citar o personagem de citação.
Aqui, uma alternativa ao forro de Jim, desta vez em um script
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Salve-o em um script, por exemplo, replace.rb
Você começa na linha de comando com
replace.rb *.txt <string_to_replace> <replacement>
* .txt pode ser substituído por outra seleção ou por 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
EDITAR: se você quiser usar uma expressão regular, use isso. Obviamente, isso é apenas para lidar com arquivos de texto relativamente pequenos, sem monstros Gigabyte
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
precisam ser moderadas com as informações em stackoverflow.com/a/25189286/128421 sobre o motivo pelo qual slurping arquivos grandes é ruim. Além disso, em vez deFile.open(filename, "w") { |file| file << content }
variações, useFile.write(filename, content)
.