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
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
Respuestas:
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
File.write(file_name, text.gsub(/regexp/, "replace")
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.
<main>': undefined method
gsub 'para main: Object (NoMethodError)
-i
ediciones en su lugar. .bak
es la extensión utilizada para un archivo de respaldo (opcional). -p
es 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!'
).
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:
Lee el archivo antiguo y escribe en el nuevo archivo. (Debe tener cuidado al absorber archivos completos en la memoria).
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.
Corrige los permisos y modos de archivo en el nuevo archivo.
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.mv
para 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 mv
el archivo a través de un montaje de dispositivo, se convertirá de manera transparente en cp
comportamiento. 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 mv
comandos 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 mv
y los cp
comandos 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 mv
mismo 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:
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.
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.foreach
para 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.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 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 ''
.
read
) el archivo. Es escalable y debería ser muy rápido.
Esto funciona para mi:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
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
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 necesita hacer sustituciones a través de los límites de las líneas, entonces el uso ruby -pi -e
no funcionará porque los p
procesos 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.
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]))}
File.read
debe moderarse con la información en stackoverflow.com/a/25189286/128421 sobre por qué es malo sorber archivos grandes. Además, en lugar deFile.open(filename, "w") { |file| file << content }
utilizar variacionesFile.write(filename, content)
.