Cara mencari teks file untuk pola dan menggantinya dengan nilai yang diberikan

117

Saya mencari skrip untuk mencari sebuah file (atau daftar file) untuk sebuah pola dan, jika ditemukan, ganti pola itu dengan nilai yang diberikan.

Pikiran?

Dane O'Connor
sumber
1
Dalam jawaban di bawah ini, ketahuilah bahwa setiap rekomendasi untuk digunakan File.readperlu disesuaikan dengan informasi di stackoverflow.com/a/25189286/128421 untuk mengapa menyeruput file besar itu buruk. Juga, alih-alih File.open(filename, "w") { |file| file << content }menggunakan variasi File.write(filename, content).
Tin Man

Jawaban:

190

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
Max Chernyak
sumber
Apakah menempatkan menulis perubahan kembali ke file? Saya pikir itu hanya akan mencetak konten ke konsol.
Dane O'Connor
Ya, itu mencetak konten ke konsol.
sepp2k
7
Ya, saya tidak yakin itu yang Anda inginkan. Untuk menulis gunakan File.open (nama_file, "w") {| file | file.puts output_of_gsub}
Max Chernyak
7
Saya harus menggunakan file.write: File.open (nama_file, "w") {| file | file.write (teks)}
austen
3
Untuk menulis file, ganti put 'line denganFile.write(file_name, text.gsub(/regexp/, "replace")
ketat
106

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.

Jim Kane
sumber
1
Apa sih pi.bak itu? Tanpa itu, saya mendapatkan kesalahan. -e: 1: di <main>': undefined method gsub 'untuk main: Object (NoMethodError)
Ninad
15
@NinadPachpute -imengedit di tempat. .bakadalah ekstensi yang digunakan untuk file cadangan (opsional). -padalah sesuatu seperti while gets; <script>; puts $_; end. ( $_adalah baris baca terakhir, tetapi Anda dapat menetapkannya untuk sesuatu seperti echo aa | ruby -p -e '$_.upcase!'.)
Lri
1
Ini adalah jawaban yang lebih baik daripada jawaban yang diterima, IMHO, jika Anda ingin mengubah file.
Colin K
6
Bagaimana saya bisa menggunakan ini di dalam skrip ruby ​​??
Saurabh
1
Ada banyak hal yang bisa menyebabkan kesalahan ini, jadi ujilah secara menyeluruh sebelum mencobanya pada file penting.
the Tin Man
49

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:

  1. Membaca file lama dan menulis ke file baru. (Anda harus berhati-hati saat memasukkan seluruh file ke dalam memori).

  2. 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.

  3. Memperbaiki izin dan mode file pada file baru.

  4. 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.mvuntuk 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 mvfile di perangkat mount Anda akan secara transparan diubah ke cpperilaku. 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 mvperintah 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 mvdan cpperintah 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 mvsendiri 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:

  • Terkadang Anda mungkin tidak dapat membuka Tempfile di sana, seperti jika Anda mencoba 'mengedit' file di / proc misalnya. Oleh karena itu, Anda mungkin ingin mundur dan mencoba / tmp jika membuka file di direktori tujuan gagal.
  • Anda harus memiliki cukup ruang di partisi tujuan untuk menyimpan file lama dan file baru secara lengkap. Namun, jika Anda tidak memiliki cukup ruang untuk menyimpan kedua salinan maka Anda mungkin kekurangan ruang disk dan risiko sebenarnya dari menulis file yang terpotong jauh lebih tinggi, jadi saya berpendapat ini adalah pertukaran yang sangat buruk di luar beberapa yang sangat sempit (dan baik -Diawasi) kasus tepi.

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.

lamont
sumber
Alih-alih membuka file temp di direktori tempat Anda berada, sebenarnya akan secara otomatis membuatnya di direktori data aplikasi (pada Windows) dan dari mereka Anda dapat melakukan file.unlink untuk menghapusnya ..
13aal
3
Saya sangat menghargai pemikiran ekstra yang dimasukkan ke dalam hal ini. Sebagai seorang pemula, sangat menarik untuk melihat pola pikir dari developer berpengalaman yang tidak hanya dapat menjawab pertanyaan awal, tetapi juga mengomentari konteks yang lebih luas dari apa sebenarnya arti pertanyaan asli.
ramijames
Pemrograman bukan hanya tentang memperbaiki masalah langsung, ini juga tentang berpikir jauh ke depan untuk menghindari masalah lain yang menunggu. Tidak ada yang lebih mengganggu pengembang senior selain menemukan kode yang mengecat algoritme menjadi sudut, memaksa kludge yang canggung, ketika penyesuaian kecil sebelumnya akan menghasilkan aliran yang bagus. Analisis dapat memakan waktu berjam-jam, atau berhari-hari untuk memahami tujuan, dan kemudian beberapa baris menggantikan halaman kode lama. Ini seperti permainan catur melawan data dan sistem pada waktu tertentu.
Tin Man
11

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.foreachuntuk 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.mvuntuk menimpanya dengan file sementara.

sepp2k.dll
sumber
1
Saya suka pendekatan streaming. Kami menangani file besar secara bersamaan sehingga kami biasanya tidak memiliki ruang dalam RAM untuk membaca seluruh file
Shane
" Mengapa" menyeruput "file bukanlah praktik yang baik? " Mungkin bacaan yang berguna terkait dengan ini.
Tin Man
9

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 ''.

DavidG
sumber
1
Ini akan lebih baik daripada mencoba slurp ( read) file. Ini dapat diskalakan dan harus sangat cepat.
Manusia Timah
Ada bug di suatu tempat yang menyebabkan Ruby 2.3.0p0 di Windows gagal dengan izin ditolak jika ada beberapa blok inplace_edit berturut-turut yang bekerja pada file yang sama. Untuk mereproduksi tes split search1 dan search2 menjadi 2 blok. Tidak menutup sepenuhnya?
mlt
Saya mengharapkan masalah dengan beberapa pengeditan file teks yang terjadi secara bersamaan. Jika tidak ada yang lain, Anda bisa mendapatkan file teks yang rusak parah.
Tin Man
7

Ini bekerja untuk saya:

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

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
penyamak
sumber
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
sumber
2
Akan lebih membantu jika Anda memberikan penjelasan mengapa ini adalah solusi yang disukai dan menjelaskan cara kerjanya. Kami ingin mendidik, bukan hanya memberikan kode.
Tin Man
trollop berganti nama menjadi optimist github.com/manageiq/optimist . Juga itu hanya parser opsi CLI yang tidak benar-benar diperlukan untuk menjawab pertanyaan.
noraj
1

Jika Anda perlu melakukan substitusi melintasi batas garis, maka penggunaan ruby -pi -etidak akan berfungsi karena pprosesnya 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.

Dan Kohn
sumber
1

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]))}
peter
sumber
Kode ini tidak akan berfungsi. Saya sarankan mengujinya sebelum memposting, lalu salin dan tempel kode yang berfungsi.
Tin Man
@ theTinMan Saya selalu menguji sebelum menerbitkan, jika memungkinkan. Saya menguji ini dan berhasil, baik versi singkat sebagai komentar. Menurut Anda, mengapa tidak?
peter
jika Anda bermaksud menggunakan ekspresi reguler lihat suntingan saya, juga diuji:>)
peter