Bir kalıp için dosya metni nasıl aranır ve belirli bir değerle nasıl değiştirilir


117

Bir kalıp için bir dosyayı (veya dosya listesini) aramak ve bulunursa bu kalıbı belirli bir değerle değiştirmek için bir komut dosyası arıyorum.

Düşünceler?


1
Aşağıdaki yanıtlarda, büyük dosyaları höpürdetmenin neden kötü olduğu konusunda, kullanılacak tüm önerilerin stackoverflow.com/a/25189286/128421File.read adresindeki bilgilerle düzeltilmesi gerektiğini unutmayın . Ayrıca varyasyonlar yerine kullanın . File.open(filename, "w") { |file| file << content }File.write(filename, content)
Teneke Adam

Yanıtlar:


190

Sorumluluk Reddi: Bu yaklaşım, Ruby'nin yeteneklerinin naif bir örneğidir ve dosyalardaki dizeleri değiştirmek için üretim düzeyinde bir çözüm değildir. Bir çökme, kesinti veya diskin dolması durumunda veri kaybı gibi çeşitli arıza senaryolarına eğilimlidir. Bu kod, tüm verilerin yedeklendiği tek seferlik hızlı bir komut dosyası dışında hiçbir şey için uygun değildir. Bu nedenle, bu kodu programlarınıza kopyalamayın.

İşte bunu yapmanın hızlı ve kısa bir yolu.

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

Değişikliği dosyaya geri yazıyor mu? Bunun sadece içeriği konsola yazdıracağını düşündüm.
Dane O'Connor

Evet, içeriği konsola yazdırır.
sepp2k

7
Evet, istediğinin bu olduğundan emin değildim. Yazmak için File.open (dosya_adı, "w") {| dosya | file.puts output_of_gsub}
Max Chernyak

7
File.write kullanmam gerekiyordu: File.open (dosya_adı, "w") {| dosya | file.write (metin)}
Austen

3
Dosya yazmak için, puts 'satırınıFile.write(file_name, text.gsub(/regexp/, "replace")
sıkı

106

Aslında, Ruby'nin yerinde düzenleme özelliği vardır. Perl gibi diyebilirsin

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

Bu, kodu çift tırnak içinde, geçerli dizindeki adları ".txt" ile biten tüm dosyalara uygulayacaktır. Düzenlenen dosyaların yedek kopyaları bir ".bak" uzantısıyla ("foobar.txt.bak" sanırım) oluşturulacaktır.

NOT: bu, çok satırlı aramalarda işe yaramıyor gibi görünmektedir. Bunlar için, normal ifadenin etrafına bir sarmalayıcı komut dosyası koyarak daha az hoş bir şekilde yapmanız gerekir.


1
Pi.bak da neyin nesi? O olmadan bir hata alıyorum. -e: 1: in <main>': undefined method gsub 'for main: Object (NoMethodError)
Ninad

15
@NinadPachpute -idüzenlemeleri yerinde. .bakyedekleme dosyası için kullanılan uzantıdır (isteğe bağlı). -pgibi bir şey while gets; <script>; puts $_; end. ( $_son okunan satırdır, ancak buna benzer bir şey için atayabilirsiniz echo aa | ruby -p -e '$_.upcase!'.)
Lri

1
Dosyayı değiştirmek istiyorsanız bu, kabul edilen cevaptan daha iyi bir cevaptır, IMHO.
Colin K

6
Bunu Ruby komut dosyası içinde nasıl kullanabilirim?
Saurabh

1
Bunun yanlış gitmesinin pek çok yolu vardır, bu nedenle kritik bir dosyaya karşı denemeden önce iyice test edin.
The Tin Man

49

Bunu yaptığınızda dosya sisteminin boş olabileceğini ve sıfır uzunlukta bir dosya oluşturabileceğinizi unutmayın. Sistem yapılandırma yönetiminin bir parçası olarak / etc / passwd dosyalarını yazmak gibi bir şey yapıyorsanız, bu felakettir.

Kabul edilen yanıtta olduğu gibi yerinde dosya düzenlemenin her zaman dosyayı keseceğini ve yeni dosyayı sırayla yazacağını unutmayın. Eşzamanlı okuyucuların kesilmiş bir dosya göreceği bir yarış koşulu her zaman olacaktır. İşlem, yazma sırasında herhangi bir nedenle (ctrl-c, OOM katili, sistem çökmesi, elektrik kesintisi, vb.) İptal edilirse, kesilen dosya da geride kalacak ve bu da felaket olabilir. Bu, geliştiricilerin dikkate alması GEREKEN bir tür veri kaybı senaryosudur çünkü gerçekleşecektir. Bu nedenle, kabul edilen cevabın büyük olasılıkla kabul edilen cevap olmaması gerektiğini düşünüyorum. En azından bir geçici dosyaya yazın ve bu cevabın sonundaki "basit" çözüm gibi dosyayı yerine taşıyın / yeniden adlandırın.

Şunları yapan bir algoritma kullanmanız gerekir:

  1. Eski dosyayı okur ve yeni dosyaya yazar. (Tüm dosyaları hafızaya alma konusunda dikkatli olmanız gerekir).

  2. Açıkça yeni geçici dosyayı kapatır, burada bir istisna atabilirsiniz çünkü yer olmadığı için dosya arabellekleri diske yazılamaz. (Bunu yakalayın ve isterseniz geçici dosyayı temizleyin, ancak bu noktada bir şeyi yeniden atmanız veya oldukça zorlanmanız gerekir.

  3. Yeni dosyadaki dosya izinlerini ve modlarını düzeltir.

  4. Yeni dosyayı yeniden adlandırır ve yerine bırakır.

Ext3 dosya sistemleri ile, dosyayı yerine taşımak için yazılan metadata'nın dosya sistemi tarafından yeniden düzenlenmeyeceği ve yeni dosya için veri arabellekleri yazılmadan önce yazılmayacağı garanti edilir, bu yüzden bu başarılı olmalı veya başarısız olmalıdır. Ext4 dosya sistemine de bu tür davranışları desteklemek için yama uygulanmıştır. Çok paranoyak iseniz fdatasync(), dosyayı yerine taşımadan önce 3.5 adımı olarak sistem çağrısını aramalısınız.

Dil ne olursa olsun, bu en iyi uygulamadır. close()Çağrının bir istisna oluşturmadığı dillerde (Perl veya C), dönüşünü açıkça kontrol etmeli close()ve başarısız olursa bir istisna atmalısınız.

Yukarıdaki öneri, dosyayı hafızaya almak, değiştirmek ve dosyaya yazmak için tam bir dosya sistemi üzerinde sıfır uzunlukta dosyalar üretmeyi garanti edecektir. Tam olarak yazılmış bir geçici dosyayı yerine taşımak için her zaman kullanmanız gerekir FileUtils.mv.

Son bir değerlendirme, geçici dosyanın yerleştirilmesidir. / Tmp dosyasında bir dosya açarsanız, birkaç sorunu göz önünde bulundurmanız gerekir:

  • / Tmp farklı bir dosya sistemine bağlanmışsa, aksi takdirde eski dosyanın hedefine konuşlandırılabilecek dosyayı yazmadan önce / tmp alanını boş alan çalıştırabilirsiniz.

  • Muhtemelen daha önemlisi, mvdosyayı bir aygıt bağlantısında denediğinizde şeffaf bir şekilde cpdavranışa dönüşeceksiniz . Eski dosya açılacak, eski dosyalar korunacak ve yeniden açılacak ve dosya içeriği kopyalanacaktır. Bu, büyük olasılıkla istediğiniz şey değildir ve çalışan bir dosyanın içeriğini düzenlemeye çalışırsanız "metin dosyası meşgul" hatalarıyla karşılaşabilirsiniz. Bu aynı zamanda dosya sistemi mvkomutlarını kullanma amacını da ortadan kaldırır ve hedef dosya sistemini yalnızca kısmen yazılmış bir dosya ile alan dışında çalıştırabilirsiniz.

    Bunun Ruby'nin uygulamasıyla da ilgisi yoktur. Sistem mvve cpkomutlar benzer şekilde davranır.

Daha çok tercih edilen, eski dosyayla aynı dizinde bir Tempfile açmaktır. Bu, cihazlar arası taşıma sorunu olmamasını sağlar. mvKendisi başarısız asla ve her zaman tam ve untruncated dosyasını almalısınız. Cihazın boş alanı, izin hataları, vb. Gibi herhangi bir arıza, Tempfile dışarı yazılırken karşılaşılmalıdır.

Hedef dizinde Tempfile oluşturma yaklaşımının tek dezavantajı şunlardır:

  • Bazen, örneğin / proc içindeki bir dosyayı 'düzenlemeye' çalışıyorsanız, orada bir Tempfile açamayabilirsiniz. Bu nedenle, dosyayı hedef dizinde açmak başarısız olursa geri dönüp / tmp'yi denemek isteyebilirsiniz.
  • Hem eski dosyanın tamamını hem de yeni dosyayı tutabilmek için hedef bölümde yeterli alana sahip olmanız gerekir. Bununla birlikte, her iki kopyayı da tutmak için yeterli alanınız yoksa, muhtemelen disk alanınız kısadır ve kesilmiş bir dosya yazmanın gerçek riski çok daha yüksektir, bu yüzden bunun aşırı derecede dar (ve pekala -izlenen) uç durumlar.

İşte tam algoritmayı uygulayan bazı kodlar (Windows kodu test edilmemiştir ve bitmemiştir):

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

Ve işte her olası uç durum için endişelenmeyen biraz daha sıkı bir sürüm (Unix'teyseniz ve / proc'a yazmayı umursamıyorsanız):

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

Dosya sistemi izinlerini önemsemediğiniz zamanlar için gerçekten basit kullanım durumu (ya kök olarak çalışmıyorsunuz ya da kök olarak çalışıyorsunuz ve dosya kök sahibi):

#!/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 : Güncellemenin atomik olduğundan ve eşzamanlı okuyucuların kesilmiş dosyaları görmemesini sağlamak için her durumda, en azından kabul edilen yanıt yerine kullanılmalıdır. Yukarıda bahsettiğim gibi, / tmp farklı bir aygıta takılıysa, aygıtlar arası mv işlemlerinin cp işlemlerine dönüştürülmesini önlemek için burada, düzenlenen dosyayla aynı dizinde Tempfile oluşturmak önemlidir. Fdatasync'i çağırmak ek bir paranoya katmanıdır, ancak bir performans düşüşüne neden olacaktır, bu yüzden yaygın olarak uygulanmadığı için bu örnekten çıkarmıştım.


İçinde bulunduğunuz dizinde geçici bir dosya açmak yerine, aslında otomatik olarak bir uygulama veri dizininde (yine de Windows'ta) bir tane oluşturur ve onlardan bir file.unlink yaparak onu silebilirsiniz ..
13

3
Bu konudaki ekstra düşünceyi gerçekten takdir ettim. Yeni başlayan biri olarak, sadece orijinal soruyu yanıtlayamayan deneyimli geliştiricilerin düşünce kalıplarını görmek çok ilginç, aynı zamanda orijinal sorunun gerçekte ne anlama geldiğine dair daha geniş bağlamda yorum yapıyor.
ramijames

Programlama sadece acil sorunu çözmekle ilgili değildir, aynı zamanda bekleyen diğer sorunları önlemek için çok ileriyi düşünmekle de ilgilidir. Hiçbir şey kıdemli bir geliştiriciyi, daha önce küçük bir ayarlamanın güzel bir akışla sonuçlanacağı zaman, algoritmayı köşeye sıkıştıran, garip bir kludge zorlayan bir kodla karşılaşmaktan daha fazla rahatsız edemez. Hedefi anlamak genellikle saatler veya günler alabilir ve ardından eski bir kod sayfasının yerini birkaç satır alır. Zaman zaman verilere ve sisteme karşı oynanan bir satranç oyunu gibidir.
Teneke Adam

11

Dosyaları yerinde düzenlemenin gerçekten bir yolu yoktur. Bundan kurtulabileceğiniz zaman genellikle yaptığınız şey (yani dosyalar çok büyük değilse), dosyayı memory ( File.read) 'ye okur String#gsub, değiştirmelerinizi okuma dizesi ( ) üzerinde gerçekleştirir ve sonra değiştirilen dizeyi tekrar dosya ( File.open, File#write).

Kullanabileceğiniz - dosyaları yapmanız gereken şey bu olanaksız olması için yeterince büyük, iseniz, desen birden satırdan sonra bir yığın genellikle bir satır anlamına olmaz değiştirmek isterseniz (parçalar dosyayı okumak olduğunu File.foreachiçin bir dosyayı satır satır okuyun) ve her yığın için ikame işlemini gerçekleştirin ve geçici bir dosyaya ekleyin. Kaynak dosya üzerinde yinelemeyi bitirdiğinizde, onu kapatır ve FileUtils.mvgeçici dosyanın üzerine yazmak için kullanırsınız .


1
Akış yaklaşımını beğendim. Eşzamanlı olarak büyük dosyalarla çalışıyoruz, bu nedenle RAM'de dosyanın tamamını okuyacak
Shane

" Neden bir dosyayı" höpürdetmek "iyi bir uygulama değil? " Bununla ilgili okumak yararlı olabilir.
The Tin Man

9

Diğer bir yaklaşım Ruby içinde yerinde düzenlemeyi kullanmaktır (komut satırından değil):

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

Eğer bir yedek oluşturmak için istemiyorsanız o zaman değiştirmek '.bak'için ''.


1
Bu read, dosyayı bulandırmaya çalışmaktan ( ) daha iyi olur . Ölçeklenebilir ve çok hızlı olmalıdır.
The Tin Man

Bir yerde Ruby 2.3.0p0'ın, aynı dosya üzerinde çalışan birkaç ardışık inplace_edit bloğu varsa izin reddedilerek başarısız olmasına neden olan bir hata var. Bölünmüş arama1 ve arama2 testlerini 2 blok halinde yeniden üretmek için. Tamamen kapanmıyor mu?
mlt

Aynı anda gerçekleşen bir metin dosyasının birden çok düzenlemesiyle ilgili sorunlar beklerdim. Başka hiçbir şey yoksa, kötü bir şekilde karıştırılmış bir metin dosyası elde edebilirsiniz.
Teneke Adam

7

Bu benim için çalışıyor:

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

6

Belirli bir dizinin tüm dosyalarında bul / değiştir için bir çözüm burada. Temel olarak sepp2k tarafından sağlanan cevabı aldım ve genişlettim.

# 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
Bunun neden tercih edilen çözüm olduğunu ve nasıl çalıştığını açıklarsanız daha fazla yardımcı olur. Sadece kod sağlamak değil, eğitmek istiyoruz.
The Tin Man

trollop , optimist github.com/manageiq/optimist olarak yeniden adlandırıldı . Ayrıca soruyu yanıtlamak için gerçekten gerekli olmayan bir CLI seçenek ayrıştırıcısıdır.
noraj

1

Hat sınırları ötesinde değişiklik yapmanız gerekiyorsa, kullanmak ruby -pi -eişe yaramaz çünkü pher seferinde bir satır işler. Bunun yerine, aşağıdakileri öneririm, ancak çok GB'lık bir dosyada başarısız olabilir:

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

, Bir alıntıdan sonra beyaz boşluk (potansiyel olarak yeni satırlar dahil) arıyor, bu durumda boşluktan kurtulmuş oluyor. Bu %q('), alıntı karakterinden alıntı yapmanın süslü bir yoludur.


1

İşte Jim'in bir satırına bir alternatif, bu sefer bir senaryoda

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

Bir komut dosyasında kaydedin, örneğin, replace.rb

Şununla komut satırından başlıyorsunuz:

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

* .txt başka bir seçimle veya bazı dosya adları veya yollarla değiştirilebilir

ne olduğunu açıklayabilmem için ancak yine de yürütülebilir

# 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

DÜZENLEME: Normal bir ifade kullanmak istiyorsanız, bunun yerine bunu kullanın Açıkçası, bu yalnızca nispeten küçük metin dosyalarını kullanmak içindir, Gigabyte canavarları yok

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

Bu kod çalışmayacak. Göndermeden önce test etmenizi ve ardından çalışma kodunu kopyalayıp yapıştırmanızı öneririm.
The Tin Man

@theTinMan Mümkünse her zaman yayınlamadan önce test ederim. Bunu test ettim ve çalışıyor, hem kısa hem de yorumlu versiyon. Neden olmayacağını düşünüyorsun?
peter

Normal bir ifade kullanmayı kastediyorsanız, düzenlememe bakın, ayrıca test edilmiştir:>)
peter
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.