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


117

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

생각?


1
아래의 답변에서, 사용에 대한 권고 알고있을 File.read필요가있는 정보를 강화하는 stackoverflow.com/a/25189286/128421 큰 파일을 소리내어하는 것이 나쁜 이유를. 또한 File.open(filename, "w") { |file| file << content }변형 대신 File.write(filename, content).
Tin Man

답변:


190

면책 조항 : 이 접근 방식은 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

Puts가 변경 사항을 파일에 다시 기록합니까? 내용이 콘솔에 인쇄 될 것이라고 생각했습니다.
Dane O'Connor

예, 콘텐츠를 콘솔에 인쇄합니다.
sepp2k

7
예, 그게 당신이 원했던 것인지 잘 모르겠습니다. 쓰려면 File.open (file_name, "w") {| file | file.puts output_of_gsub}
Max Chernyak

7
나는 file.write를 사용해야했다 : File.open (file_name, "w") {| file | file.write가 (텍스트)}
오스틴

3
쓰기 파일에와 풋 '라인을 교체File.write(file_name, text.gsub(/regexp/, "replace")

106

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

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

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

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


1
도대체 pi.bak은 무엇입니까? 그것 없이는 오류가 발생합니다. -e : 1 : in <main>': undefined method gsub 'for main : Object (NoMethodError)
Ninad

15
@NinadPachpute -i가 제자리에서 편집합니다. .bak백업 파일에 사용되는 확장자입니다 (선택 사항). -p같은 것 while gets; <script>; puts $_; end입니다. ( $_마지막으로 읽은 echo aa | ruby -p -e '$_.upcase!'
줄이지 만

1
파일을 수정하려는 경우 허용되는 대답 IMHO보다 더 나은 대답입니다.
Colin K

6
이것을 루비 스크립트 안에서 어떻게 사용할 수 있습니까 ??
Saurabh

1
이것이 잘못 될 수있는 많은 방법이 있으므로 중요한 파일에 대해 시도하기 전에 철저히 테스트하십시오.
The Tin Man

49

이렇게하면 파일 시스템에 공간이 부족할 수 있으며 길이가 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.unlink를 할 수 있습니다.
13aal

3
나는 이것에 대한 추가 생각에 정말로 감사했습니다. 초보자로서 원래 질문에 답할 수있을뿐만 아니라 원래 질문이 실제로 의미하는 바에 대한 더 큰 맥락에 대해 언급 할 수있는 숙련 된 개발자의 사고 패턴을 보는 것은 매우 흥미 롭습니다.
ramijames

프로그래밍은 즉각적인 문제를 해결하는 것뿐만 아니라 다른 문제를 피하기 위해 미리 생각하는 것이기도합니다. 이전에 약간의 조정이 좋은 흐름을 가져 왔을 때 알고리즘을 구석에 칠한 코드를 만나는 것보다 시니어 개발자를 짜증나게하는 것은 없습니다. 목표를 이해하기 위해 분석하는 데 몇 시간 또는 며칠이 걸리는 경우가 많으며 이전 코드 페이지를 몇 줄로 대체합니다. 그것은 때때로 데이터와 시스템에 대한 체스 게임과 같습니다.
The Tin Man

11

제자리에서 파일을 편집하는 방법은 없습니다. 파일이 너무 크지 않을 때 일반적으로 수행하는 작업은 파일을 메모리로 읽고 ( File.read), 읽기 문자열 ( String#gsub) 에서 대체를 수행 한 다음 변경된 문자열을 다시 파일 ( File.open, File#write).

파일이 실행 불가능할만큼 충분히 크면 파일을 청크 단위로 읽는 것입니다 (대체하려는 패턴이 여러 줄에 걸쳐 있지 않으면 일반적으로 한 청크가 한 줄을 의미 File.foreach합니다. 한 줄씩 파일을 읽고) 각 청크에 대해 대체를 수행하고 임시 파일에 추가합니다. 소스 파일에 대한 반복이 완료되면 파일을 닫고을 사용 FileUtils.mv하여 임시 파일로 덮어 씁니다.


1
나는 스트리밍 방식을 좋아합니다. 대용량 파일을 동시에 처리하므로 일반적으로 전체 파일을 읽을 수있는 RAM 공간이 없습니다
Shane


9

또 다른 접근 방식은 명령 줄이 아닌 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'''.


1
이것은 read파일 을 슬러 핑 ( )하는 것보다 낫습니다 . 확장 가능하고 매우 빠릅니다.
Tin Man

동일한 파일에서 작동하는 연속적인 inplace_edit 블록이 여러 개있는 경우 Windows에서 Ruby 2.3.0p0이 실패하고 권한이 거부되는 버그가 있습니다. 분할 search1 및 search2 테스트를 2 개의 블록으로 재현합니다. 완전히 닫히지 않습니까?
mlt

동시에 발생하는 텍스트 파일의 여러 편집에 문제가있을 것으로 예상합니다. 다른 것이 없다면 심하게 엉망인 텍스트 파일을 얻을 수 있습니다.
The Tin Man

7

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

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

6

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

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) }

2
이것이 선호되는 솔루션 인 이유를 설명하고 작동 방식을 설명하면 더 많은 도움이됩니다. 우리는 단순히 코드를 제공하는 것이 아니라 교육을 원합니다.
The Tin Man

trollop 은 optimist github.com/manageiq/optimist 로 이름이 변경 되었습니다 . 또한 질문에 답하는 데 실제로 필요하지 않은 CLI 옵션 파서 일뿐입니다.
noraj

1

라인 경계를 넘어서 대체해야하는 경우 한 번에 한 라인 씩 처리 ruby -pi -e하므로 사용 이 작동하지 않습니다 p. 대신 다중 GB 파일로 실패 할 수 있지만 다음을 권장합니다.

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

는 따옴표 뒤에 오는 공백 (새 줄을 포함 할 수 있음)을 찾고 있으며,이 경우 공백을 제거합니다. 는 %q(')인용 문자를 인용 단지 멋진 방법입니다.


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

편집 : 정규식을 사용하려면 대신 이것을 사용하십시오. 당연히 이것은 상대적으로 작은 텍스트 파일을 처리하기위한 것이며 기가 바이트 괴물이 아닙니다.

ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}

이 코드는 작동하지 않습니다. 게시하기 전에 테스트 한 다음 작업 코드를 복사하여 붙여 넣는 것이 좋습니다.
The Tin Man

@theTinMan 가능하면 게시하기 전에 항상 테스트합니다. 나는 이것을 테스트했고 그것은 주석이 달린 버전만큼 짧게 작동합니다. 왜 그렇지 않을까요?
피터

정규 표현식을 사용하는 것을 의미한다면 내 편집을 참조하십시오. :>)
peter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.