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?
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?
Yanıtlar:
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
File.write(file_name, text.gsub(/regexp/, "replace")
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.
<main>': undefined method
gsub 'for main: Object (NoMethodError)
-i
düzenlemeleri yerinde. .bak
yedekleme dosyası için kullanılan uzantıdır (isteğe bağlı). -p
gibi 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!'
.)
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:
Eski dosyayı okur ve yeni dosyaya yazar. (Tüm dosyaları hafızaya alma konusunda dikkatli olmanız gerekir).
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.
Yeni dosyadaki dosya izinlerini ve modlarını düzeltir.
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, mv
dosyayı bir aygıt bağlantısında denediğinizde şeffaf bir şekilde cp
davranış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 mv
komutları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 mv
ve cp
komutlar 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. mv
Kendisi 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:
İş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.
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.foreach
iç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.mv
geçici dosyanın üzerine yazmak için kullanırsınız .
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 ''
.
read
, dosyayı bulandırmaya çalışmaktan ( ) daha iyi olur . Ölçeklenebilir ve çok hızlı olmalıdır.
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 }
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
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) }
Hat sınırları ötesinde değişiklik yapmanız gerekiyorsa, kullanmak ruby -pi -e
işe yaramaz çünkü p
her 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.
İş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]))}
File.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)