我有一个相当大的文件(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
时没有截断。终止后,如果它删除了一些行,现在指向 内的某个位置,我们需要删除该点之外的内容。因此,该命令在当前位置(由 所返回)截断文件 ( ) 。bigfile
cat
stdin
grep
stdout
bigfile
perl
truncate STDOUT
tell STDOUT
(这cat
是针对 GNU 的grep
,否则如果 stdin 和 stdout 指向同一个文件,则会抱怨)。
1 好吧,虽然<>
从七十年代末开始就在 Bourne shell 中,但它最初是无证且未正确实施。它不在ash
1989 年的原始实现中,虽然它是一个 POSIXsh
重定向运算符(自 90 年代初以来,POSIX一直sh
基于ksh88
它),但它没有添加到 FreeBSD 中sh
,因此可移植15岁可能更准确。另请注意,未指定时的默认文件描述符在所有 shell 中均为 0,但ksh93
2010 年 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 代码,您可以,请参见下文。这截断实用程序将任意文件描述符截断为任意长度,默认为标准输出和当前位置。
上面的命令(第一个例子)
- 打开文件描述符 4
T
进行更新。与 open(2) 一样,以这种方式打开文件会将当前偏移量定位为 0。 - grep然后正常处理
T
,shell 将其输出重定向到T
via 描述符 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 提供。