Sto cercando uno script per cercare un file (o un elenco di file) per un pattern e, se trovato, sostituire quel pattern con un dato valore.
Pensieri?
Sto cercando uno script per cercare un file (o un elenco di file) per un pattern e, se trovato, sostituire quel pattern con un dato valore.
Pensieri?
Risposte:
Dichiarazione di non responsabilità: questo approccio è un'illustrazione ingenua delle capacità di Ruby e non una soluzione di livello di produzione per la sostituzione di stringhe nei file. È soggetto a vari scenari di errore, come la perdita di dati in caso di crash, interruzione o disco pieno. Questo codice non è adatto a nulla oltre a un rapido script una tantum in cui viene eseguito il backup di tutti i dati. Per questo motivo, NON copiare questo codice nei tuoi programmi.
Ecco un modo breve e veloce per farlo.
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")
In realtà, Ruby ha una funzione di modifica sul posto. Come Perl, puoi dire
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Questo applicherà il codice tra virgolette a tutti i file nella directory corrente i cui nomi terminano con ".txt". Verranno create copie di backup dei file modificati con l'estensione ".bak" ("foobar.txt.bak" credo).
NOTA: questo non sembra funzionare per le ricerche su più righe. Per quelli, devi farlo in un altro modo meno carino, con uno script wrapper attorno alla regex.
<main>': undefined method
gsub 'per main: Object (NoMethodError)
-i
modifiche in atto. .bak
è l'estensione utilizzata per un file di backup (opzionale). -p
è qualcosa di simile while gets; <script>; puts $_; end
. ( $_
è l'ultima riga letta, ma puoi assegnarla per qualcosa di simile echo aa | ruby -p -e '$_.upcase!'
.)
Tieni presente che, quando lo fai, lo spazio del file system potrebbe essere esaurito e potresti creare un file di lunghezza zero. Questo è catastrofico se stai facendo qualcosa come scrivere file / etc / passwd come parte della gestione della configurazione del sistema.
Nota che la modifica sul posto del file come nella risposta accettata troncerà sempre il file e scriverà il nuovo file in sequenza. Ci sarà sempre una condizione di competizione in cui i lettori simultanei vedranno un file troncato. Se il processo viene interrotto per qualsiasi motivo (ctrl-c, OOM killer, crash del sistema, interruzione di corrente, ecc.) Durante la scrittura, anche il file troncato verrà lasciato, il che può essere catastrofico. Questo è il tipo di scenario di perdita di dati che gli sviluppatori DEVONO considerare perché accadrà. Per questo motivo, penso che la risposta accettata molto probabilmente non dovrebbe essere la risposta accettata. Come minimo, scrivi in un file temporaneo e sposta / rinomina il file in posizione come la soluzione "semplice" alla fine di questa risposta.
È necessario utilizzare un algoritmo che:
Legge il vecchio file e scrive nel nuovo file. (È necessario fare attenzione a risucchiare interi file in memoria).
Chiude esplicitamente il nuovo file temporaneo, che è il punto in cui potresti generare un'eccezione perché i buffer dei file non possono essere scritti su disco perché non c'è spazio. (Prendi questo e ripulisci il file temporaneo, se lo desideri, ma a questo punto devi rilanciare qualcosa o fallire abbastanza difficile.
Corregge le autorizzazioni e le modalità del file sul nuovo file.
Rinomina il nuovo file e lo rilascia in posizione.
Con i filesystem ext3 si ha la garanzia che i metadati scritti per spostare il file in posizione non verranno riorganizzati dal filesystem e scritti prima che i buffer di dati per il nuovo file siano scritti, quindi questo dovrebbe riuscire o fallire. Anche il filesystem ext4 è stato aggiornato per supportare questo tipo di comportamento. Se sei molto paranoico dovresti chiamare la chiamata di fdatasync()
sistema come passo 3.5 prima di spostare il file in posizione.
Indipendentemente dalla lingua, questa è la migliore pratica. Nelle lingue in cui la chiamata close()
non genera un'eccezione (Perl o C), è necessario verificare esplicitamente la restituzione di close()
e lanciare un'eccezione se fallisce.
Il suggerimento sopra di slurp semplicemente il file in memoria, manipolarlo e scriverlo sul file sarà garantito per produrre file di lunghezza zero su un filesystem completo. È necessario utilizzare sempreFileUtils.mv
per spostare in posizione un file temporaneo completamente scritto.
Un'ultima considerazione è il posizionamento del file temporaneo. Se apri un file in / tmp, devi considerare alcuni problemi:
Se / tmp è montato su un file system diverso, è possibile eseguire / tmp senza spazio prima di aver scritto il file che sarebbe altrimenti distribuibile nella destinazione del vecchio file.
Probabilmente ancora più importante, quando provi a inserire mv
il file su un dispositivo montato, verrai convertito in modo trasparente in cp
comportamento. Il vecchio file verrà aperto, il vecchio inode dei file verrà conservato e riaperto e il contenuto del file verrà copiato. Molto probabilmente non è ciò che desideri e potresti incappare in errori di "file di testo occupato" se provi a modificare il contenuto di un file in esecuzione. Questo vanifica anche lo scopo dell'uso dei mv
comandi del filesystem e potresti eseguire il filesystem di destinazione senza spazio con solo un file parzialmente scritto.
Anche questo non ha nulla a che fare con l'implementazione di Ruby. Il sistema mv
e i cp
comandi si comportano in modo simile.
Ciò che è più preferibile è aprire un Tempfile nella stessa directory del vecchio file. Ciò garantisce che non ci saranno problemi di spostamento tra dispositivi. Lo mv
stesso non dovrebbe mai fallire e dovresti sempre ottenere un file completo e non troncato. Eventuali errori, come dispositivo esaurito, errori di autorizzazione, ecc., Dovrebbero essere riscontrati durante la scrittura del Tempfile.
Gli unici svantaggi dell'approccio alla creazione del Tempfile nella directory di destinazione sono:
Ecco un po 'di codice che implementa l'algoritmo completo (il codice di Windows non è stato testato e non è finito):
#!/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")
Ed ecco una versione leggermente più stretta che non si preoccupa di ogni possibile caso limite (se sei su Unix e non ti interessa scrivere su / 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")
Il caso d'uso davvero semplice, per quando non ti interessano le autorizzazioni del file system (o non stai eseguendo come root, o stai eseguendo come root e il file è di proprietà di 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 : dovrebbe essere usato al posto della risposta accettata come minimo, in tutti i casi, al fine di garantire che l'aggiornamento sia atomico e che i lettori simultanei non vedano i file troncati. Come accennato in precedenza, creare il file Temp nella stessa directory del file modificato è importante qui per evitare che le operazioni mv cross-device vengano tradotte in operazioni cp se / tmp è montato su un dispositivo diverso. Chiamare fdatasync è un ulteriore livello di paranoia, ma incorrerà in un calo delle prestazioni, quindi l'ho omesso da questo esempio poiché non è comunemente praticato.
Non c'è davvero un modo per modificare i file sul posto. Quello che di solito fai quando puoi farla franca (cioè se i file non sono troppo grandi) è leggere il file in memory ( File.read
), eseguire le sostituzioni sulla stringa di lettura ( String#gsub
) e quindi riscrivere la stringa modificata nel file ( File.open
, File#write
).
Se i file sono abbastanza grandi da non essere fattibile, quello che devi fare è leggere il file in blocchi (se il modello che vuoi sostituire non si estende su più righe, un pezzo di solito significa una riga - puoi usare File.foreach
per leggere un file riga per riga), e per ogni blocco eseguire la sostituzione su di esso e aggiungerlo a un file temporaneo. Quando hai finito di iterare sul file sorgente, lo chiudi e FileUtils.mv
lo sovrascrivi con il file temporaneo.
Un altro approccio consiste nell'usare la modifica sul posto all'interno di Ruby (non dalla riga di 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 non desideri creare un backup, '.bak'
passa a ''
.
read
) il file. È scalabile e dovrebbe essere molto veloce.
Questo funziona per me:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Ecco una soluzione per trovare / sostituire in tutti i file di una determinata directory. Fondamentalmente ho preso la risposta fornita da sepp2k e l'ho ampliata.
# 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 è necessario eseguire sostituzioni oltre i confini delle linee, l'utilizzo ruby -pi -e
non funzionerà perché p
elabora una riga alla volta. Invece, consiglio quanto segue, anche se potrebbe non riuscire con un file multi-GB:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Il sta cercando uno spazio bianco (potenzialmente includendo nuove righe) seguito da una citazione, nel qual caso si sbarazza degli spazi bianchi. Il %q(')
è solo un modo elegante per citare il carattere preventivo.
Ecco un'alternativa a quella di jim, questa volta in una sceneggiatura
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Salvalo in uno script, ad es. Replace.rb
Inizi dalla riga di comando con
replace.rb *.txt <string_to_replace> <replacement>
* .txt può essere sostituito con un'altra selezione o con alcuni nomi di file o percorsi
suddiviso in modo da poter spiegare cosa sta succedendo ma ancora eseguibile
# 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
EDIT: se vuoi usare un'espressione regolare usa invece questa Ovviamente, questo è solo per la gestione di file di testo relativamente piccoli, nessun mostro Gigabyte
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
deve essere temperato con le informazioni in stackoverflow.com/a/25189286/128421 per il motivo per cui lo slurping di file di grandi dimensioni è dannoso. Inoltre, invece diFile.open(filename, "w") { |file| file << content }
utilizzare variazioniFile.write(filename, content)
.