回答:
免責事項: このアプローチはRubyの機能の単純な例であり、ファイル内の文字列を置き換えるための本番環境レベルのソリューションではありません。クラッシュ、割り込み、ディスクがいっぱいになった場合のデータ損失など、さまざまな障害シナリオが発生しやすくなります。このコードは、すべてのデータがバックアップされる簡単な1回限りのスクリプト以外には適していません。そのため、このコードをプログラムにコピーしないでください。
これを行う簡単な短い方法を次に示します。
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")
実際、Rubyにはインプレース編集機能があります。Perlのように、あなたは言うことができます
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
これにより、名前が「.txt」で終わる現在のディレクトリ内のすべてのファイルに二重引用符で囲まれたコードが適用されます。編集したファイルのバックアップコピーは、「。bak」拡張子(「foobar.txt.bak」だと思います)を付けて作成されます。
注:これは複数行検索では機能しないようです。それらについては、正規表現の周りのラッパースクリプトを使用して、それほど簡単ではない方法で行う必要があります。
<main>': undefined method
gsub 'for main:Object(NoMethodError)
-i
編集を行います。.bak
バックアップファイルに使用される拡張子です(オプション)。-p
のようなものですwhile gets; <script>; puts $_; end
。($_
は最後の読み取り行ですが、に割り当てることができますecho aa | ruby -p -e '$_.upcase!'
。)
これを行うと、ファイルシステムがスペース不足になり、長さがゼロのファイルが作成される可能性があることに注意してください。システム構成管理の一部として/ etc / passwdファイルを書き出すような場合、これは壊滅的です。
受け入れられた回答のようなインプレースファイル編集では、常にファイルが切り捨てられ、新しいファイルが順番に書き出されることに注意してください。同時読み取りを行うと、切り捨てられたファイルが表示されるという競合状態が常に発生します。書き込み中にプロセスが何らかの理由(ctrl-c、OOMキラー、システムクラッシュ、停電など)で中止された場合、切り捨てられたファイルも残され、これは壊滅的な場合があります。これは、データ損失が発生するため、開発者が考慮しなければならない種類のデータ損失シナリオです。そのため、受け入れられた答えはおそらく受け入れられた答えではないはずです。最小限の一時ファイルへの書き込みと、この回答の最後にある「単純な」ソリューションのようにファイルを適切な場所に移動/名前変更します。
次のアルゴリズムを使用する必要があります。
古いファイルを読み取り、新しいファイルに書き込みます。(ファイル全体をメモリに投入することに注意する必要があります)。
新しい一時ファイルを明示的に閉じます。スペースがないため、ファイルバッファーをディスクに書き込むことができないため、例外がスローされる可能性があります。(これをキャッチして、必要に応じて一時ファイルをクリーンアップしますが、この時点で何かを再スローするか、かなり激しく失敗する必要があります。
新しいファイルのファイル権限とモードを修正します。
新しいファイルの名前を変更し、所定の場所にドロップします。
ext3ファイルシステムでは、ファイルを所定の場所に移動するためのメタデータの書き込みがファイルシステムによって再配置されず、新しいファイルのデータバッファーが書き込まれる前に書き込まれるため、これは成功または失敗するはずです。この種の動作をサポートするために、ext4ファイルシステムにもパッチが適用されています。非常に偏執的である場合はfdatasync()
、ファイルを所定の場所に移動する前に、ステップ3.5としてシステムコールを呼び出す必要があります。
言語に関係なく、これはベストプラクティスです。呼び出しclose()
が例外をスローしない言語(PerlまたはC)では、戻り値を明示的に確認し、close()
失敗した場合は例外をスローする必要があります。
ファイルをメモリに丸呑みし、操作してファイルに書き出すという上記の提案は、完全なファイルシステムで長さゼロのファイルを生成することが保証されます。完全に書き込まれた一時ファイルを所定の場所に移動するには、常にを使用する必要がありFileUtils.mv
ます。
最後の考慮事項は、一時ファイルの配置です。/ tmp内のファイルを開く場合、いくつかの問題を考慮する必要があります。
/ tmpが別のファイルシステムにマウントされている場合は、古いファイルの宛先にデプロイできるファイルを書き出す前に、/ tmpのスペースが不足する可能性があります。
おそらくもっと重要なmv
ことは、デバイスマウント全体でファイルにアクセスしようとすると、透過的にcp
動作に変換されることです。古いファイルが開かれ、古いファイルのiノードが保持されて再度開かれ、ファイルの内容がコピーされます。これはおそらく望んでいることではなく、実行中のファイルの内容を編集しようとすると、「テキストファイルビジー」エラーが発生する可能性があります。これは、ファイルシステムmv
コマンドを使用する目的にも反するものであり、部分的に書き込まれたファイルのみを使用して、宛先のファイルシステムをスペース不足で実行する可能性があります。
これもRubyの実装とは関係ありません。システムmv
とcp
コマンドは同様に動作します。
古いファイルと同じディレクトリでTempfileを開くのがより望ましいです。これにより、デバイス間の移動の問題が発生しなくなります。mv
自身が失敗することはありません、あなたは常に完全で切り捨てられていないファイルを取得する必要があります。Tempfileの書き込み中に、デバイスの容量不足、権限エラーなどの障害が発生するはずです。
宛先ディレクトリにTempfileを作成する方法の唯一の欠点は次のとおりです。
以下は、完全アルゴリズムを実装するコードです(Windowsコードはテストされておらず、未完成です)。
#!/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")
そして、これは可能なすべてのエッジケースを心配しない少しタイトなバージョンです(Unixを使用していて、/ 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")
ファイルシステムのアクセス許可を気にしない場合(ルートとして実行していないか、ルートとして実行していて、ファイルがルート所有である場合)の非常に単純なユースケース:
#!/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:更新がアトミックであり、同時リーダーが切り捨てられたファイルを表示しないようにするために、すべての場合において、最低でも承認された回答の代わりに使用する必要があります。上で述べたように、/ tmpが別のデバイスにマウントされている場合、クロスデバイスmv操作がcp操作に変換されないようにするには、編集済みファイルと同じディレクトリにTempfileを作成することが重要です。fdatasyncの呼び出しはパラノイアの追加レイヤーですが、パフォーマンスに影響を与えるため、一般的には実行されないため、この例では省略しました。
ファイルをその場で編集する方法は実際にはありません。ファイルを大きくしない場合(つまり、ファイルが大きすぎない場合)は通常、ファイルをメモリに読み込み(File.read
)、読み取り文字列に置換を実行し()、String#gsub
変更された文字列をファイル(File.open
、File#write
)。
ファイルがそれを実行不可能にするのに十分な大きさである場合、あなたがしなければならないことは、ファイルをチャンクで読み取ることです(置換するパターンが複数行にまたがらない場合、1つのチャンクは通常1行を意味します-を使用File.foreach
して行ごとにファイルを読み取ります)。チャンクごとに置換を実行し、一時ファイルに追加します。ソースファイルの反復が完了したら、ソースファイルを閉じ、を使用FileUtils.mv
して一時ファイルで上書きします。
別のアプローチは、コマンドラインからではなく、Ruby内でインプレース編集を使用することです。
#!/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
バックアップを作成したくない場合は、に変更'.bak'
してください''
。
read
、ファイルを丸呑みにする()よりも優れています。スケーラブルで、非常に高速でなければなりません。
これは、特定のディレクトリのすべてのファイルで検索/置換するためのソリューションです。基本的に、私はsepp2kによって提供された答えを受け取り、それを拡張しました。
# 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) }
ここではジムのワンライナーの代わりに、今回はスクリプトで
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
スクリプトに保存します。例:replace.rb
コマンドラインから始めます
replace.rb *.txt <string_to_replace> <replacement>
* .txtは別の選択、または一部のファイル名またはパスに置き換えることができます
何が起こっているのかを説明できるように分解されましたが、まだ実行可能です
# 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
編集:正規表現を使用したい場合は代わりにこれを使用してください明らかに、これは比較的小さなテキストファイルを処理するためだけであり、ギガバイトのモンスターは対象外です
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
する推奨事項はすべて、大きなファイルを丸呑みすることが悪い理由として、stackoverflow.com / a / 25189286/128421の情報で調整する必要があることに注意してください 。また、File.open(filename, "w") { |file| file << content }
バリエーションの代わりにを使用しますFile.write(filename, content)
。