Чи є спосіб змінити файл на місці?


54

У мене досить великий файл (35Gb), і я хотів би відфільтрувати цей файл in situ (тобто мені не вистачає дискового простору для іншого файлу), зокрема я хочу зірвати і проігнорувати деякі шаблони - чи є спосіб зробити це без використання іншого файлу?

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


3
@Tshepang: Я думаю, він хоче написати назад у той самий файл.
Faheem Mitha

5
"in situ" - латинська фраза, що означає "на місці". Буквально «в положенні».
Faheem Mitha

3
У такому випадку питання повинно бути зрозумілішим, щось подібне, чи існує спосіб змінити файл на місці ?
thepang

5
@Tshepang, "in situ" - це досить поширена фраза, що використовується англійською мовою, щоб точно описати це - я вважав, що заголовок є досить зрозумілим для себе ... @Gilles, я зрозумів, що набагато простіше чекати більше місця на диску! ;)
Нім

2
@Nim: Ну, я думаю, що місце є більш поширеним, ніж in situ .
tshepang

Відповіді:


41

На рівні системного виклику це повинно бути можливим. Програма може відкрити ваш цільовий файл для запису, не обрізуючи його, і почати писати те, що він читає з stdin. Під час читання EOF вихідний файл може бути усічений.

Оскільки ви фільтруєте рядки з вхідних даних, позиція виводу файлу виводу завжди повинна бути меншою, ніж позиція зчитування. Це означає, що ви не повинні пошкоджувати свої дані новим результатом.

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

Оскільки з точки зору системного виклику це дуже просто, я написав невелику програму і протестував її на невеликій (1 Мбіт) файловій системі повного циклу. Він робив те, що хотів, але ви хочете спершу протестувати це за допомогою інших файлів. Перезапис файлу завжди буде ризикованим.

overrite.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

Я здебільшого публікую це для інших, щоб прокоментувати його, перш ніж спробувати. Можливо, хтось ще знає про програму, яка робить щось подібне, що є більш перевіреним.


Я хотів подивитися, чи можу я піти, не написавши щось для цього! :) Гадаю, це зробить трюк! Дякую!
Нім

2
+1 для C; здається, працює, але я бачу потенційну проблему: файл читається з лівого боку в той час, коли право записує в той самий файл, і якщо ви не координуєте два процеси, ви могли б переписати проблеми потенційно на той самий файл блоки. Можливо, для цілісності файлів буде використовувати менший розмір блоку, оскільки більшість основних інструментів, ймовірно, використовуватимуть 8192. Це може сповільнити програму достатньо, щоб уникнути конфліктів (але не може гарантувати). Можливо, прочитайте більші частини в пам'яті (не всі) та запишіть меншими блоками. Можна також додати наноспі (2) / usleep (3).
Арседж

4
@Arcege: Написання не здійснюється блоками. Якщо ваш процес читання прочитав 2 байти, а ваш процес запису пише 1 байт, зміниться лише перший байт, і процес читання може продовжувати читання в байті 3 з початковим вмістом у цьому пункті без змін. Оскільки grepне буде видано більше даних, ніж прочитане, позиція запису завжди повинна бути поза позицією читання. Навіть якщо ви пишете з такою ж швидкістю, як і читання, все одно буде нормально. Спробуйте rot13 з цим замість grep, а потім ще раз. md5sum до і після, і ви побачите його те саме.
camh

6
Приємно. Це може бути цінним доповненням до більшої корисності Джої Гесса . Ви можете використовувати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
Про це не вимагає ОП, оскільки створює другий файл.
Арседж

1
Це рішення буде не в змозі в файлової системі тільки для читання, де «тільки для читання» означає , що ваш $HOME буде мати права на запис, але /tmpбуде доступний тільки для читання (за замовчуванням). Наприклад, якщо у вас є Ubuntu і ви завантажилися на консоль відновлення, це зазвичай так. Крім того, тут <<<не буде працювати оператор документів тут, оскільки він /tmpповинен бути r / w, тому що він також запише тимчасовий файл туди. (див. це запитання, включаючи straceвихід «d»)
синтаксис-помилка

так, це також не допоможе для мене, всі команди 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

З будь-якою оболонкою Борна:

{
  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команда, яка обрізає файл ( truncate STDOUT) у поточному положенні (як повернуто tell STDOUT).

( catє для GNU, grepяке інакше скаржиться, якщо stdin і stdout вказують на один і той же файл).


Добре, хоча <>він знаходився в обороті Борна з самого початку в кінці сімдесятих років, він спочатку був недокументований і не був належним чином реалізований . Це було не в оригінальній реалізації ashз 1989 року, і, хоча це shоператор перенаправлення POSIX (з початку 90-х, як shце засновано на POSIX , ksh88який завжди був у ньому), він не був доданий до FreeBSD, shнаприклад, до 2000 року, тому портативно 15 років старий , мабуть, більш точний. Також зауважте, що дескриптор файлу за замовчуванням, коли його не вказано, є <>у всіх оболонках, за винятком того, що ksh93він змінився з 0 на 1 у ksh93t + у 2010 році (порушення зворотної сумісності та відповідності 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 . Про кілька десятків рядків С ви можете подивитися нижче. Ця утиліта ftruncate обрізає довільний дескриптор файлу довільної довжини, дефолтуючи до стандартного виводу та поточного положення.

Наведена вище команда (1-й приклад)

  • відкриває дескриптор файлів 4 Tдля оновлення. Так само, як і у відкритому (2), відкриття файлу таким чином розміщує поточне зміщення у 0.
  • Потім grep обробляється Tнормально, і оболонка перенаправляє свій вихід на Tдескриптор 4.
  • ftruncate виклики ftruncate (2) на дескрипторі 4, встановивши довжину до значення поточного зміщення (саме там, де залишився греп ).

Потім нижня частина корпусу виходить, закриваючи дескриптор 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 , щоб відкрити новий дескриптор, і <>оператор, обидва з яких є таємними. Я не можу придумати стандартну утиліту, яка маніпулює inode дескриптором файлів. (Синтаксис міг бути ftruncate >&4, але я не впевнений, що поліпшення.) Це значно коротше, ніж компетентний дослідницький відповідь Кем. Це просто трохи зрозуміліше, ніж Стефан, ІМО, якщо ви не любите Перла більше, ніж я. Сподіваюся, хтось вважає це корисним.

Іншим способом зробити те саме було б виконувана версія lseek (2), яка повідомляє про поточне зміщення; вихід може бути використаний для / usr / bin / усікання , який надають деякі 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 ГБ файлів, оскільки файл зчитується в буфер.
glenn jackman

2
Я думав, що це означає, що повний файл буде завантажений у буфер .. але, можливо, лише той розділ, який він потребує, завантажується в буфер. Мені цікаво про Ед деякий час ... Я думав, що це могло б зробити in-situ редагування ... Мені просто доведеться спробувати великий файл ... Якщо він працює, це розумне рішення, але, як я пишу, я починаю думати, що це може бути натхненним sed ( звільнений від роботи з великими фрагментами даних ... Я помітив, що "ed" може насправді приймати потоковий вхід зі сценарію (з префіксом !), тому у нього може бути ще кілька цікавих хитрощів
Peter.O

Я впевнений, що операція запису в edусікає файл і переписує його. Таким чином, це не змінить даних на диску на місці, як бажає ОП. Крім того, він не може працювати, якщо файл занадто великий, щоб завантажити його в пам'ять.
Нік Маттео

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

3

Я пам’ятаю файл пам’яті, виконую все на місці, використовуючи покажчики char * для оголеної пам’яті, а потім скасовуйте файл та обрізайте його.


3
+1, але лише тому, що широка доступність 64-бітних процесорів та ОС дозволяє це робити з файлом об'ємом 35 ГБ зараз. Тим, хто все ще на 32-бітних системах (переважна більшість навіть аудиторії цього сайту, я підозрюю) не зможе використовувати це рішення.
Воррен Янг

2

Не зовсім in situ, але це може бути корисним у подібних обставинах.
Якщо проблема з диском є ​​проблемою, спершу стисніть файл (оскільки це текст, це призведе до величезного зменшення), а потім використовуйте 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
Я можу помилково зрозуміти, але не має нічого спільного з тим, про що ОР оригінально попросив. aka inline редагування bigfile без достатньої кількості дискового простору для тимчасового файлу.
Ківі

@Kiwy Це відповідь, спрямована на інших глядачів цього питання (яких до цього часу було майже 15 000). Питання "Чи є спосіб змінити файл на місці?" має більш широке значення, ніж конкретний випадок використання ОП.
Тодд Оуен

-3

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


3
Це не працює, якщо файл великий, а greppedдані перевищують довжину, яку дозволяє командний рядок. Потім він пошкоджує дані
Антон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.