有没有办法就地修改文件?

有没有办法就地修改文件?

我有一个相当大的文件(35Gb),我想就地过滤这个文件(即我没有足够的磁盘空间来容纳另一个文件),特别是我想 grep 并忽略一些模式 - 有没有办法在不使用其他文件的情况下执行此操作?

假设我想过滤掉foo:包含以下内容的所有行...

答案1

在系统调用级别这应该是可能的。程序可以打开目标文件进行写入,而无需截断它,并开始写入从标准输入读取的内容。当读取EOF时,输出文件可以被截断。

由于您要过滤输入中的行,因此输出文件写入位置应始终小于读取位置。这意味着您不应该用新的输出破坏您的输入。

然而,找到一个能做到这一点的程序是个问题。dd(1)具有在打开时不截断输出文件的选项conv=notrunc,但它也不会在末尾截断,将原始文件内容保留在 grep 内容之后(使用类似 的命令grep pattern bigfile | dd of=bigfile conv=notrunc

由于从系统调用的角度来看它非常简单,因此我编写了一个小程序并在小型(1MiB)完整环回文件系统上对其进行了测试。它完成了您想要的操作,但您确实想先用其他一些文件来测试它。覆盖文件总是有风险的。

覆盖.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

对于任何类似 Bourne 的 shell:

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

出于某种原因,人们似乎倾向于忘记那个 40 岁的人标准读+写重定向运算符。

我们bigfile以读+写模式打开,并且(这里最重要的是)在while上打开(单独)stdout时没有截断。终止后,如果它删除了一些行,现在指向 内的某个位置,我们需要删除该点之外的内容。因此,该命令在当前位置(由 所返回)截断文件 ( ) 。bigfilecatstdingrepstdoutbigfileperltruncate STDOUTtell STDOUT

(这cat是针对 GNU 的grep,否则如果 stdin 和 stdout 指向同一个文件,则会抱怨)。


1 好吧,虽然<>从七十年代末开始就在 Bourne shell 中,但它最初是无证且未正确实施。它不在ash1989 年的原始实现中,虽然它是一个 POSIXsh重定向运算符(自 90 年代初以来,POSIX一直sh基于ksh88它),但它没有添加到 FreeBSD 中sh,因此可移植15岁可能更准确。另请注意,未指定时的默认文件描述符在所有 shell 中均为 0,但ksh932010 年 ksh93t+ 中的文件描述符从 0 更改为 1(破坏了向后兼容性和 POSIX 合规性)

答案3

您可以使用sed就地编辑文件(但这确实会创建一个中间临时文件):

要删除包含以下内容的所有行foo

sed -i '/foo/d' myfile

保留包含以下内容的所有行foo

sed -i '/foo/!d' myfile

答案4

尽管这是一个老问题,但在我看来,这是一个长期存在的问题,并且比迄今为止所建议的更普遍、更清晰的解决方案是可行的。值得赞扬的地方:如果不考虑 Stéphane Chazelas 提到的<>更新操作符, 我不确定我是否会想出它。

打开文件用于更新在 Bourne shell 中的用途有限。 shell 无法让您查找文件,也无法设置其新长度(如果比旧长度短)。但这很容易解决,所以我很惊讶它不属于/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 代码,您可以,请参见下文。这截断实用程序将任意文件描述符截断为任意长度,默认为标准输出和当前位置。

上面的命令(第一个例子)

  • 打开文件描述符 4T进行更新。与 open(2) 一样,以这种方式打开文件会将当前偏移量定位为 0。
  • grep然后正常处理T,shell 将其输出重定向到Tvia 描述符 4。
  • 截断在描述符 4 上调用 ftruncate(2),将长度设置为当前偏移量的值(确切地说,其中grep留下它)。

然后子 shell 退出,关闭描述符 4。这是截断

#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 年历史了,我想说这个解决方案不是显而易见的。它利用了执行打开一个新的描述符和<>操作符,这两个都是晦涩难懂的。我想不出一个通过文件描述符操作 inode 的标准实用程序。 (语法可以是ftruncate >&4,但我不确定是否有改进。)它比 camh 的有能力的探索性答案要短得多。在我看来,它比 Stéphane 的更清晰一点,除非你比我更喜欢 Perl。我希望有人觉得它有用。

执行相同操作的另一种方法是报告当前偏移量的 lseek(2) 的可执行版本;输出可用于/usr/bin/截断,某些 Linuxi 提供。

相关内容