Szukam skryptu do przeszukiwania pliku (lub listy plików) pod kątem wzorca i, jeśli zostanie znaleziony, zamień ten wzorzec na podaną wartość.
Myśli?
Szukam skryptu do przeszukiwania pliku (lub listy plików) pod kątem wzorca i, jeśli zostanie znaleziony, zamień ten wzorzec na podaną wartość.
Myśli?
Odpowiedzi:
Zastrzeżenie: To podejście jest naiwną ilustracją możliwości Rubiego, a nie rozwiązaniem do zastępowania ciągów w plikach na poziomie produkcyjnym. Jest podatny na różne scenariusze awarii, takie jak utrata danych w przypadku awarii, przerwania lub zapełnienia dysku. Ten kod nie nadaje się do niczego poza szybkim, jednorazowym skryptem, w którym wszystkie dane są archiwizowane. Z tego powodu NIE kopiuj tego kodu do swoich programów.
Oto krótki krótki sposób, aby to zrobić.
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")
W rzeczywistości Ruby ma funkcję edycji lokalnej. Możesz powiedzieć, jak Perl
ruby -pi.bak -e "gsub(/oldtext/, 'newtext')" *.txt
Spowoduje to zastosowanie kodu w cudzysłowach do wszystkich plików w bieżącym katalogu, których nazwy kończą się na „.txt”. Kopie zapasowe edytowanych plików zostaną utworzone z rozszerzeniem „.bak” (chyba „foobar.txt.bak”).
UWAGA: wygląda na to, że nie działa w przypadku wyszukiwania wielowierszowego. W tym przypadku musisz zrobić to w inny mniej ładny sposób, stosując skrypt opakowujący wokół wyrażenia regularnego.
<main>': undefined method
gsub 'dla main: Object (NoMethodError)
-i
edytuje w miejscu. .bak
to rozszerzenie pliku kopii zapasowej (opcjonalnie). -p
jest czymś w rodzaju while gets; <script>; puts $_; end
. ( $_
to ostatnia przeczytana linia, ale możesz przypisać do niej coś w rodzaju echo aa | ruby -p -e '$_.upcase!'
.)
Pamiętaj, że kiedy to zrobisz, w systemie plików może zabraknąć miejsca i możesz utworzyć plik o zerowej długości. Jest to katastrofalne, jeśli robisz coś takiego jak wypisywanie plików / etc / passwd w ramach zarządzania konfiguracją systemu.
Zwróć uwagę, że edycja plików w miejscu, taka jak w zaakceptowanej odpowiedzi, zawsze spowoduje obcięcie pliku i sekwencyjne wypisanie nowego pliku. Zawsze wystąpi sytuacja wyścigu, w której współbieżni czytelnicy zobaczą obcięty plik. Jeśli proces zostanie przerwany z jakiegokolwiek powodu (ctrl-c, OOM killer, awaria systemu, awaria zasilania itp.) Podczas zapisu, to obcięty plik również zostanie pozostawiony, co może być katastrofalne. Jest to rodzaj scenariusza utraty danych, który programiści MUSZĄ wziąć pod uwagę, ponieważ tak się stanie. Z tego powodu uważam, że zaakceptowana odpowiedź najprawdopodobniej nie powinna być zaakceptowaną odpowiedzią. Jako minimum napisz do pliku tymczasowego i przenieś / zmień nazwę pliku na miejsce, tak jak w „prostym” rozwiązaniu na końcu tej odpowiedzi.
Musisz użyć algorytmu, który:
Odczytuje stary plik i zapisuje do nowego pliku. (Musisz uważać na wrzucanie całych plików do pamięci).
Jawnie zamyka nowy plik tymczasowy, w którym można zgłosić wyjątek, ponieważ nie można zapisać buforów plików na dysku, ponieważ nie ma miejsca. (Złap to i wyczyść plik tymczasowy, jeśli chcesz, ale musisz w tym momencie coś ponownie wrzucić lub dość mocno zawieść.
Naprawia uprawnienia do plików i tryby w nowym pliku.
Zmienia nazwę nowego pliku i umieszcza go na miejscu.
Dzięki systemom plików ext3 masz gwarancję, że metadane zapisywane w celu przeniesienia pliku na miejsce nie zostaną przestawione przez system plików i zapisane przed zapisaniem buforów danych dla nowego pliku, więc powinno to się powieść lub zakończyć niepowodzeniem. System plików ext4 również został załatany, aby obsługiwał tego typu zachowanie. Jeśli jesteś bardzo paranoikiem, powinieneś wywołać wywołanie fdatasync()
systemowe w kroku 3.5 przed przeniesieniem pliku na miejsce.
Niezależnie od języka jest to najlepsza praktyka. W językach, w których wywołanie close()
nie zgłasza wyjątku (Perl lub C), należy jawnie sprawdzić wynik close()
i zgłosić wyjątek, jeśli się nie powiedzie.
Powyższa sugestia, aby po prostu wsypać plik do pamięci, manipulować nim i zapisać go do pliku, gwarantuje utworzenie plików o zerowej długości na pełnym systemie plików. Trzeba zawsze używać FileUtils.mv
do poruszania się w pełni napisany plik tymczasowy na miejscu.
Ostatnią kwestią jest umieszczenie pliku tymczasowego. Jeśli otworzysz plik w / tmp, musisz wziąć pod uwagę kilka problemów:
Jeśli / tmp jest zamontowany w innym systemie plików, możesz uruchomić / tmp bez miejsca przed wypisaniem pliku, który w innym przypadku byłby możliwy do wdrożenia w miejscu docelowym starego pliku.
Prawdopodobnie ważniejsze jest to, że kiedy spróbujesz mv
przenieść plik na urządzenie, zostaniesz przekonwertowany na cp
zachowanie. Stary plik zostanie otwarty, stary i-węzeł plików zostanie zachowany i ponownie otwarty, a zawartość pliku zostanie skopiowana. Najprawdopodobniej to nie jest to, czego chcesz i możesz napotkać błąd „plik tekstowy zajęty”, jeśli spróbujesz edytować zawartość uruchomionego pliku. To również przeczy celowi używania mv
poleceń systemu plików i możesz uruchomić docelowy system plików bez miejsca, mając tylko częściowo zapisany plik.
Nie ma to również nic wspólnego z implementacją Rubiego. System mv
i cp
polecenia zachowują się podobnie.
Bardziej preferowane jest otwarcie pliku tymczasowego w tym samym katalogu, w którym znajduje się stary plik. Gwarantuje to, że nie będzie problemów z przenoszeniem między urządzeniami. mv
Sama nigdy nie uda, i zawsze należy uzyskać pełną i untruncated pliku. Podczas zapisywania pliku tymczasowego należy napotkać wszelkie awarie, takie jak brak miejsca na urządzeniu, błędy uprawnień itp.
Jedyne wady podejścia do tworzenia pliku tymczasowego w katalogu docelowym to:
Oto kod, który implementuje pełny algorytm (kod systemu Windows jest nieprzetestowany i niedokończony):
#!/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")
A tutaj jest nieco ściślejsza wersja, która nie przejmuje się wszystkimi możliwymi przypadkami skrajnymi (jeśli używasz Uniksa i nie obchodzi Cię pisanie do / 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")
Naprawdę prosty przypadek użycia, gdy nie dbasz o uprawnienia systemu plików (albo nie pracujesz jako root, albo pracujesz jako root i plik należy do administratora):
#!/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 : Powinno to być używane zamiast zaakceptowanej odpowiedzi jako minimum we wszystkich przypadkach, aby mieć pewność, że aktualizacja jest niepodzielna, a współbieżni czytelnicy nie zobaczą obciętych plików. Jak wspomniałem powyżej, utworzenie pliku Temp w tym samym katalogu co edytowany plik jest tutaj ważne, aby uniknąć tłumaczenia operacji mv na różnych urządzeniach na operacje cp, jeśli / tmp jest zamontowany na innym urządzeniu. Wywołanie fdatasync to dodatkowa warstwa paranoi, ale spowoduje to spadek wydajności, więc pominąłem to w tym przykładzie, ponieważ nie jest powszechnie praktykowane.
Tak naprawdę nie ma sposobu na edycję plików w miejscu. To, co zwykle robisz, kiedy możesz sobie z tym poradzić (tj. Jeśli pliki nie są zbyt duże), to wczytujesz plik do pamięci ( File.read
), wykonujesz podstawienia na odczytanym ciągu ( String#gsub
), a następnie zapisujesz zmieniony ciąg z powrotem do plik ( File.open
, File#write
).
Jeśli pliki są wystarczająco duże, aby było to niewykonalne, to co musisz zrobić, to czytać plik w fragmentach (jeśli wzór, który chcesz zastąpić, nie obejmuje wielu linii, to jeden fragment zwykle oznacza jedną linię - możesz użyć File.foreach
do czytaj plik wiersz po wierszu), a dla każdej porcji wykonaj na nim podstawienie i dołącz go do pliku tymczasowego. Kiedy skończysz iterować plik źródłowy, zamknij go i użyj, FileUtils.mv
aby zastąpić go plikiem tymczasowym.
Innym podejściem jest użycie edycji w miejscu w Rubim (nie z wiersza poleceń):
#!/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
Jeśli nie chcesz tworzyć kopii zapasowej, zmień '.bak'
na ''
.
read
) pliku. Jest skalowalny i powinien być bardzo szybki.
To działa dla mnie:
filename = "foo"
text = File.read(filename)
content = text.gsub(/search_regexp/, "replacestring")
File.open(filename, "w") { |file| file << content }
Oto rozwiązanie umożliwiające znajdowanie / zastępowanie we wszystkich plikach w danym katalogu. Zasadniczo wziąłem odpowiedź udzieloną przez sepp2k i rozszerzyłem ją.
# 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) }
Jeśli musisz wykonać podstawienia w granicach linii, użycie ruby -pi -e
nie zadziała, ponieważ p
przetwarza jedną linię na raz. Zamiast tego zalecam następujące czynności, chociaż może się to nie udać w przypadku pliku o wielu GB:
ruby -e "file='translation.ja.yml'; IO.write(file, (IO.read(file).gsub(/\s+'$/, %q('))))"
Poszukuje białych znaków (potencjalnie zawierających nowe linie), po których następuje cudzysłów, w którym to przypadku pozbywa się białych znaków. To %q(')
tylko fantazyjny sposób cytowania znaku cudzysłowu.
Tutaj alternatywa dla one linera od jima, tym razem w scenariuszu
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(ARGV[-2],ARGV[-1]))}
Zapisz go w skrypcie, np. Replace.rb
Zaczynasz w wierszu poleceń z
replace.rb *.txt <string_to_replace> <replacement>
* .txt można zastąpić innym zaznaczeniem lub niektórymi nazwami plików lub ścieżkami
podzielony, aby móc wyjaśnić, co się dzieje, ale nadal jest wykonywalny
# 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
EDYTUJ: jeśli chcesz użyćwyrażenia regularnego użyj tego Oczywiście jest to tylko do obsługi stosunkowo małych plików tekstowych, bez gigabajtowych potworów
ARGV[0..-3].each{|f| File.write(f, File.read(f).gsub(/#{ARGV[-2]}/,ARGV[-1]))}
File.read
należy złagodzić informacjami ze stackoverflow.com/a/25189286/128421, wyjaśniającymi, dlaczego siorbanie dużych plików jest złe. Również zamiastFile.open(filename, "w") { |file| file << content }
odmian użyjFile.write(filename, content)
.