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


117

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?


1
Dans les réponses ci-dessous, sachez que toutes les recommandations à utiliser File.readdoivent ê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 variantes File.write(filename, content).
the Tin Man

Réponses:


190

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

Est-ce que met réécrit la modification dans le fichier? Je pensais que cela ne ferait qu'imprimer le contenu sur la console.
Dane O'Connor

Oui, il imprime le contenu sur la console.
sepp2k

7
Oui, je n'étais pas sûr que ce soit ce que vous vouliez. Pour écrire, utilisez File.open (nom_fichier, "w") {| fichier | file.puts output_of_gsub}
Max Chernyak

7
J'ai dû utiliser file.write: File.open (file_name, "w") {| file | file.write (text)}
austen

3
Pour écrire un fichier, remplacez met 'line withFile.write(file_name, text.gsub(/regexp/, "replace")
tight

106

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.


1
Qu'est-ce que c'est que pi.bak? Sans cela, j'obtiens une erreur. -e: 1: dans <main>': undefined method gsub 'pour main: Object (NoMethodError)
Ninad

15
@NinadPachpute -imodifie en place. .bakest l'extension utilisée pour un fichier de sauvegarde (facultatif). -pest 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!'.)
Lri

1
C'est une meilleure réponse que la réponse acceptée, à mon humble avis, si vous cherchez à modifier le fichier.
Colin K

6
Comment puis-je utiliser cela dans un script ruby ​​??
Saurabh

1
Cela peut mal tourner de nombreuses manières, alors testez-le soigneusement avant de le tenter contre un fichier critique.
the Tin Man

49

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:

  1. 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).

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

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

  4. 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.mvpour 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 cpcomportement. 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 mvcommandes 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 mvet les cpcommandes 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:

  • Parfois, vous ne pourrez peut-être pas y ouvrir un fichier temporaire, par exemple si vous essayez d'éditer un fichier dans / proc. 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 contenir à la fois l'ancien fichier complet et le nouveau fichier. Cependant, si vous n'avez pas suffisamment d'espace pour contenir les deux copies, vous manquez probablement d'espace disque 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 extrêmement étroits (et bien -surveillance) les cas de bord.

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


Au lieu d'ouvrir un fichier temporaire dans le répertoire dans lequel vous vous trouvez, il en créera automatiquement un dans le répertoire de données de l'application (de toute façon sous Windows) et à partir de leur, vous pouvez faire un file.unlink pour le supprimer ..
13aal

3
J'ai vraiment apprécié la réflexion supplémentaire qui a été apportée à cela. En tant que débutant, il est très intéressant de voir les schémas de pensée des développeurs expérimentés qui ne peuvent pas simplement répondre à la question originale, mais aussi commenter le contexte plus large de ce que signifie réellement la question originale.
ramijames

La programmation ne consiste pas seulement à résoudre le problème immédiat, c'est aussi à penser à l'avance pour éviter que d'autres problèmes ne vous attendent. Rien n'irrite plus un développeur senior que de rencontrer du code qui a dépeint l'algorithme dans un coin, forçant un kludge maladroit, alors qu'un ajustement mineur plus tôt aurait abouti à un bon flux. L'analyse peut souvent prendre des heures, voire des jours, pour comprendre l'objectif, puis quelques lignes remplacent une page d'ancien code. C'est comme un jeu d'échecs contre les données et le système parfois.
the Tin Man

11

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.foreachpour 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.mvpour l'écraser avec le fichier temporaire.


1
J'aime l'approche du streaming. Nous traitons de gros fichiers simultanément, donc nous n'avons généralement pas l'espace dans la RAM pour lire le fichier entier
Shane

« Pourquoi« slurper »un fichier n'est pas une bonne pratique? » Pourrait être une lecture utile à ce sujet.
the Tin Man

9

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


1
Ce serait mieux que d'essayer de slurp ( read) le fichier. Il est évolutif et devrait être très rapide.
the Tin Man

Il y a un bogue quelque part provoquant l'échec de Ruby 2.3.0p0 sur Windows avec l'autorisation refusée s'il y a plusieurs blocs inplace_edit consécutifs travaillant sur le même fichier. Pour reproduire les tests search1 et search2 fractionnés en 2 blocs. Vous ne fermez pas complètement?
mlt

Je m'attendrais à des problèmes avec plusieurs modifications d'un fichier texte simultanément. Si rien d'autre, vous pourriez obtenir un fichier texte mal mutilé.
the Tin Man

7

Cela fonctionne pour moi:

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

6

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

4
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) }

2
Cela aide davantage si vous expliquez pourquoi c'est la solution préférée et expliquez son fonctionnement. Nous voulons éduquer, pas seulement fournir du code.
the Tin Man

trollop a été renommé optimist github.com/manageiq/optimist . De plus, c'est juste un analyseur d'options CLI qui n'est pas vraiment nécessaire pour répondre à la question.
noraj

1

Si vous devez effectuer des substitutions au-delà des limites de ligne, l'utilisation ruby -pi -ene fonctionnera pas car le ptraitement 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.


1

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]))}

Ce code ne fonctionnera pas. Je suggérerais de le tester avant de publier, puis de copier et coller le code de travail.
the Tin Man

@theTinMan Je teste toujours avant de publier, si possible. J'ai testé ça et ça marche, aussi bien la version courte que la version commentée. Pourquoi pensez-vous que ce ne serait pas le cas?
peter

si vous voulez dire en utilisant une expression régulière voir ma modification, également testée:>)
peter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.