web-dev-qa-db-fra.com

Comment rechercher un modèle dans le texte d'un fichier et le remplacer par une valeur donnée

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?

112
Dane O'Connor

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
184
Max Chernyak

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.

100
Jim Kane

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:

  1. lit l'ancien fichier et écrit dans le nouveau fichier. (Vous devez faire attention de ne pas mettre des fichiers entiers en mémoire).

  2. 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.

  3. corrige les autorisations de fichier et les modes sur le nouveau fichier.

  4. 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:

  • Si/tmp est monté sur un système de fichiers différent, vous pouvez utiliser/tmp à court d'espace avant d'écrire le fichier qui pourrait sinon être déployé sur la destination de l'ancien fichier.
  • 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:

  • parfois, vous ne pourrez peut-être pas y ouvrir de fichier temporaire, par exemple si vous essayez de modifier un fichier dans/proc, par exemple. Pour cette raison, vous voudrez peut-être revenir en arrière et essayer/tmp si l'ouverture du fichier dans le répertoire de destination échoue.
  • vous devez disposer de suffisamment d'espace sur la partition de destination pour pouvoir contenir l'ancien fichier complet et le nouveau fichier. Cependant, si vous ne disposez pas de suffisamment d’espace pour conserver les deux copies, votre espace disque est probablement insuffisant et le risque réel d’écrire un fichier tronqué est beaucoup plus élevé. Je dirais donc que c’est un très mauvais compromis en dehors de certains très étroits (et bien surveillés).

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é.

46
lamont

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.

11
sepp2k

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 ''.

9
DavidG

Cela fonctionne pour moi:

filename = "foo"
text = File.read(filename) 
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
6
Alain Beauvois

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
6
tanner
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) }
4
Ninad

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.

1
Dan Kohn

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
0
peter