Cómo buscar un patrón en el texto del archivo y reemplazarlo con un valor dado

117

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

Pensamientos

Dane O'Connor
fuente
1
En las respuestas a continuación, tenga en cuenta que cualquier recomendación de uso File.readdebe moderarse con la información en stackoverflow.com/a/25189286/128421 sobre por qué es malo sorber archivos grandes. Además, en lugar de File.open(filename, "w") { |file| file << content }utilizar variaciones File.write(filename, content).
The Tin Man

Respuestas:

190

Descargo de responsabilidad: este enfoque es una ilustración ingenua de las capacidades de Ruby y no una solución de producción para reemplazar cadenas en archivos. Es propenso a varios escenarios de falla, como la pérdida de datos en caso de una falla, una interrupción o un disco lleno. Este código no es apto para nada más que un script rápido y único en el que se realiza una copia de seguridad de todos los datos. Por esa razón, NO copie este código en sus programas.

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
Max Chernyak
fuente
¿La opción put escribe el cambio de nuevo en el archivo? Pensé que solo imprimiría el contenido en la consola.
Dane O'Connor
Sí, imprime el contenido en la consola.
sepp2k
7
Sí, no estaba seguro de que eso fuera lo que querías. Para escribir use File.open (file_name, "w") {| file | file.puts output_of_gsub}
Max Chernyak
7
Tuve que usar file.write: File.open (file_name, "w") {| file | file.write (text)}
austen
3
Para escribir un archivo, reemplace la línea File.write(file_name, text.gsub(/regexp/, "replace")
apretado
106

De hecho, Ruby tiene una función de edición in situ. Como Perl, puedes decir

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

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

NOTA: esto no parece funcionar para búsquedas de varias líneas. Para esos, debe hacerlo de la otra manera menos bonita, con un script de envoltura alrededor de la expresión regular.

Jim Kane
fuente
1
¿Qué diablos es pi.bak? Sin eso, obtengo un error. -e: 1: en <main>': undefined method gsub 'para main: Object (NoMethodError)
Ninad
15
@NinadPachpute -iediciones en su lugar. .bakes la extensión utilizada para un archivo de respaldo (opcional). -pes algo así como while gets; <script>; puts $_; end. ( $_es la última línea leída, pero puede asignarle algo como echo aa | ruby -p -e '$_.upcase!').
Lri
1
Esta es una mejor respuesta que la respuesta aceptada, en mi humilde opinión, si está buscando modificar el archivo.
Colin K
6
¿Cómo puedo usar esto dentro de un script ruby?
Saurabh
1
Hay muchas formas en que esto puede salir mal, así que pruébelo a fondo antes de intentarlo con un archivo crítico.
The Tin Man
49

Tenga en cuenta que, cuando haga esto, el sistema de archivos podría quedarse 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.

Tenga en cuenta que la edición de archivos en el lugar 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 simultáneos verán un archivo truncado. Si el proceso se aborta por cualquier motivo (ctrl-c, asesino de OOM, caída del sistema, corte de energía, etc.) durante la escritura, el archivo truncado también quedará, lo que puede ser catastrófico. Este es el tipo de escenario de pérdida de datos que los desarrolladores DEBEN considerar porque sucederá. Por esa razón, creo que la respuesta aceptada probablemente no debería ser la respuesta aceptada. Como mínimo, escriba en un archivo temporal y mueva / cambie el nombre del archivo en su lugar como la solución "simple" al final de esta respuesta.

Necesita utilizar un algoritmo que:

  1. Lee el archivo antiguo y escribe en el nuevo archivo. (Debe tener cuidado al absorber archivos completos en la memoria).

  2. Cierra explícitamente el nuevo archivo temporal, que es donde puede lanzar una excepción porque los búferes 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 fallar bastante en este punto.

  3. Corrige los permisos y modos de archivo en el nuevo archivo.

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

Con los sistemas de archivos ext3, tiene la garantía de que la escritura de metadatos para mover el archivo a su lugar no será reorganizada por el sistema de archivos y escrita antes de que se escriban los búferes 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 parcheado para admitir este tipo de comportamiento. Si está muy paranoico, debe llamar a la llamada al fdatasync()sistema como paso 3.5 antes de mover el archivo a su lugar.

Independientemente del idioma, esta es la mejor práctica. En los lenguajes donde la llamada close()no arroja una excepción (Perl o C), debe verificar explícitamente el retorno de close()y lanzar una excepción si falla.

La sugerencia anterior de simplemente absorber el archivo en la memoria, manipularlo y escribirlo en el archivo garantizará la producción de archivos de longitud cero en un sistema de archivos completo. Es necesario que siempre se utilice FileUtils.mvpara mover un archivo temporal totalmente escrito en su lugar.

Una consideración final es la ubicación del archivo temporal. Si abre un archivo en / tmp, 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, se podría implementar en el destino del archivo anterior.

  • Probablemente, lo que es más importante, cuando intente mvel archivo a través de un montaje de dispositivo, se convertirá de manera transparente en cpcomportamiento. El archivo antiguo se abrirá, el inodo de archivos antiguos se conservará y se volverá a abrir y se copiará el contenido del archivo. Lo más probable es que esto no sea lo que desea 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 mvcomandos del sistema de archivos y puede ejecutar el sistema de archivos de destino sin espacio con solo un archivo escrito parcialmente.

    Esto tampoco tiene nada que ver con la implementación de Ruby. El sistema mvy los cpcomandos se 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 mvmismo nunca debería fallar, y siempre debería obtener un archivo completo y sin truncar. Cualquier falla, como dispositivo sin espacio, errores de permisos, etc., debe encontrarse durante la escritura del Tempfile.

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

  • A veces, es posible que no pueda abrir un archivo temporal allí, por ejemplo, si está intentando '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 contener tanto el archivo antiguo completo como el nuevo. Sin embargo, si no tiene suficiente espacio para guardar ambas copias, entonces probablemente tenga poco espacio en el disco y el riesgo real de escribir un archivo truncado es mucho mayor, por lo que yo diría que esta es una compensación muy pobre fuera de algunas extremadamente estrechas (y bueno -monitorizados) casos extremos.

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

#!/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 cada caso de borde posible (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, para cuando no le importan los permisos del sistema de archivos (o no se está ejecutando como root o se está ejecutando 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 : Debe usarse en lugar de la respuesta aceptada como mínimo, en todos los casos, para garantizar que la actualización sea atómica y que los lectores simultáneos no vean archivos truncados. Como mencioné anteriormente, crear el Tempfile en el mismo directorio que el archivo editado es importante aquí para evitar que las operaciones mv entre dispositivos se traduzcan en operaciones cp si / tmp está montado en un dispositivo diferente. Llamar a fdatasync es una capa adicional de paranoia, pero generará un impacto en el rendimiento, por lo que lo omití de este ejemplo ya que no se practica comúnmente.

Lamont
fuente
En lugar de abrir un archivo temporal en el directorio en el que se encuentra, se creará automáticamente uno en el directorio de datos de la aplicación (de todos modos en Windows) y desde allí puede hacer un archivo.unlink para eliminarlo ..
13aal
3
Realmente aprecié el pensamiento adicional que se puso en esto. Como principiante, es muy interesante ver los patrones de pensamiento de desarrolladores experimentados que no solo pueden responder la pregunta original, sino también comentar sobre el contexto más amplio de lo que realmente significa la pregunta original.
ramijames
La programación no se trata solo de solucionar el problema inmediato, también se trata de pensar con anticipación para evitar otros problemas al acecho. Nada irrita más a un desarrollador senior que encontrar un código que colocó el algoritmo en una esquina, forzando un torpeza incómodo, cuando un pequeño ajuste anterior hubiera resultado en un buen flujo. A menudo, puede llevar horas o días analizar para comprender el objetivo, y luego unas pocas líneas reemplazan una página de código antiguo. Es como una partida de ajedrez contra los datos y el sistema a veces.
The Tin Man
11

Realmente no hay una forma de editar archivos in situ. Lo que suele hacer cuando puede salirse con la suya (es decir, si los archivos no son demasiado grandes) es leer el archivo en la memoria ( File.read), realizar sus sustituciones en la cadena de lectura ( String#gsub) y luego escribir la cadena cambiada de nuevo en la archivo ( File.open, File#write).

Si los archivos son lo suficientemente grandes como para que eso sea inviable, lo que debe hacer es leer el archivo en fragmentos (si el patrón que desea reemplazar no abarca varias líneas, entonces un fragmento generalmente significa una línea; puede usar File.foreachpara leer un archivo línea por línea), y para cada fragmento, realice la sustitución y añádalo a un archivo temporal. Cuando haya terminado de iterar sobre el archivo fuente, ciérrelo y utilice FileUtils.mvpara sobrescribirlo con el archivo temporal.

sepp2k
fuente
1
Me gusta el enfoque de transmisión. Trabajamos con archivos grandes al mismo tiempo, por lo que generalmente no tenemos el espacio en la RAM para leer el archivo completo
Shane
" ¿Por qué" sorber "un archivo no es una buena práctica? " Podría ser una lectura útil en relación con esto.
The Tin Man
9

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

#!/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 ''.

DavidG
fuente
1
Esto sería mejor que intentar sorber ( read) el archivo. Es escalable y debería ser muy rápido.
The Tin Man
Hay un error en alguna parte que hace que Ruby 2.3.0p0 en Windows falle con el permiso denegado si hay varios bloques inplace_edit consecutivos trabajando en el mismo archivo. Para reproducir pruebas divididas search1 y search2 en 2 bloques. ¿No se cierra por completo?
mlt
Esperaría que se produjeran problemas con varias ediciones de un archivo de texto simultáneamente. Si nada más, podría obtener un archivo de texto muy destrozado.
The Tin Man
7

Esto funciona para mi:

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

Aquí hay una solución para buscar / reemplazar en todos los archivos de un directorio determinado. Básicamente tomé la respuesta proporcionada 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
curtidor
fuente
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) }
Ninad
fuente
2
Será más útil si proporciona una explicación de por qué esta es la solución preferida y explica cómo funciona. Queremos educar, no solo proporcionar código.
The Tin Man
trollop pasó a llamarse optimist github.com/manageiq/optimist . Además, es solo un analizador de opciones CLI que no se requiere realmente para responder la pregunta.
Noraj
1

Si necesita hacer sustituciones a través de los límites de las líneas, entonces el uso ruby -pi -eno funcionará porque los pprocesos de una línea a la vez. En su lugar, 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 de una cita, en cuyo caso se deshace del espacio en blanco. El %q(')es sólo una forma elegante de citar el carácter de comillas.

Dan Kohn
fuente
1

Aquí una alternativa a la única línea 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, por ejemplo, replace.rb

Empiezas en la línea de comando con

replace.rb *.txt <string_to_replace> <replacement>

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

desglosado para poder explicar lo que está sucediendo 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

EDITAR: si desea usar una expresión regular, use esto en su lugar.Obviamente, esto es solo para manejar archivos de texto relativamente pequeños, no monstruos Gigabyte

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
pedro
fuente
Este código no funcionará. Sugeriría probarlo antes de publicarlo, luego copiar y pegar el código de trabajo.
The Tin Man
@theTinMan Siempre pruebo antes de publicar, si es posible. Probé esto y funciona, tanto la versión corta como comentada. ¿Por qué crees que no lo haría?
peter
si te refieres al uso de una expresión regular, mira mi edición, también probado:>)
peter