Saya mencari skrip untuk mencari sebuah file (atau daftar file) untuk sebuah pola dan, jika ditemukan, ganti pola itu dengan nilai yang diberikan.
Pikiran?
Saya mencari skrip untuk mencari sebuah file (atau daftar file) untuk sebuah pola dan, jika ditemukan, ganti pola itu dengan nilai yang diberikan.
Pikiran?
Jawaban:
Penafian: Pendekatan ini adalah ilustrasi naif dari kemampuan Ruby, dan bukan solusi tingkat produksi untuk mengganti string dalam file. Ini rentan terhadap berbagai skenario kegagalan, seperti kehilangan data jika terjadi crash, interupsi, atau disk penuh. Kode ini tidak cocok untuk apa pun selain skrip cepat satu kali yang semua datanya dicadangkan. Oleh karena itu, JANGAN menyalin kode ini ke dalam program Anda.
Berikut cara singkat dan cepat untuk melakukannya.
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")
Sebenarnya, Ruby memiliki fitur pengeditan di tempat. Seperti Perl, bisa dibilang
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Ini akan menerapkan kode dalam tanda kutip ganda ke semua file di direktori saat ini yang namanya diakhiri dengan ".txt". Salinan cadangan dari file yang diedit akan dibuat dengan ekstensi ".bak" ("foobar.txt.bak" menurut saya).
CATATAN: ini tampaknya tidak berfungsi untuk pencarian multiline. Untuk itu, Anda harus melakukannya dengan cara lain yang kurang cantik, dengan skrip pembungkus di sekitar regex.
<main>': undefined method
gsub 'untuk main: Object (NoMethodError)
-i
mengedit di tempat. .bak
adalah ekstensi yang digunakan untuk file cadangan (opsional). -p
adalah sesuatu seperti while gets; <script>; puts $_; end
. ( $_
adalah baris baca terakhir, tetapi Anda dapat menetapkannya untuk sesuatu seperti echo aa | ruby -p -e '$_.upcase!'
.)
Ingatlah bahwa, ketika Anda melakukan ini, sistem file mungkin kehabisan ruang dan Anda dapat membuat file dengan panjang nol. Ini bencana jika Anda melakukan sesuatu seperti menulis file / etc / passwd sebagai bagian dari manajemen konfigurasi sistem.
Perhatikan bahwa pengeditan file di tempat seperti pada jawaban yang diterima akan selalu memotong file dan menulis file baru secara berurutan. Akan selalu ada kondisi balapan di mana pembaca yang bersamaan akan melihat file yang terpotong. Jika proses dibatalkan karena alasan apa pun (ctrl-c, OOM killer, system crash, power outage, dll) selama penulisan, maka file yang terpotong juga akan tertinggal, yang dapat menjadi bencana besar. Ini adalah jenis skenario dataloss yang HARUS dipertimbangkan oleh pengembang karena itu akan terjadi. Untuk alasan itu, saya pikir jawaban yang diterima kemungkinan besar bukan jawaban yang diterima. Minimal tulis ke tempfile dan pindahkan / ganti nama file ke tempatnya seperti solusi "sederhana" di akhir jawaban ini.
Anda perlu menggunakan algoritme yang:
Membaca file lama dan menulis ke file baru. (Anda harus berhati-hati saat memasukkan seluruh file ke dalam memori).
Menutup file sementara baru secara eksplisit, di mana Anda dapat melontarkan pengecualian karena buffer file tidak dapat ditulis ke disk karena tidak ada ruang. (Tangkap ini dan bersihkan file sementara jika Anda mau, tetapi Anda perlu mengembalikan sesuatu atau gagal cukup keras saat ini.
Memperbaiki izin dan mode file pada file baru.
Ubah nama file baru dan letakkan di tempatnya.
Dengan sistem file ext3 Anda dijamin bahwa penulisan metadata untuk memindahkan file ke tempatnya tidak akan diatur ulang oleh sistem file dan ditulis sebelum buffer data untuk file baru ditulis, jadi ini akan berhasil atau gagal. Sistem file ext4 juga telah ditambal untuk mendukung perilaku semacam ini. Jika Anda sangat paranoid, Anda harus memanggil panggilan fdatasync()
sistem sebagai langkah 3.5 sebelum memindahkan file ke tempatnya.
Terlepas dari bahasanya, ini adalah praktik terbaik. Dalam bahasa di mana pemanggilan close()
tidak memunculkan pengecualian (Perl atau C), Anda harus secara eksplisit memeriksa kembalinya close()
dan melempar pengecualian jika gagal.
Saran di atas untuk hanya menghirup file ke dalam memori, memanipulasinya, dan menulisnya ke file akan dijamin menghasilkan file dengan panjang nol pada sistem file penuh. Anda harus selalu menggunakan FileUtils.mv
untuk memindahkan file sementara yang ditulis lengkap ke tempatnya.
Pertimbangan terakhir adalah penempatan file sementara. Jika Anda membuka file di / tmp maka Anda harus mempertimbangkan beberapa masalah:
Jika / tmp dipasang pada sistem file yang berbeda, Anda dapat menjalankan / tmp kehabisan ruang sebelum Anda menulis file yang seharusnya dapat diterapkan ke tujuan file lama.
Mungkin yang lebih penting, ketika Anda mencoba mv
file di perangkat mount Anda akan secara transparan diubah ke cp
perilaku. File lama akan dibuka, inode file lama akan dipertahankan dan dibuka kembali dan konten file akan disalin. Ini kemungkinan besar bukan yang Anda inginkan, dan Anda mungkin mengalami kesalahan "file teks sibuk" jika Anda mencoba mengedit konten file yang sedang berjalan. Ini juga menggagalkan tujuan penggunaan mv
perintah sistem berkas dan Anda dapat menjalankan sistem berkas tujuan di luar ruang dengan hanya berkas yang ditulis sebagian.
Ini juga tidak ada hubungannya dengan implementasi Ruby. Sistem mv
dan cp
perintah berperilaku serupa.
Apa yang lebih disukai adalah membuka Tempfile di direktori yang sama dengan file lama. Ini memastikan bahwa tidak akan ada masalah perpindahan lintas perangkat. Itu mv
sendiri tidak akan pernah gagal, dan Anda harus selalu mendapatkan file yang lengkap dan tidak terpotong. Kegagalan apa pun, seperti perangkat kehabisan ruang, kesalahan izin, dll., Harus ditemui selama penulisan Tempfile.
Satu-satunya kelemahan pendekatan pembuatan Tempfile di direktori tujuan adalah:
Berikut beberapa kode yang menerapkan algoritme lengkap (kode windows belum teruji dan belum selesai):
#!/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")
Dan ini adalah versi yang sedikit lebih ketat yang tidak mengkhawatirkan setiap kemungkinan kasus tepi (jika Anda menggunakan Unix dan tidak peduli tentang menulis ke / 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")
Kasus penggunaan yang sangat sederhana, ketika Anda tidak peduli dengan izin sistem file (baik Anda tidak menjalankan sebagai root, atau Anda menjalankan sebagai root dan file tersebut dimiliki 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 : Itu setidaknya harus digunakan sebagai ganti jawaban yang diterima, dalam semua kasus, untuk memastikan pembaruan bersifat atomic dan pembaca yang bersamaan tidak akan melihat file yang terpotong. Seperti yang saya sebutkan di atas, membuat Tempfile di direktori yang sama dengan file yang diedit penting di sini untuk menghindari operasi mv lintas perangkat diterjemahkan ke dalam operasi cp jika / tmp dipasang pada perangkat yang berbeda. Memanggil fdatasync adalah lapisan tambahan dari paranoia, tetapi akan menimbulkan performa yang buruk, jadi saya menghilangkannya dari contoh ini karena hal ini tidak umum dilakukan.
Sebenarnya tidak ada cara untuk mengedit file di tempat. Apa yang biasanya Anda lakukan ketika Anda dapat melakukannya (misalnya jika file tidak terlalu besar) adalah, Anda membaca file ke dalam memori ( File.read
), melakukan penggantian pada string baca ( String#gsub
) dan kemudian menulis string yang diubah kembali ke file ( File.open
, File#write
).
Jika file cukup besar sehingga tidak dapat digunakan, yang perlu Anda lakukan, adalah membaca file dalam potongan (jika pola yang ingin Anda ganti tidak akan menjangkau banyak baris maka satu potongan biasanya berarti satu baris - Anda dapat menggunakannya File.foreach
untuk membaca file baris demi baris), dan untuk setiap potongan melakukan substitusi di atasnya dan menambahkannya ke file sementara. Ketika Anda selesai mengulang file sumber, Anda menutupnya dan menggunakan FileUtils.mv
untuk menimpanya dengan file sementara.
Pendekatan lain adalah dengan menggunakan pengeditan di dalam Ruby (bukan dari baris perintah):
#!/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
Jika Anda tidak ingin membuat cadangan, ubah '.bak'
ke ''
.
read
) file. Ini dapat diskalakan dan harus sangat cepat.
Ini bekerja untuk saya:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Berikut adalah solusi untuk menemukan / mengganti di semua file dari direktori tertentu. Pada dasarnya saya mengambil jawaban yang diberikan oleh sepp2k dan mengembangkannya.
# 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) }
Jika Anda perlu melakukan substitusi melintasi batas garis, maka penggunaan ruby -pi -e
tidak akan berfungsi karena p
prosesnya satu baris dalam satu waktu. Sebagai gantinya, saya merekomendasikan yang berikut ini, meskipun bisa gagal dengan file multi-GB:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Pencarian spasi putih (kemungkinan termasuk baris baru) diikuti dengan kutipan, dalam hal ini menghilangkan spasi. Ini %q(')
hanyalah cara yang bagus untuk mengutip karakter kutipan.
Berikut alternatif satu liner dari jim, kali ini dalam skrip
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Simpan dalam sebuah script, mis. Replace.rb
Anda mulai pada baris perintah dengan
replace.rb *.txt <string_to_replace> <replacement>
* .txt dapat diganti dengan pilihan lain atau dengan beberapa nama file atau jalur
rusak sehingga saya bisa menjelaskan apa yang terjadi tetapi masih dapat dieksekusi
# 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
EDIT: jika Anda ingin menggunakan ekspresi reguler gunakan ini sebagai gantinya Jelas, ini hanya untuk menangani file teks yang relatif kecil, tidak ada monster Gigabyte
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
perlu disesuaikan dengan informasi di stackoverflow.com/a/25189286/128421 untuk mengapa menyeruput file besar itu buruk. Juga, alih-alihFile.open(filename, "w") { |file| file << content }
menggunakan variasiFile.write(filename, content)
.