Je recherche un script pour rechercher un fichier (ou une liste de fichiers) pour un modèle et, s'il est trouvé, le remplacer par une valeur donnée.
Pensées?
Je recherche un script pour rechercher un fichier (ou une liste de fichiers) pour un modèle et, s'il est trouvé, le remplacer par une valeur donnée.
Pensées?
Réponses:
Clause de non-responsabilité: Cette approche est une illustration naïve des capacités de Ruby, et non une solution de niveau production pour remplacer des chaînes dans des fichiers. Il est sujet à divers scénarios de défaillance, tels que la perte de données en cas de panne, d'interruption ou de saturation du disque. Ce code n'est pas adapté à autre chose qu'un script ponctuel rapide où toutes les données sont sauvegardées. Pour cette raison, ne copiez PAS ce code dans vos programmes.
Voici un petit moyen rapide de le faire.
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")
En fait, Ruby dispose d'une fonction d'édition sur place. Comme Perl, vous pouvez dire
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Cela appliquera le code entre guillemets à tous les fichiers du répertoire courant dont les noms se terminent par ".txt". Des copies de sauvegarde des fichiers édités seront créées avec une extension ".bak" ("foobar.txt.bak" je pense).
REMARQUE: cela ne semble pas fonctionner pour les recherches multilignes. Pour ceux-ci, vous devez le faire de l'autre manière moins jolie, avec un script wrapper autour de l'expression régulière.
<main>': undefined method
gsub 'pour main: Object (NoMethodError)
-i
modifie en place. .bak
est l'extension utilisée pour un fichier de sauvegarde (facultatif). -p
est quelque chose comme while gets; <script>; puts $_; end
. ( $_
est la dernière ligne lue, mais vous pouvez lui attribuer quelque chose comme echo aa | ruby -p -e '$_.upcase!'
.)
Gardez à l'esprit que, lorsque vous faites cela, le système de fichiers peut être à court d'espace et vous pouvez créer un fichier de longueur nulle. C'est catastrophique si vous faites quelque chose comme l'écriture des fichiers / etc / passwd dans le cadre de la gestion de la configuration du système.
Notez que l'édition de fichier sur place comme dans la réponse acceptée tronquera toujours le fichier et écrira le nouveau fichier de manière séquentielle. Il y aura toujours une condition de concurrence où les lecteurs simultanés verront un fichier tronqué. Si le processus est interrompu pour une raison quelconque (ctrl-c, tueur de MOO, plantage du système, panne de courant, etc.) pendant l'écriture, le fichier tronqué sera également laissé de côté, ce qui peut être catastrophique. C'est le genre de scénario de perte de données que les développeurs DOIVENT considérer car cela se produira. Pour cette raison, je pense que la réponse acceptée ne devrait probablement pas être la réponse acceptée. Au strict minimum, écrivez dans un fichier temporaire et déplacez / renommez le fichier en place comme la solution «simple» à la fin de cette réponse.
Vous devez utiliser un algorithme qui:
Lit l'ancien fichier et écrit dans le nouveau fichier. (Vous devez faire attention à ne pas insérer des fichiers entiers dans la mémoire).
Ferme explicitement le nouveau fichier temporaire, dans lequel vous pouvez lever une exception car les tampons de fichiers ne peuvent pas être écrits sur le disque car il n'y a pas d'espace. (Attrapez ceci et nettoyez le fichier temporaire si vous le souhaitez, mais vous devez relancer quelque chose ou échouer assez dur à ce stade.
Corrige les autorisations et les modes de fichier sur le nouveau fichier.
Renomme le nouveau fichier et le met en place.
Avec les systèmes de fichiers ext3, vous avez la garantie que l'écriture de métadonnées pour déplacer le fichier en place ne sera pas réorganisée par le système de fichiers et écrite avant l'écriture des tampons de données pour le nouveau fichier, donc cela devrait réussir ou échouer. Le système de fichiers ext4 a également été patché pour prendre en charge ce type de comportement. Si vous êtes très paranoïaque, vous devriez appeler lefdatasync()
système à l'étape 3.5 avant de déplacer le fichier en place.
Quelle que soit la langue, c'est la meilleure pratique. Dans les langages où l'appel close()
ne lève pas d'exception (Perl ou C), vous devez vérifier explicitement le retour declose()
et lever une exception en cas d'échec.
La suggestion ci-dessus de simplement glisser le fichier en mémoire, de le manipuler et de l'écrire dans le fichier sera garanti pour produire des fichiers de longueur nulle sur un système de fichiers complet. Vous devez toujours utiliser FileUtils.mv
pour déplacer un fichier temporaire entièrement écrit en place.
Une dernière considération est le placement du dossier temporaire. Si vous ouvrez un fichier dans / tmp, vous devez prendre en compte quelques problèmes:
Si / tmp est monté sur un système de fichiers différent, vous pouvez exécuter / tmp à court d'espace avant d'avoir écrit le fichier qui serait autrement déployable vers la destination de l'ancien fichier.
Probablement plus important, lorsque vous essayez de mv
utiliser le fichier sur un montage de périphérique, vous serez converti de manière transparente en cp
comportement. L'ancien fichier sera ouvert, l'ancien inode de fichiers sera conservé et rouvert et le contenu du fichier sera copié. Ce n'est probablement pas ce que vous voulez et vous risquez de rencontrer des erreurs «fichier texte occupé» si vous essayez de modifier le contenu d'un fichier en cours d'exécution. Cela va également à l'encontre de l'objectif de l'utilisation des mv
commandes du système de fichiers et vous pouvez exécuter le système de fichiers de destination à court d'espace avec seulement un fichier partiellement écrit.
Cela n'a également rien à voir avec l'implémentation de Ruby. Le système mv
et les cp
commandes se comportent de la même manière.
Il est préférable d'ouvrir un fichier temporaire dans le même répertoire que l'ancien fichier. Cela garantit qu'il n'y aura pas de problèmes de déplacement entre appareils. lemv
lui-même ne devrait jamais échouer et vous devriez toujours obtenir un fichier complet et non tronqué. Toutes les pannes, telles que le manque d'espace, les erreurs d'autorisation, etc., doivent être rencontrées lors de l'écriture du fichier Temp.
Les seuls inconvénients de l'approche de création du fichier Temp dans le répertoire de destination sont:
Voici un code qui implémente l'algorithme complet (le code Windows n'est ni testé ni terminé):
#!/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")
Et voici une version légèrement plus stricte qui ne se soucie pas de tous les cas de bord possibles (si vous êtes sous Unix et que vous ne vous souciez pas d'écrire dans / 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")
Le cas d'utilisation vraiment simple, lorsque vous ne vous souciez pas des autorisations du système de fichiers (soit vous ne l'utilisez pas en tant que root, soit vous exécutez en tant que root et le fichier appartient à 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 : Cela devrait être utilisé à la place de la réponse acceptée au minimum, dans tous les cas, afin de garantir que la mise à jour est atomique et que les lecteurs simultanés ne verront pas les fichiers tronqués. Comme je l'ai mentionné ci-dessus, créer le fichier Temp dans le même répertoire que le fichier édité est important ici pour éviter que les opérations mv inter-périphériques ne soient traduites en opérations cp si / tmp est monté sur un périphérique différent. Appeler fdatasync est une couche supplémentaire de paranoïa, mais cela entraînera un coup de performance, donc je l'ai omis de cet exemple car il n'est pas couramment pratiqué.
Il n'y a pas vraiment de moyen de modifier les fichiers sur place. Ce que vous faites habituellement lorsque vous pouvez vous en tirer (c'est-à-dire si les fichiers ne sont pas trop gros) est de lire le fichier en mémoire ( File.read
), d'effectuer vos substitutions sur la chaîne de lecture ( String#gsub
), puis de réécrire la chaîne modifiée dans le fichier ( File.open
,File#write
).
Si les fichiers sont suffisamment gros pour que cela ne soit pas faisable, ce que vous devez faire est de lire le fichier par morceaux (si le modèle que vous souhaitez remplacer ne s'étend pas sur plusieurs lignes, un morceau signifie généralement une ligne - vous pouvez utiliser File.foreach
pour lire un fichier ligne par ligne), et pour chaque morceau, effectuez la substitution et ajoutez-le à un fichier temporaire. Lorsque vous avez terminé l'itération sur le fichier source, vous le fermez et utilisez FileUtils.mv
pour l'écraser avec le fichier temporaire.
Une autre approche consiste à utiliser l'édition inplace dans Ruby (pas à partir de la ligne de commande):
#!/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
Si vous ne souhaitez pas créer de sauvegarde, passez '.bak'
à ''
.
read
) le fichier. Il est évolutif et devrait être très rapide.
Cela fonctionne pour moi:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Voici une solution pour rechercher / remplacer dans tous les fichiers d'un répertoire donné. En gros, j'ai pris la réponse fournie par sepp2k et l'ai développée.
# 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) }
Si vous devez effectuer des substitutions au-delà des limites de ligne, l'utilisation ruby -pi -e
ne fonctionnera pas car le p
traitement une ligne à la fois. Au lieu de cela, je recommande ce qui suit, bien que cela puisse échouer avec un fichier de plusieurs Go:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Le recherche un espace blanc (y compris potentiellement de nouvelles lignes) suivi d'une citation, auquel cas il se débarrasse de l'espace blanc. Il %q(')
s'agit simplement d'une manière élégante de citer le caractère de citation.
Voici une alternative au one liner de jim, cette fois dans un script
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Enregistrez-le dans un script, par exemple replace.rb
Vous commencez sur la ligne de commande avec
replace.rb *.txt <string_to_replace> <replacement>
* .txt peut être remplacé par une autre sélection ou par certains noms de fichiers ou chemins
décomposé pour que je puisse expliquer ce qui se passe mais toujours exécutable
# 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: si vous souhaitez utiliser une expression régulière, utilisez-la à la place. Évidemment, ce n'est que pour gérer des fichiers texte relativement petits, pas de monstres Gigabyte
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
doivent être tempérées avec les informations contenues dans stackoverflow.com/a/25189286/128421 pour savoir pourquoi le slurping de gros fichiers est mauvais. En outre, au lieu d'File.open(filename, "w") { |file| file << content }
utiliser des variantesFile.write(filename, content)
.