Je cherche un script pour rechercher un modèle dans un fichier (ou une liste de fichiers) et, le cas échéant, remplacer ce modèle par une valeur donnée.
Pensées?
Disclaimer: Cette approche est une illustration naïve des capacités de Ruby et non une solution de niveau production pour le remplacement de chaînes dans les 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 ne convient à rien au-delà d'un script unique et rapide dans lequel toutes les données sont sauvegardées. Pour cette raison, ne copiez pas ce code dans vos programmes.
Voici un moyen rapide et 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
En fait, Ruby possède 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 en cours dont le nom se termine par ".txt". Les 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 sur plusieurs lignes. Pour ceux-là, vous devez le faire de manière moins jolie, avec un script wrapper autour de la regex.
Gardez à l'esprit que, lorsque vous faites cela, le système de fichiers risque de manquer d'espace et vous pouvez créer un fichier de longueur nulle. Ceci est catastrophique si vous écrivez des fichiers/etc/passwd dans le cadre de la gestion de la configuration du système.
[EDIT: notez que la modification 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 critique dans laquelle les lecteurs simultanés verront un fichier tronqué. Si le processus est interrompu pour une raison quelconque (ctrl-c, tueur de MOO, panne système, panne d'alimentation, etc.) pendant l'écriture, le fichier tronqué sera également laissé, ce qui peut être catastrophique. C’est le genre de scénario de perte de données que les développeurs DOIVENT envisager car cela se produira. Pour cette raison, je pense que la réponse acceptée ne devrait probablement pas l'être. Au minimum, écrivez dans un fichier temporaire et déplacez/renommez le fichier 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 de ne pas mettre des fichiers entiers en mémoire).
ferme explicitement le nouveau fichier temporaire; vous pouvez donc déclencher une exception car les tampons de fichier ne peuvent pas être écrits sur le disque car il n’ya pas d’espace. (Catch this et nettoyer le fichier temporaire si vous voulez, mais vous devez retaper quelque chose ou échouer assez fort à ce stade.
corrige les autorisations de fichier et les modes sur le nouveau fichier.
renomme le nouveau fichier et le met en place.
Avec les systèmes de fichiers ext3, vous êtes assuré 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. Cela devrait donc réussir ou échouer. Le système de fichiers ext4 a également été corrigé pour prendre en charge ce type de comportement. Si vous êtes très paranoïaque, appelez l’appel système fdatasync()
à l'étape 3.5 avant de placer le fichier en place.
Indépendamment de la langue, c'est la meilleure pratique. Dans les langues où l'appel de close()
ne lève pas d'exception (Perl ou C), vous devez vérifier explicitement le retour de close()
et lever une exception en cas d'échec.
La suggestion ci-dessus consistant à simplement verrouiller le fichier en mémoire, à le manipuler et à l'écrire dans le fichier garantira la production de 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.
Une dernière considération est le placement du fichier temporaire. Si vous ouvrez un fichier dans/tmp, vous devez prendre en compte quelques problèmes:
Probablement plus important encore, lorsque vous essayez de mv
le fichier sur un montage de périphérique, vous êtes converti de manière transparente en comportement cp
. L'ancien fichier sera ouvert, les anciens fichiers inode seront préservés et rouverts 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 annule également l'utilisation des commandes du système de fichiers mv
et vous pouvez exécuter le système de fichiers de destination sans espace avec un fichier partiellement écrit.
Cela n'a rien à voir avec la mise en œuvre de Ruby. Les commandes système mv
et cp
se comportent de la même manière.
Ce qui est préférable, c’est 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 périphériques. Le mv
lui-même ne devrait jamais échouer, et vous devriez toujours obtenir un fichier complet et non tronqué. Toute défaillance, telle qu'un espace insuffisant sur le périphérique, des erreurs d'autorisation, etc., doit être rencontrée lors de l'écriture du fichier temporaire.
Les seuls inconvénients de la création du fichier temporaire dans le répertoire de destination sont les suivants:
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 s'inquiète pas de tous les cas possibles d'Edge (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 très simple, lorsque vous ne vous souciez pas des autorisations du système de fichiers (que vous n'exécutiez pas en tant que root ou que vous exécutiez en tant que root et que le fichier appartienne à la racine):
#!/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é au minimum à la place de la réponse acceptée, dans tous les cas, afin de s’assurer 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, il est important de créer le fichier Temp dans le même répertoire que le fichier modifié pour éviter que les opérations mv entre périphériques ne soient traduites en opérations cp si/tmp est monté sur un autre périphérique. L'appel de fdatasync est une couche supplémentaire de paranoïa, mais cela entraînera un coup dur en performances, je l'ai donc omis de cet exemple car il n'est pas couramment utilisé.
Il n'y a pas vraiment de moyen d'éditer des fichiers sur place. Ce que vous faites habituellement quand vous pouvez vous en sortir (c’est-à-dire si les fichiers ne sont pas trop gros), c’est que vous lisiez le fichier en mémoire (File.read
), effectuez vos substitutions sur la chaîne de lecture (String#gsub
) puis écrivez la chaîne modifiée dans le fichier (File.open
, File#write
).
Si les fichiers sont assez gros pour que cela soit irréalisable, vous devez le lire en morceaux (si le motif 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 bloc, effectuez la substitution et ajoutez-le à un fichier temporaire. Lorsque vous avez terminé d'itérer le fichier source, fermez-le et utilisez FileUtils.mv
pour l'écraser avec le fichier temporaire.
Une autre approche consiste à utiliser la modification en place 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 voulez pas créer de sauvegarde, remplacez '.bak' par ''.
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 je 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 entre lignes, utiliser Ruby -pi -e
Ne fonctionnera pas, car p
ne traite qu'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 des espaces (éventuellement de nouvelles lignes) suivis par une citation, auquel cas il supprime les espaces. La %q(')
est simplement 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 en 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é afin 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