ファイルをインプレースで変更する方法はありますか?


54

かなり大きなファイル(35Gb)があり、このファイルをその場でフィルター処理したい(つまり、別のファイル用に十分なディスク容量がない)、特にgrepを行い、いくつかのパターンを無視したい-方法はありますか別のファイルを使用せずにこれを行いますか?

foo:たとえば、以下を含むすべての行を除外したいとしましょう...


3
@Tshepang:彼は同じファイルに書き戻したいと思う。
ファヒムミタ

5
「in situ」は「インプレース」を意味するラテン語のフレーズです。文字通り、「インポジション」。
ファヒムミタ

3
その場合、質問はより明確でなければなりません。ファイルをインプレースで変更する方法はありますか?
tshepang

5
@Tshepang、「in situ」はそれを正確に説明するために英語で使用されるかなり一般的なフレーズです-タイトルはかなり自明だと思いました... @Gilles、私はより多くのディスクスペースを待つのが簡単だと思いました!;)
ニム

2
@Nim:そうですね、インプレースインサイチュよりも一般的だと思います。
-tshepang

回答:


41

システムコールレベルでこれが可能になります。プログラムは、ターゲットファイルを切り捨てずに書き込み用に開き、stdinから読み取ったものの書き込みを開始できます。EOFを読み取るとき、出力ファイルは切り捨てられる場合があります。

入力から行をフィルタリングしているため、出力ファイルの書き込み位置は常に読み取り位置より小さくする必要があります。これは、新しい出力で入力を破損しないことを意味します。

ただし、これを行うプログラムを見つけることが問題です。開くときに出力ファイルを切り捨てないdd(1)オプションがありますconv=notruncが、grepの内容の後に元のファイルの内容を残して、最後に切り捨てもしません(などのコマンドを使用grep pattern bigfile | dd of=bigfile conv=notrunc

システムコールの観点からは非常に単純なので、小さなプログラムを作成し、小さな(1MiB)フルループバックファイルシステムでテストしました。それはあなたが望むことをしましたが、あなたは本当に最初にいくつかの他のファイルでこれをテストしたいです。ファイルを上書きすることは常に危険を伴います。

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

私は主に、あなたがそれを試す前に他の人がコメントするためにこれを投稿しています。おそらく他の誰かが、よりテストされた同様のことをするプログラムを知っているでしょう。


何も書かずに逃げることができるかどうかを見たかったのです!:)これでうまくいくと思います!ありがとう!
ニム

2
Cの場合は+1。動作しているように見えますが、潜在的な問題があります。右側が同じファイルに書き込みを行っているため、左側からファイルが読み取られています。2つのプロセスを調整しない限り、同じブロック。コアツールのほとんどは8192を使用する可能性が高いため、ファイルの整合性のために小さいブロックサイズを使用する方が良い場合があります。大きい部分(すべてではない)をメモリに読み込み、小さいブロックに書き込むことができます。nanosleep(2)/ usleep(3)も追加できます。
アルケージュ

4
@Arcege:書き込みはブロック単位で行われません。読み取りプロセスが2バイトを読み取り、書き込みプロセスが1バイトを書き込む場合、最初のバイトのみが変更され、読み取りプロセスは、その時点の元の内容を変更せずにバイト3で読み取りを続行できます。grep読み取るよりも多くのデータを出力しないため、書き込み位置は常に読み取り位置の後ろにある必要があります。読書と同じ速さで書いていても、大丈夫です。grepの代わりにrot13を試してから、もう一度試してください。md5sumの前後で同じことがわかります。
CAMH

6
いいね これは、Joey Hessのmoreutilsに追加する価値があるかもしれません。あなたは使用することができますddが、それは面倒です。
ジル 'SO-悪であるのをやめる'

'grep pattern bigfile | 私はこれをエラーなしで動作させましたが、私が理解していないことは-パターンの内容を他のテキストに置き換える必要はありませんか?次のようなものであってはなりません: 'grep pattern bigfile | / replace-text / bigfileを上書き '
Alexander Mills

20

sedファイルをその場で編集するために使用できます(ただし、これは中間の一時ファイルを作成します):

を含むすべての行を削除するにはfoo

sed -i '/foo/d' myfile

を含むすべての行を保持するにはfoo

sed -i '/foo/!d' myfile

興味深いことに、この一時ファイルは元のファイルと同じサイズである必要がありますか?
ニム

3
はい、だからそれはおそらく良くないでしょう。
pjc50

17
これは、OPが2番目のファイルを作成するため、OPが要求するものではありません。
アルケージュ

1
このソリューションは、「読み取り専用」あなたがいることを意味し、読み取り専用ファイルシステム上で失敗し$HOME ます書き込み可能ではなく、/tmpされる読み取り専用(デフォルト)。たとえば、Ubuntuがあり、回復コンソールを起動した場合、これは一般的なケースです。また、ヒアドキュメント演算子<<<も一時ファイルを書き込むためr / wである必要がある/tmpため、そこでも動作しません。(「d出力を含むこの質問」を参照)strace
syntaxerror

ええ、これも私には機能しません。私が試したすべてのsedコマンドは、現在のファイルを新しいファイルに置き換えます(--in-placeフラグにもかかわらず)。
アレクサンダーミルズ

19

あなたのフィルターコマンドは、少なくともNバイトの入力を読み取る前に出力のバイトNが決して書き込まれないという特性を持つ、私がプレフィックス縮小フィルターと呼ぶものであると仮定します。grepこのプロパティがあります(フィルタリングのためだけであり、一致する行番号を追加するような他のことをしない限り)。このようなフィルターを使用すると、入力に合わせて上書きできます。もちろん、ファイルの先頭の上書きされた部分は永久に失われるため、間違いを犯さないようにする必要があります。

ほとんどのUNIXツールは、ファイルに追加するか切り捨てるかを選択するだけで、上書きする可能性はありません。標準ツールボックスの例外の1つは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に(別途)が開いているcatstdin。終了後grep、いくつかの行を削除した場合、のいずれstdoutかの場所を指すbigfileようになり、このポイントを超えたものを取り除く必要があります。したがって、現在の位置でperlファイル(truncate STDOUT)を切り捨てるコマンド(によって返されるtell STDOUT)。

(これcatは、grep標準入力と標準出力が同じファイルを指している場合に文句を言うGNU用です)。


¹まあ、<>70年代後半に最初からBourneシェルに入っていましたが、最初は文書化されておらず、適切に実装されていませんでしたash1989年の元の実装ではなく、POSIX shリダイレクション演算子(POSIX shksh88常に持っていた90年代前半であるため)は、shたとえば2000年までFreeBSD に追加されなかったため、移植性が15年でした。 oldはおそらくより正確です。また、指定されていない場合のデフォルトのファイル記述子は<>すべてのシェルにありますがksh93、2010年にksh93t +で0から1に変更されたことを除きます(後方互換性とPOSIX準拠に違反します)


2
説明できますperl -e 'truncate STDOUT, tell STDOUT'か?それを含めずに私のために動作します。Perlを使用せずに同じことを達成する方法はありますか?
アーロンブレヌーシュ16

1
@AaronBlenkush、編集を参照してください。
ステファンシャゼラス16

1
絶対に素晴らしい-ありがとう。そのとき、私はそこにいましたが、これを覚えていません。「36歳」の標準の参照は、en.wikipedia.org/wiki/Bourne_shellで言及されていないので、楽しいでしょう。そして、それは何のために使われましたか?SunOS 5.6のバグ修正への参照があります:redirection "<>" fixed and documented (used in /etc/inittab f.i.). これは1つのヒントです。
nealmcb

2
@nealmcb、編集を参照してください。
ステファンシャゼラス

@StéphaneChazelasあなたのソリューションはこの答えと比較してどうですか?どうやら同じことをしているように見えますが、よりシンプルに見えます。
akhan

9

これは古い質問ですが、私には永遠の質問のようです。これまで提案されてきたよりも一般的で明確な解決策が利用可能です。クレジットが支払われるべきクレジット:StéphaneChazelasの<>更新オペレーターについての言及を考慮せずにそれを思いつくかどうかはわかりません。

Bourneシェルで更新のためにファイル開くことは、限られたユーティリティです。シェルでは、ファイルを検索する方法も、新しい長さを設定する方法もありません(古いものより短い場合)。しかし、それは簡単に修正できるので、それがの標準ユーティリティに含まれていないことに簡単に驚きます/usr/bin

これは動作します:

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

これも同様です(Stéphaneへのヒント):

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

(私はGNU grepを使用しています。おそらく彼が答えを書いてから何かが変わったのでしょう。)

ただし、/ usr / bin / ftruncateはありません。Cの数十行については、以下を参照してください。このftruncateユーティリティは、任意のファイル記述子を任意の長さに切り捨てます。デフォルトは標準出力と現在の位置です。

上記のコマンド(最初の例)

  • T更新のためにファイル記述子4を開きます。open(2)と同様に、この方法でファイルを開くと、現在のオフセットが0に配置されます。
  • その後、grepT正常に処理され、シェルはT記述子4 を介して出力をリダイレクトします。
  • ftruncateは記述子4でftruncate(2)を呼び出し、長さを現在のオフセットの値に設定します(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;
}

NB、この方法で使用すると、ftruncate(2)は移植できません。絶対的な一般性のために、最後に書き込まれたバイトを読み取り、ファイルO_WRONLYを再度開き、シークし、バイトを書き込み、閉じます。

質問が5年前であることを考えると、この解決策は自明ではないと言います。execを利用して、新しい記述子と<>演算子を開きます。どちらも難解です。ファイル記述子によってiノードを操作する標準ユーティリティは考えられません。(構文はになる可能性がありますがftruncate >&4、改善されるかどうかはわかりません。)camhの有能で探索的な答えよりもかなり短いです。あなたが私よりもPerlを好まない限り、それはStéphaneのIMOよりも少し明確です。誰かがそれを役に立つと思うことを願っています。

同じことを行う別の方法は、現在のオフセットを報告するlseek(2)の実行可能バージョンです。出力は、一部のLinuxiが提供する/ usr / bin / truncateに使用できます。


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ファイルはバッファに読み込まれるため、35GBファイルを編集するためのgoolソリューションではありません。
グレンジャックマン

2
私はそれが完全なファイルがバッファにロードされることを意味すると考えていました..しかし、おそらくそれが必要とするセクションのみがバッファにロードされます..私はしばらくの間edに興味がありました...私はそれを考えましたin-situ編集を行うことができます... 大きなファイルを試す必要があります...それが機能する場合、それは合理的な解決策ですが、私が書いているように、これがsedに影響を与えたものであると考え始めています(大規模なデータチャンクでの作業から解放された...私は実際に(で始まるスクリプトからストリーミングされた入力を受け付けることができる「エド」ことに気付きました!)ので、その袖をさらにいくつかの興味深いトリックを持っていることがあります。
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

3

私はファイルをメモリマップし、裸のメモリへのchar *ポインタを使用してすべてをインプレースで実行し、ファイルのマップを解除して切り捨てます。


3
+1。ただし、64ビットCPUとOSの広範な可用性により、現在35 GBのファイルでそれが可能になっているからです。まだ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

これは、さらに2つの代わりのみ圧縮を実行するように最適化することができる巧妙な解決策である:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
トッド・オーウェン

0

この質問をグーグルする人の利益のために、正しい答えは、ごくわずかなパフォーマンス向上のためにファイルを破損する危険性のある不明瞭なシェル機能の検索を停止し、代わりにこのパターンのいくつかのバリエーションを使用することです:

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

これが何らかの理由で実行不可能であるという非常にまれな状況でのみ、このページの他の回答を真剣に検討する必要があります(確かに読むのは面白いですが)。私は、2番目のファイルを作成するためのディスクスペースがないというOPの難題がまさにそのような状況であることを認めます。それでも、@ Ed Randallや@Basile Starynkevitchによって提供されるような他のオプションが利用可能です。


1
私は理解し損ねるかもしれませんが、OPが元々要求したこととは何の関係もありません。別名一時ファイル用の十分なディスクスペースを持たないビッグファイルのインライン編集。
Kiwy

@Kiwyこれは、この質問の他の視聴者を対象とした回答です(これまでにほぼ15,000人がいます)。「ファイルをその場で変更する方法はありますか?」という質問 OPの特定のユースケースよりも幅広い関連性があります。
トッドオーウェン

-3

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


3
ファイルが大きく、greppedデータがコマンドラインで許可されている長さを超える場合、これは機能しません。それは、データを破損
Anthonの
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.