Есть ли способ изменить файл на месте?

54

У меня довольно большой файл (35 ГБ), и я хотел бы отфильтровать этот файл на месте (т. Е. У меня недостаточно места на диске для другого файла), в частности, я хочу grep и игнорировать некоторые шаблоны - есть ли способ сделать это без использования другого файла?

Допустим, я хочу отфильтровать все строки, содержащие, foo:например ...

Nim
источник
3
@Tshepang: я думаю, что он хочет написать обратно в тот же файл.
Фахим Митха
5
«in situ» - латинская фраза, означающая «на месте». Буквально «в положении».
Фахим Митха
3
В этом случае вопрос должен быть более понятным, например, есть ли способ изменить файл на месте ?
Чепанг
5
@Tshepang, «in situ» - довольно распространенная фраза, используемая в английском языке для точного описания этого - я думал, что название довольно само собой разумеющееся ... @ Жиль, я подумал так же, проще ждать больше дискового пространства! ;)
Ним
2
@Nim: Ну, я думаю, что на месте чаще, чем на месте .
tshepang

Ответы:

41

На уровне системных вызовов это должно быть возможно. Программа может открыть целевой файл для записи, не обрезая его, и начать писать то, что читает со стандартного ввода. При чтении EOF выходной файл может быть обрезан.

Поскольку вы фильтруете строки из входных данных, позиция записи выходного файла всегда должна быть меньше позиции чтения. Это означает, что вы не должны портить свой ввод новым выводом.

Однако, поиск программы, которая делает это, является проблемой. dd(1)имеет опцию conv=notrunc, которая не усекает выходной файл при открытии, но также не усекает в конце, оставляя исходное содержимое файла после содержимого grep (с помощью команды вроде grep pattern bigfile | dd of=bigfile conv=notrunc)

Поскольку это очень просто с точки зрения системного вызова, я написал небольшую программу и протестировал ее на небольшой (1 МБ) файловой системе с полной обратной связью. Он сделал то, что хотел, но вы действительно хотите сначала проверить это с некоторыми другими файлами. Переписывать файл всегда будет рискованно.

overwrite.c

/* This code is placed in the public domain by camh */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
        int outfd;
        char buf[1024];
        int nread;
        off_t file_length;

        if (argc != 2) {
                fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
                exit(1);
        }
        if ((outfd = open(argv[1], O_WRONLY)) == -1) {
                perror("Could not open output file");
                exit(2);
        }
        while ((nread = read(0, buf, sizeof(buf))) > 0) {
                if (write(outfd, buf, nread) == -1) {
                        perror("Could not write to output file");
                        exit(4);
                }
        }
        if (nread == -1) {
                perror("Could not read from stdin");
                exit(3);
        }
        if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
                perror("Could not get file position");
                exit(5);
        }
        if (ftruncate(outfd, file_length) == -1) {
                perror("Could not truncate file");
                exit(6);
        }
        close(outfd);
        exit(0);
}

Вы бы использовали его как:

grep pattern bigfile | overwrite bigfile

В основном я публикую это, чтобы другие могли прокомментировать, прежде чем вы попробуете. Возможно, кто-то еще знает о программе, которая делает что-то подобное, что более проверено.

CAMH
источник
Я хотел посмотреть, смогу ли я уйти, не написав что-нибудь для этого! :) Я думаю, это сработает! Спасибо!
Ним
2
+1 за C; кажется, работает, но я вижу потенциальную проблему: файл читается с левой стороны в то время, как правая записывает в тот же файл, и если вы не координируете два процесса, у вас могут возникнуть проблемы перезаписи на одной и той же блоки. Для целостности файла может быть лучше использовать меньший размер блока, так как большинство основных инструментов, вероятно, будут использовать 8192. Это может замедлить программу достаточно, чтобы избежать конфликтов (но не может гарантировать). Может быть, прочитать большие части в память (не все) и записать меньшими блоками. Также можно добавить наносон (2) / уснуть (3).
Arcege
4
@Arcege: запись не делается в блоках. Если ваш процесс чтения прочитал 2 байта, а ваш процесс записи записал 1 байт, изменится только первый байт, и процесс чтения может продолжить чтение с байта 3 с исходным содержимым в этой точке без изменений. Поскольку grepне будет выводиться больше данных, чем считывается, позиция записи всегда должна быть позади позиции чтения. Даже если вы пишете с той же скоростью, что и чтение, все равно все будет в порядке. Попробуйте с помощью rot13 вместо grep, а затем снова. md5sum до и после, и вы увидите то же самое.
Camh
6
Приятно. Это может быть ценным дополнением к moreutils Джои Хесса . Вы можете использоватьdd , но это громоздко.
Жиль "ТАК - перестань быть злым"
'grep pattern bigfile | переписать bigfile '- я получил это без ошибок, но я не понимаю, не является ли это требованием заменить то, что в шаблоне, каким-то другим текстом? поэтому не должно быть что-то вроде: 'grep pattern bigfile | перезаписать / заменить текст / bigfile '
Александр Миллс
20

Вы можете использовать sedдля редактирования файлов на месте (но это создает промежуточный временный файл):

Удалить все строки, содержащие foo:

sed -i '/foo/d' myfile

Сохранить все строки, содержащие foo:

sed -i '/foo/!d' myfile
кендырь
источник
Интересно, будет ли этот временный файл иметь тот же размер, что и оригинал?
Ним
3
Да, так что это, вероятно, не хорошо.
pjc50
17
Это не то, что запрашивает OP, поскольку он создает второй файл.
Arcege
1
Это решение не сработает в файловой системе только для чтения, где «только для чтения» означает, что ваша $HOME будет доступна для записи, но /tmpбудет только для чтения (по умолчанию). Например, если у вас Ubuntu и вы загрузились в консоль восстановления, это обычно так. Кроме того, оператор here-document там <<<также не будет работать, поскольку он /tmpдолжен быть r / w, потому что он также запишет туда временный файл. (см. этот вопрос с straceвыводом «а»)
syntaxerror
да, у меня это тоже не сработает, все команды sed, которые я пробовал, заменят текущий файл новым файлом (несмотря на флаг --in-place).
Александр Миллс
19

Я предполагаю, что ваша команда фильтра - это то, что я назову фильтром сжатия префикса , у которого есть свойство, что байт N в выходных данных никогда не записывается прежде, чем прочитает по крайней мере N байтов ввода. grepимеет это свойство (при условии, что он только фильтрует и не выполняет никаких других действий, таких как добавление номеров строк для совпадений). С таким фильтром вы можете перезаписать ввод по мере продвижения. Конечно, вы должны быть уверены, что не допустили ошибок, так как перезаписанная часть в начале файла будет потеряна навсегда.

Большинство инструментов Unix предоставляют выбор добавления файла или его усечения, без возможности его перезаписи. Единственное исключение в стандартном наборе инструментов - ddэто указание не обрезать свой выходной файл. Таким образом, план , чтобы отфильтровать команду в dd conv=notrunc. Это не меняет размер файла, поэтому мы также берем длину нового содержимого и усекаем файл до этой длины (снова с помощью dd). Обратите внимание, что эта задача по своей сути не является надежной - если возникает ошибка, вы самостоятельно.

export LC_ALL=C
n=$({ grep -v foo <big_file |
      tee /dev/fd/3 |
      dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n

Вы можете написать грубый эквивалент Perl. Вот быстрая реализация, которая не пытается быть эффективной. Конечно, вы можете захотеть выполнить первоначальную фильтрацию непосредственно на этом языке.

grep -v foo <big_file | perl -e '
  close STDOUT;
  open STDOUT, "+<", $ARGV[0] or die;
  while (<STDIN>) {print}
  truncate STDOUT, tell STDOUT or die
' big_file
Жиль "ТАК - перестань быть злым"
источник
16

С любой Bourne-подобной оболочкой:

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile

Почему-то кажется, что люди забывают об этом 40-летнем и стандартном операторе перенаправления чтения + записи.

Мы открываем bigfileв режиме чтения + записи и (что здесь наиболее важно) без усечения, в stdoutто время bigfileкак открыт (отдельно) на cat's stdin. После grepзавершения, и если он удалил несколько строк, stdoutтеперь указывает где-то внутри bigfile, нам нужно избавиться от того, что находится за этой точкой. Следовательно perlкоманда, которая усекает file ( truncate STDOUT) в текущей позиции (как возвращено tell STDOUT).

( catэто для GNU, grepкоторый иначе жалуется, если stdin и stdout указывают на один и тот же файл).


¹ Ну, хотя <>он был в оболочке Bourne с самого начала в конце семидесятых, он изначально не был документирован и не реализован должным образом . Его не было в первоначальной реализации ash1989 года, и, хотя он является shоператором перенаправления POSIX (с начала 90-х, поскольку POSIX shоснован на том, ksh88что всегда имелось), он не был добавлен во FreeBSD, shнапример, до 2000 года, так что 15 лет назад старый , вероятно, более точный. Также обратите внимание, что дескриптор файла по умолчанию, если он не указан, присутствует <>во всех оболочках, за исключением того, что ksh93в 2010 году он был изменен с 0 на 1 в ksh93t + (что нарушает обратную совместимость и соответствие POSIX).

Стефан Шазелас
источник
2
Можешь объяснить perl -e 'truncate STDOUT, tell STDOUT'? Это работает для меня без учета этого. Есть ли способ достичь того же, не используя Perl?
Аарон Бленкуш
1
@AaronBlenkush, см. Редактировать.
Стефан Шазелас
1
Абсолютно блестящий - спасибо. Я был там тогда, но не помню этого ... Ссылка на стандарт "36 лет" была бы забавной, так как она не упомянута на en.wikipedia.org/wiki/Bourne_shell . И для чего он использовался? Я вижу ссылку на исправление ошибки в SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). один совет.
nealmcb
2
@nealmcb, см. редактировать.
Стефан
@ StéphaneChazelas Как ваше решение по сравнению с этим ответом ? Это, очевидно, делает то же самое, но выглядит проще.
ахан
9

Несмотря на то, что это старый вопрос, мне кажется, что это постоянный вопрос, и доступно более общее, более четкое решение, чем предполагалось до сих пор. Кредит, где кредит должен: я не уверен, что я придумал бы это без учета упоминания Стефана Шазеласа об <>операторе обновлений.

Открытие файла для обновления в оболочке Bourne имеет ограниченную полезность. Оболочка не дает возможности искать файл и не может устанавливать его новую длину (если она короче старой). Но это легко исправить, так легко я удивляюсь, что он не входит в число стандартных утилит в /usr/bin.

Это работает:

$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T; 
     1  foo

Как это (шляпа Стефану):

$ { grep foo T && ftruncate; } 1<>T  && nl T; 
     1  foo

(Я использую GNU grep. Возможно, что-то изменилось, так как он написал свой ответ.)

За исключением того, что у вас нет / usr / bin / ftruncate . Для пары десятков строк C, вы можете увидеть ниже. Эта утилита ftruncate усекает произвольный дескриптор файла до произвольной длины, по умолчанию используется стандартный вывод и текущая позиция.

Приведенная выше команда (1-й пример)

  • открывает дескриптор файла 4 Tдля обновления. Как и в случае с open (2), открытие файла таким образом устанавливает текущее смещение в 0.
  • Затем grep обрабатывается Tнормально, и оболочка перенаправляет свои выходные данные Tчерез дескриптор 4.
  • ftruncate вызывает ftruncate (2) для дескриптора 4, устанавливая длину в значение текущего смещения (именно там, где его оставил grep ).

Затем подоболочка завершается, закрывая дескриптор 4. Вот ftruncate :

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main( int argc, char *argv[] ) {
  off_t i, fd=1, len=0;
  off_t *addrs[2] = { &fd, &len };

  for( i=0; i < argc-1; i++ ) {
    if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
      err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
    }
  }

  if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
    err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
  }


  if( 0 != ftruncate((int)fd, len) ) {
    err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
  }

  return EXIT_SUCCESS;
}

Примечание: ftruncate (2) является непереносимым при использовании таким способом. Для абсолютной общности прочитайте последний записанный байт, снова откройте файл O_WRONLY, найдите, запишите байт и закройте.

Учитывая, что этому вопросу 5 лет, я собираюсь сказать, что это решение неочевидно. Для открытия нового дескриптора используется exec , а <>оператор - оба являются загадочными. Я не могу вспомнить стандартную утилиту, которая манипулирует индексом по дескриптору файла. (Синтаксис может быть ftruncate >&4, но я не уверен, что это улучшение.) Это значительно короче, чем компетентный, исследовательский ответ Camh. Это немного яснее, чем у Стефана, ИМО, если вы не любите Perl больше, чем я. Я надеюсь, что кто-то найдет это полезным.

Другим способом сделать то же самое может быть исполняемая версия lseek (2), которая сообщает о текущем смещении; вывод может быть использован для / usr / bin / truncate , который предоставляют некоторые Linuxi.

Джеймс К. Лоуден
источник
5

ed вероятно правильный выбор для редактирования файла на месте:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
Гленн Джекман
источник
Мне нравится идея, но если разные edверсии ведут себя по-разному ..... это из man ed(GNU Ed 1.4) ...If invoked with a file argument, then a copy of file is read into the editor's buffer. Changes are made to this copy and not directly to file itself.
Peter.O
@fred, если вы подразумеваете, что сохранение изменений не повлияет на указанный файл, вы ошиблись. Я интерпретирую эту цитату, чтобы сказать, что ваши изменения не отражены, пока вы не сохраните их. Я признаю, что edэто не решение gool для редактирования 35-гигабайтных файлов, поскольку файл считывается в буфер.
Гленн Джекман
2
Я думал, что это означает, что полный файл будет загружен в буфер ... но, возможно, только те разделы, которые ему нужны, загружены в буфер ... Мне было любопытно, какое-то время ed ... Я думал, что это мог бы сделать редактирование на месте ... Мне просто нужно попробовать большой файл ... Если это работает, это разумное решение, но когда я пишу, я начинаю думать, что это может быть то, что вдохновило Sed ( Освобожден от работы с большими порциями данных ... Я заметил, что 'ed' может фактически принимать потоковый ввод из скрипта (с префиксом !), поэтому у него может быть несколько более интересных трюков
Peter.O
Я почти уверен, что операция записи edусекает файл и переписывает его. Так что это не изменит данные на диске на месте, как того пожелает OP. Кроме того, он не может работать, если файл слишком велик для загрузки в память.
Ник Маттео
5

Вы можете использовать файловый дескриптор bash для чтения / записи, чтобы открыть ваш файл (перезаписать его in-situ), затем sedи truncate... но, конечно, никогда не допускайте, чтобы ваши изменения превышали объем прочитанных данных. ,

Вот скрипт (использует: переменная bash $ BASHPID)

# Create a test file
  echo "going abc"  >junk
  echo "going def" >>junk
  echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
  exec 3<> junk  
#
# Choose a unique filename to hold the new file size  and the pid 
# of the semi-asynchrounous process to which 'tee' streams the new file..  
  [[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER" 
  f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
  [[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ... 
#  to modify the file in-situ, and to count the bytes  
  <junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process, 
# so 'wait' doesn't work... but wait we must...  
  pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}  
  # $f_pid_size may initially contain only the pid... 
  # get the size when pid termination is assured
  while [[ "$pid" != "" ]] ; do
    if ! kill -0 "$pid" 2>/dev/null; then
       pid=""  # pid has terminated. get the byte count
       pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
    fi
  done
  rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
  exec 3>&- # close fd 3.
  newsize=$(cat newsize)
  echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
 truncate -s $newsize junk
 echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
exit

Вот тестовый вывод

# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes

# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes

# NEW (truncated) file
abc
def
# 2 lines, 8 bytes
Peter.O
источник
3

Я бы отображал файл в памяти, делал все на месте, используя указатели char *, чтобы освободить память, а затем разархивировать файл и обрезать его.

bmcnett
источник
3
+1, но только потому, что широко распространенная доступность 64-битных процессоров и операционных систем позволяет сделать это с файлом размером 35 ГБ. Те, кто по-прежнему работает на 32-битных системах (я подозреваю, что подавляющее большинство аудитории этого сайта) не смогут использовать это решение.
Уоррен Янг
2

Не совсем на месте, но - это может быть полезно в подобных обстоятельствах.
Если дисковое пространство является проблемой, сначала сожмите файл (так как это текст, это даст огромное сокращение), затем используйте sed (или grep, или что-то еще) обычным способом в середине конвейера распаковки / сжатия.

# Reduce size from ~35Gb to ~6Gb
$ gzip MyFile

# Edit file, creating another ~6Gb file
$ gzip -dc <MyFile.gz | sed -e '/foo/d' | gzip -c >MyEditedFile.gz
Эд Рэндалл
источник
2
Но, безусловно, gzip записывает сжатую версию на диск, прежде чем заменять ее сжатой версией, поэтому вам нужно как минимум столько дополнительного места, в отличие от других опций. Но это безопаснее, если у вас есть место (которого у меня нет ...)
nealmcb
Это умное решение, которое может быть дополнительно оптимизировано для выполнения только одного сжатия вместо двух:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Тодд Оуэн,
0

В интересах любого, кто ищет этот вопрос, правильный ответ - прекратить поиск неясных функций оболочки, которые рискуют испортить ваш файл из-за незначительного прироста производительности, и вместо этого использовать некоторые варианты этого шаблона:

grep "foo" file > file.new && mv file.new file

Только в крайне необычной ситуации, когда это по какой-то причине неосуществимо, вы должны серьезно рассмотреть любые другие ответы на этой странице (хотя их, безусловно, интересно читать). Я признаю, что загадка ОП, связанная с отсутствием дискового пространства для создания второго файла, является именно такой ситуацией. Хотя даже тогда есть и другие доступные варианты, например, предоставленные @Ed Randall и @Basile Starynkevitch.

Тодд Оуэн
источник
1
Я могу неправильно понять, но не имеет ничего общего с тем, что первоначально спросил ОП. встроенное редактирование bigfile без достаточного места на диске для временного файла.
Киви
@Kiwy Это ответ, предназначенный для других зрителей этого вопроса (из которых до сих пор было почти 15 000). Вопрос "Есть ли способ изменить файл на месте?" имеет более широкое значение, чем конкретный вариант использования ОП.
Тодд Оуэн
-3

echo -e "$(grep pattern bigfile)" >bigfile

user54620
источник
3
Это не работает, если файл большой и greppedданные превышают длину, разрешенную командной строкой. Затем он искажает данные
Anthon