Cómo buscar texto de archivo para un patrón y reemplazarlo por un valor determinado

Estoy buscando un script para buscar un archivo (o una lista de archivos) para un patrón y, si lo encuentra, reemplazar ese patrón con un valor determinado.

¿Pensamientos?

Aquí hay una forma rápida y corta de hacerlo.

 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 realidad, Ruby tiene una función de edición in situ. Al igual que Perl, puedes decir

 ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt 

Esto aplicará el código entre comillas dobles a todos los archivos en el directorio actual cuyos nombres terminan con “.txt”. Las copias de seguridad de los archivos editados se crearán con una extensión “.bak” (“foobar.txt.bak”, creo).

NOTA: esto no parece funcionar para búsquedas de líneas múltiples. Para ellos, tienes que hacerlo de la otra manera menos bonita, con un script envoltorio alrededor de la expresión regular.

Tenga en cuenta que, cuando hace esto, el sistema de archivos puede quedar sin espacio y puede crear un archivo de longitud cero. Esto es catastrófico si está haciendo algo como escribir archivos / etc / passwd como parte de la administración de la configuración del sistema.

[EDITAR: tenga en cuenta que la edición in situ de archivos como en la respuesta aceptada siempre truncará el archivo y escribirá el nuevo archivo secuencialmente. Siempre habrá una condición de carrera en la que los lectores concurrentes verán un archivo truncado o parcialmente truncado, que puede ser catastrófico. Por esa razón, creo que la respuesta aceptada probablemente no sea la respuesta aceptada. ]

Necesita usar un algoritmo que:

  1. lee el archivo antiguo y escribe en el nuevo archivo. (Debe tener cuidado con sorber todos los archivos en la memoria).

  2. cierra explícitamente el nuevo archivo temporal, que es donde puede lanzar una excepción porque los almacenamientos intermedios de archivos no se pueden escribir en el disco porque no hay espacio. (Capture esto y limpie el archivo temporal si lo desea, pero necesita volver a lanzar algo o fracasar bastante duro en este punto.

  3. arregla los permisos y modos del archivo nuevo.

  4. cambia el nombre del nuevo archivo y lo coloca en su lugar.

Con los sistemas de archivos ext3, se garantiza que los metadatos escritos para mover el archivo a su lugar no serán reorganizados por el sistema de archivos y escritos antes de que se escriban los almacenamientos intermedios de datos para el nuevo archivo, por lo que esto debería tener éxito o fallar. El sistema de archivos ext4 también ha sido parchado para admitir este tipo de comportamiento. Si está muy paranoico, debe llamar a la llamada al sistema fdatasync() como paso 3.5 antes de mover el archivo a su lugar.

Independientemente del idioma, esta es la mejor práctica. En los idiomas en los que llamar a close() no arroja una excepción (Perl o C), debe comprobar explícitamente el retorno de close() y lanzar una excepción si falla.

La sugerencia anterior de simplemente sorber el archivo en la memoria, manipularlo y escribirlo en el archivo garantiza la producción de archivos de longitud cero en un sistema de archivos completo. Siempre debe usar FileUtils.mv para mover un archivo temporal completamente escrito en su lugar.

Una consideración final es la ubicación del archivo temporal. Si abre un archivo en / tmp, entonces debe considerar algunos problemas:

  • Si / tmp está montado en un sistema de archivos diferente, puede ejecutar / tmp sin espacio antes de haber escrito el archivo que, de lo contrario, podría implementarse en el destino del archivo anterior.
  • Probablemente, lo más importante es que cuando intentes mv el archivo a través de una montura de dispositivo, se convertirá de forma transparente en comportamiento cp . Se abrirá el archivo anterior, se conservará y se volverá a abrir el archivo antiguo de archivos y se copiará el contenido del archivo. Es muy probable que esto no sea lo que quiere, y puede encontrarse con errores de “archivo de texto ocupado” si intenta editar el contenido de un archivo en ejecución. Esto también anula el propósito de usar los comandos mv del sistema de archivos y puede ejecutar el sistema de archivos de destino sin espacio con solo un archivo parcialmente escrito.

    Esto tampoco tiene nada que ver con la implementación de Ruby. Los comandos del sistema mv y cp comportan de manera similar.

Lo que es más preferible es abrir un Tempfile en el mismo directorio que el archivo anterior. Esto asegura que no habrá problemas de movimiento entre dispositivos. El mv nunca debe fallar, y siempre debe obtener un archivo completo y no truncado. Cualquier error, como el dispositivo sin espacio, los errores de permiso, etc., se debe encontrar durante la escritura del archivo temporal.

Las únicas desventajas del enfoque de crear el archivo Tempfile en el directorio de destino son:

  • a veces no podrá abrir un Tempfile allí, como por ejemplo si está tratando de “editar” un archivo en / proc. Por esa razón, es posible que desee retroceder y probar / tmp si falla la apertura del archivo en el directorio de destino.
  • debe tener suficiente espacio en la partición de destino para mantener tanto el archivo antiguo completo como el nuevo. Sin embargo, si no tiene suficiente espacio para almacenar ambas copias, probablemente tenga poco espacio en disco y el riesgo real de escribir un archivo truncado sea mucho mayor, por lo que yo diría que es una solución muy pobre fuera de una estrecha (y -monitorizado) casos de borde.

Aquí hay un código que implementa el algoritmo completo (el código de Windows no se ha probado y no está terminado):

 #!/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") 

Y aquí hay una versión un poco más ajustada que no se preocupa por todos los casos extremos posibles (si está en Unix y no le importa escribir en / 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") 

El caso de uso realmente simple, cuando no te importan los permisos del sistema de archivos (no estás ejecutando como root, o estás corriendo como root y el archivo es propiedad de 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: Debería utilizarse en lugar de la respuesta aceptada como mínimo, en todos los casos, para garantizar que la actualización sea atómica y los lectores concurrentes no verán los archivos truncados. Como mencioné anteriormente, la creación del archivo temporal en el mismo directorio que el archivo editado es importante aquí para evitar que las operaciones mv cross-device se traduzcan en operaciones cp si / tmp está montado en un dispositivo diferente. Llamar a fdatasync es una capa adicional de paranoia, pero tendrá un impacto en el rendimiento, por lo que lo omití en este ejemplo, ya que no se practica comúnmente.

Realmente no hay una forma de editar archivos en el lugar. Lo que suele hacer cuando puede salirse con la suya (es decir, si los archivos no son demasiado grandes) es, lee el archivo en la memoria ( File.read ), realiza las sustituciones en la cadena de lectura ( String#gsub ) y luego escribe la cadena modificada vuelve al archivo ( File.open , File#write ).

Si los archivos son lo suficientemente grandes como para que no sean factibles, lo que debe hacer es leer el archivo en fragmentos (si el patrón que desea reemplazar no abarcará varias líneas, entonces un fragmento generalmente significa una línea; puede usar el File.foreach para leer un archivo línea por línea), y para cada fragmento realice la sustitución en él y añádalo a un archivo temporal. Cuando termine de iterar sobre el archivo de origen, ciérrelo y use FileUtils.mv para sobrescribirlo con el archivo temporal.

Otro enfoque es usar la edición in situ dentro de Ruby (no desde la línea de comandos):

 #!/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 no desea crear una copia de seguridad, cambie ‘.bak’ a ”.

Aquí hay una solución para buscar / reemplazar en todos los archivos de un directorio determinado. Básicamente tomé la respuesta provista por sepp2k y la expandí.

 # 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 

Esto funciona para mí:

 filename = "foo" text = File.read(filename) content = text.gsub(/search_regexp/, "replacestring") File.open(filename, "w") { |file| file << content } 
 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) } 

Aquí una alternativa al trazador de líneas uno de Jim, esta vez en un guión

 ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))} 

Guárdelo en un script, p. Ej. Replace.rb

Comienzas en la línea de comando con

 replace.rb *.txt   

* .txt se puede reemplazar con otra selección o con algunos nombres de archivo o rutas

desglosado para que pueda explicar lo que está pasando, pero aún ejecutable

 # 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 

Si necesita hacer sustituciones a través de los límites de línea, entonces usar ruby -pi -e no funcionará porque la p procesa una línea a la vez. En cambio, recomiendo lo siguiente, aunque podría fallar con un archivo de varios GB:

 ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))" 

El está buscando un espacio en blanco (potencialmente incluyendo nuevas líneas) seguido por una comilla, en cuyo caso se deshace de los espacios en blanco. El %q(') es solo una forma elegante de citar el carácter de cita.