[ruby] 파일 텍스트에서 패턴을 검색하고 주어진 값으로 바꾸는 방법

파일 (또는 파일 목록)에서 패턴을 검색하고 찾은 경우 해당 패턴을 주어진 값으로 바꾸는 스크립트를 찾고 있습니다.

생각?



답변

면책 조항 : 이 접근 방식은 Ruby의 기능에 대한 순진한 예시이며 파일의 문자열을 대체하는 프로덕션 등급 솔루션이 아닙니다. 충돌, 인터럽트 또는 디스크가 가득 찬 경우 데이터 손실과 같은 다양한 오류 시나리오가 발생하기 쉽습니다. 이 코드는 모든 데이터가 백업되는 빠른 일회성 스크립트 외에는 적합하지 않습니다. 이러한 이유로, 프로그램이 코드를 복사하지 마십시오.

여기에 간단한 방법이 있습니다.

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


답변

실제로 Ruby에는 내부 편집 기능이 있습니다. Perl처럼 다음과 같이 말할 수 있습니다.

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

이렇게하면 이름이 “.txt”로 끝나는 현재 디렉토리의 모든 파일에 큰 따옴표로 묶인 코드가 적용됩니다. 편집 된 파일의 백업 복사본은 “.bak”확장자 ( “foobar.txt.bak”라고 생각합니다)로 생성됩니다.

참고 : 여러 줄 검색에는 작동하지 않는 것 같습니다. 이를 위해서는 정규식을 둘러싼 래퍼 스크립트를 사용하여 덜 예쁜 방법으로 수행해야합니다.


답변

이렇게하면 파일 시스템에 공간이 부족할 수 있으며 길이가 0 인 파일을 만들 수 있습니다. 시스템 구성 관리의 일부로 / etc / passwd 파일을 작성하는 것과 같은 작업을 수행하는 경우 이는 치명적입니다.

수락 된 답변과 같은 내부 파일 편집은 항상 파일을 자르고 새 파일을 순차적으로 작성합니다. 동시 독자가 잘린 파일을 보는 경쟁 조건이 항상 있습니다. 쓰기 중에 어떤 이유로 든 (ctrl-c, OOM 킬러, 시스템 충돌, 정전 등) 프로세스가 중단되면 잘린 파일도 남게되며 이는 치명적일 수 있습니다. 이는 개발자가 반드시 고려해야하는 데이터 손실 시나리오입니다. 그렇기 때문에 받아 들여진 대답은 받아 들여진 대답이 아닐 가능성이 큽니다. 최소한 임시 파일에 쓰고이 답변 끝에있는 “간단한”솔루션과 같은 위치로 파일을 이동 / 이름을 바꿉니다.

다음과 같은 알고리즘을 사용해야합니다.

  1. 이전 파일을 읽고 새 파일에 씁니다. (전체 파일을 메모리에 넣을 때주의해야합니다).

  2. 공간이 없어서 파일 버퍼를 디스크에 쓸 수 없기 때문에 예외가 발생할 수있는 새 임시 파일을 명시 적으로 닫습니다. (원하는 경우 이것을 잡고 임시 파일을 정리하십시오. 그러나이 시점에서 무언가를 다시 던지거나 상당히 열심히 실패해야합니다.

  3. 새 파일의 파일 권한 및 모드를 수정합니다.

  4. 새 파일의 이름을 바꾸고 제자리에 놓습니다.

ext3 파일 시스템을 사용하면 파일을 제자리로 옮기기위한 메타 데이터 쓰기가 파일 시스템에 의해 재 배열되지 않고 새 파일에 대한 데이터 버퍼가 기록되기 전에 기록되지 않으므로 성공하거나 실패해야합니다. ext4 파일 시스템도 이러한 종류의 동작을 지원하도록 패치되었습니다. 편집증이 심한 경우 fdatasync()파일을 제자리로 옮기기 전에 3.5 단계로 시스템 호출을 호출 해야합니다 .

언어에 관계없이 이것이 가장 좋은 방법입니다. 호출시 close()예외가 발생하지 않는 언어 (Perl 또는 C)에서는 반환을 명시 적으로 확인하고 close()실패하면 예외를 throw해야합니다.

단순히 파일을 메모리에 넣고 조작하고 파일에 쓰라는 위의 제안은 전체 파일 시스템에서 길이가 0 인 파일을 생성하도록 보장됩니다. 완전히 작성된 임시 파일을 제자리로 이동 하려면 항상를 사용해야 FileUtils.mv합니다.

마지막 고려 사항은 임시 파일의 배치입니다. / tmp에서 파일을 열면 몇 가지 문제를 고려해야합니다.

  • / tmp가 다른 파일 시스템에 마운트 된 경우 이전 파일의 대상에 배포 할 수있는 파일을 작성하기 전에 공간이 부족하여 / tmp를 실행할 수 있습니다.

  • 아마도 더 중요한 것은 mv장치 마운트를 통해 파일을 시도 할 때 투명하게 cp동작으로 변환된다는 것 입니다. 이전 파일이 열리고 이전 파일 inode가 보존되고 다시 열리고 파일 내용이 복사됩니다. 이것은 원하는 것이 아닐 가능성이 높으며 실행중인 파일의 내용을 편집하려고하면 “텍스트 파일 사용 중”오류가 발생할 수 있습니다. 이것은 또한 파일 시스템 mv명령 을 사용하는 목적을 무효화하고 부분적으로 작성된 파일만으로 공간이 부족한 대상 파일 시스템을 실행할 수 있습니다.

    이것은 또한 Ruby의 구현과 관련이 없습니다. 시스템 mvcp명령은 비슷하게 작동합니다.

더 바람직한 것은 이전 파일과 동일한 디렉토리에서 Tempfile을 여는 것입니다. 이를 통해 장치 간 이동 문제가 발생하지 않습니다. mv자체는 결코 실패한다, 그리고 당신은 항상 완전하고 untruncated 파일을 받아야합니다. 장치 공간 부족, 권한 오류 등과 같은 오류는 Tempfile을 쓰는 동안 발생해야합니다.

대상 디렉터리에 Tempfile을 만드는 방법의 유일한 단점은 다음과 같습니다.

  • 예를 들어 / proc에서 파일을 ‘편집’하려는 경우와 같이 Tempfile을 열지 못할 수도 있습니다. 따라서 대상 디렉토리에서 파일을 여는 데 실패하면 폴백하여 / tmp를 시도 할 수 있습니다.
  • 전체 이전 파일과 새 파일을 모두 보관하려면 대상 파티션에 충분한 공간이 있어야합니다. 그러나 두 복사본을 모두 보관할 공간이 충분하지 않으면 디스크 공간이 부족할 수 있으며 잘린 파일을 작성할 실제 위험이 훨씬 더 높으므로 이것은 매우 좁은 일부를 제외하고는 매우 좋지 않은 절충안이라고 주장합니다. -모니터링) 에지 케이스.

다음은 전체 알고리즘을 구현하는 일부 코드입니다 (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")

그리고 가능한 모든 경우에 대해 걱정하지 않는 약간 더 타이트한 버전이 있습니다 (유닉스를 사용하고 / 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).

파일이 실행 불가능할만큼 충분히 크면 파일을 청크 단위로 읽는 것입니다 (대체하려는 패턴이 여러 줄에 걸쳐 있지 않으면 일반적으로 한 청크가 한 줄을 의미 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'''.


답변

이것은 나를 위해 작동합니다.

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


답변

다음은 주어진 디렉토리의 모든 파일에서 찾기 / 바꾸기를위한 솔루션입니다. 기본적으로 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