web-dev-qa-db-ja.com

パターンのファイルテキストを検索し、指定された値に置き換える方法

ファイル(またはファイルのリスト)でパターンを検索し、見つかった場合はそのパターンを特定の値に置き換えるスクリプトを探しています。

考え?

112
Dane O'Connor

免責事項:このアプローチは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
184
Max Chernyak

実際、Rubyにはインプレース編集機能があります。 Perlのように、あなたは言うことができます

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

これにより、名前が「.txt」で終わる現在のディレクトリ内のすべてのファイルに二重引用符で囲まれたコードが適用されます。編集したファイルのバックアップコピーは、拡張子「.bak」(「foobar.txt.bak」だと思います)で作成されます。

注:これは、複数行の検索では機能しないようです。それらの場合、正規表現を囲むラッパースクリプトを使用して、他のあまり美しくない方法でそれを行う必要があります。

100
Jim Kane

これを行うと、ファイルシステムの容量が不足し、長さゼロのファイルを作成する可能性があることに注意してください。システム構成管理の一部として/ etc/passwdファイルを書き出すようなことをしている場合、これは壊滅的です。

[編集:受け入れられた回答のようなインプレースファイル編集では、常にファイルが切り捨てられ、新しいファイルが順番に書き出されることに注意してください。同時に実行されるリーダーに切り捨てられたファイルが表示される競合状態が常に存在します。書き込み中に何らかの理由(ctrl-c、OOMキラー、システムクラッシュ、停電など)でプロセスが中止された場合、切り捨てられたファイルも残されます。これは壊滅的です。これは、開発者が考慮する必要があるデータ損失シナリオの一種です。そのため、受け入れられた答えは受け入れられた答えではない可能性が高いと思います。少なくとも、一時ファイルに書き込み、この回答の最後にある「単純な」ソリューションのような場所にファイルを移動/名前変更します。]

次のアルゴリズムを使用する必要があります。

  1. 古いファイルを読み取り、新しいファイルに書き出します。 (ファイル全体をメモリに丸aboutみすることに注意する必要があります)。

  2. 新しい一時ファイルを明示的に閉じます。これは、スペースがないためファイルバッファをディスクに書き込むことができないため、例外をスローする可能性がある場所です。 (必要に応じてこれをキャッチし、一時ファイルをクリーンアップしますが、この時点で何かを再スローするか、かなり失敗する必要があります。

  3. 新しいファイルのファイル許可とモードを修正します。

  4. 新しいファイルの名前を変更し、所定の場所にドロップします。

Ext3ファイルシステムでは、ファイルを所定の場所に移動するためのメタデータの書き込みがファイルシステムによって再配置されず、新しいファイルのデータバッファーが書き込まれる前に書き込まれることが保証されているため、これは成功または失敗するはずです。このような動作をサポートするために、ext4ファイルシステムにもパッチが適用されています。あなたが非常に妄想している場合は、ファイルを所定の場所に移動する前に、ステップ3.5としてfdatasync()システムコールを呼び出す必要があります。

言語に関係なく、これはベストプラクティスです。 close()を呼び出しても例外(PerlまたはC)がスローされない言語では、close()の戻りを明示的にチェックし、失敗した場合は例外をスローする必要があります。

単にファイルをメモリに丸Sみし、それを操作してファイルに書き出すという上記の提案は、完全なファイルシステムで長さゼロのファイルを生成することを保証します。 always use FileUtils.mvを使用して、完全に書き込まれた一時ファイルを所定の場所に移動する必要があります。

最後の考慮事項は、一時ファイルの配置です。/tmpのファイルを開く場合、いくつかの問題を考慮する必要があります。

  • / tmpが別のファイルシステムにマウントされている場合、古いファイルの保存先に展開できるファイルを書き出す前に、/ tmpの領域を使い果たす可能性があります。
  • おそらくもっと重要なのは、デバイスマウント全体でファイルをmvしようとすると、cp動作に透過的に変換されることです。古いファイルが開かれ、古いファイルのiノードが保存されて再度開かれ、ファイルの内容がコピーされます。これはおそらくあなたが望むものではなく、実行中のファイルの内容を編集しようとすると「テキストファイルビジー」エラーが発生する可能性があります。また、これはファイルシステムmvコマンドを使用する目的を無効にし、部分的に書き込まれたファイルのみで目的のファイルシステムをスペース不足で実行できます。

    これは、Rubyの実装とも関係ありません。システムmvおよびcpコマンドは同様に動作します。

より望ましいのは、古いファイルと同じディレクトリでTempfileを開くことです。これにより、デバイス間の移動の問題が発生しなくなります。 mv自体が失敗することはなく、常に完全で切り捨てられていないファイルを取得する必要があります。デバイスの領域不足、許可エラーなどの障害は、Tempfileの書き込み中に発生する必要があります。

宛先ディレクトリにTempfileを作成するアプローチの唯一の欠点は次のとおりです。

  • たとえば、/ procのファイルを「編集」しようとしている場合など、一時ファイルを開くことができない場合があります。そのため、宛先ディレクトリのファイルを開くことができない場合は、フォールバックして/ tmpを試してください。
  • 完全な古いファイルと新しいファイルの両方を保持するために、宛先パーティションに十分なスペースが必要です。ただし、両方のコピーを保持するのに十分なスペースがない場合は、おそらくディスクスペースが不足しており、切り捨てられたファイルを書き込む実際のリスクははるかに高いので、非常に狭い(そして-monitored)エッジケース。

完全なアルゴリズムを実装するコードを次に示します(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")

そして、これは、可能性のあるすべてのEdgeケースを心配しない、少しタイトなバージョンです(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の呼び出しはパラノイアの追加レイヤーですが、パフォーマンスヒットが発生するため、一般的には実行されないため、この例から省略しました。

46
lamont

ファイルをその場で編集する方法は実際にはありません。 (ファイルが大きすぎない場合など)逃げることができる場合に通常行うことは、ファイルをメモリ(File.read)に読み込み、読み込んだ文字列(String#gsub)で置換を実行し、変更した文字列を書き戻すことです。ファイル(File.openFile#write)に。

ファイルが大きすぎて実行できない場合、必要なことは、ファイルをチャンクで読み取ります(置換するパターンが複数行にまたがらない場合、1つのチャンクは通常1行を意味します-File.foreachを使用できます)ファイルを1行ずつ読み込む)、各チャンクに対して置換を実行し、一時ファイルに追加します。ソースファイルの繰り返しが完了したら、それを閉じてFileUtils.mvを使用して一時ファイルで上書きします。

11
sepp2k

別のアプローチは、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」を「」に変更します。

9
DavidG

これは私のために働く:

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

特定のディレクトリのすべてのファイルを検索/置換するためのソリューションを次に示します。基本的に、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
6
tanner
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) }
4
Ninad

行の境界を越えて置換を行う必要がある場合、pは一度に1行を処理するため、Ruby -pi -eの使用は機能しません。代わりに、以下をお勧めしますが、マルチGBファイルでは失敗する可能性があります。

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

は、引用符が後に続く空白(潜在的に改行を含む)を探しています。この場合、空白は削除されます。 %q(')は、引用文字を引用するための凝った方法です。

1
Dan Kohn

ここでは、jimの1つのライナーの代わりに、今回はスクリプトで

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
0
peter