head
/tail
将需要迭代几乎整个文件(取决于您作为参数给出的行的位置)。然后将该结果复制到一个新文件并删除旧文件。我不确定是否
sed
会迭代整个文件,但您需要将该结果复制到新文件并删除旧文件。即使-i
(就位)它也会在后台创建一个临时文件,所以同样的情况也适用。
为什么不直接移动指向文件第一行的指针并将其移动到我们想要的行呢?
我们怎么能做这样的事呢?我必须用C 来做吗?还有其他办法吗?
那有意义吗 ??我想错了吗?如果是的话为什么?
答案1
为什么不直接移动指向文件第一行的指针并将其移动到我们想要的行呢?
因为不存在“指向文件第一行的指针”这样的东西。
修改文件的基本操作有:覆盖一定范围的字节(即用相同长度的数据替换一部分)、追加(即在末尾添加)、截断(即从末尾删除)。
大多数文件系统将文件存储在固定大小的块中,但最后一个块可能是部分的。如果修改会改变所修改内容的大小,则无法就地修改数据,除非更改是在末尾或者修改会将数据移动整数个块。将数据移动整个块数只能是巧合,并且没有广泛的接口来做到这一点。
删除文件开头数据的最有效方法是将需要保留的数据复制到新文件中。这正是“做什么”tail -n +42
或“sed '41,$p'
做什么”。
1现代 Linux 系统有一个系统调用来删除文件的一部分:fallocate(fd, FALLOC_FL_COLLAPSE_RANGE, …)
,您可以通过实用程序调用fallocate --collapse-range=…
。还有FALLOC_FL_INSERT_RANGE
和--insert-range
。但它们仅限于块,这使得它们对于文本文件来说几乎毫无用处,并且它们并不适用于所有文件系统。
答案2
吉尔斯比我先一步: 不存在“指向文件第一行的指针”。文件的第一行(文件的开头)始终是文件的第一个字符。 (可能有一些模糊的、单独的应用程序可以识别这样的概念,但在系统级别上没有这样的东西。)
您已经知道的:
命令如
sed '1,6d' filename
sed -n '7,$p' filename
tail -n +7 filename
(可能还有其他变体)将写入除前 6 行之外的所有行filename
到标准输出。 (当然,他们都阅读了所有文件。)当我们这样做时,
sed -n '1,6p' filename
sed '7,$d' filename
head -n 6 filename
sed '6q' filename
将写入前 6 行filename
到标准输出。前两个可能会也可能不会读取整个文件;最后两个可能不会。
还,
命令输入_文件名>相同的文件名不起作用,如中讨论的关于“>”的警告。
你可能不知道的是:
命令参数 1<>文件名
将打开filename
用于阅读和写作
而不截断(破坏)它。 所以,
sed '1,6d'文件名 1<>相同的文件名可能是您正在寻找的解决方案的第一步。这可能与您将要删除的第一个一样接近中号文件的行“就位”;它将读取该文件并同时覆盖它,而不创建另一个文件。如果中号足够小(或者,具体来说,如果第一个中的字节数中号行足够小),这可能会读取文件的每个块一次并写入每个块一次 - 并且您不能做得比这更好。
只是第一的步?
我创建了这个测试文件:
$ 猫 -n foo 1 个 2 码 3 埃格吉 4 jklmnop 5 qrstuvwxy 6 z0123456789 7 ABCDEFGHIJKLM 8 曾几何时,一个沉闷的午夜,当我软弱疲倦地沉思时, 9 在许多古朴而好奇的被遗忘的知识中—— 10 当我点点头,几乎要打瞌睡时,突然传来敲击声, 11、就像有人轻轻地敲击着我的房门。 12 “有位访客,”我低声说道,“敲击我的房门—— 13、仅此而已,仅此而已。” 14 快速棕色 15 狐狸跳过 16、懒狗。从前 17、在这个沉闷的午夜,
该文件经过精心构建,使得行(包括换行符)的长度为2, 4, 6, 8, 10, 12, 14, 63, 57, 63, 58, 62, 63,16、18、20, 和22 号。请注意,前六行因此包含 2+4+6+8+10+12=42 字节。最后两行包含 20+22 字节,巧合的是(!)也是 42。(总文件大小为 504。)所以,
$ ls -l foo -rw-r--r-- 1我的用户名 我的组名504 五月 18 04:25 富 $ sed '1,6d' foo 1<> foo $ ls -l foo -rw-r--r-- 1我的用户名 我的组名504 五月 18 04:32 富 $ 猫 -n foo 1 ABCDEFGHIJKLM 2 曾几何时,一个沉闷的午夜,当我软弱疲倦地沉思时, 3 在许多古朴而好奇的被遗忘的知识中—— 4 当我点点头,快要打瞌睡时,突然传来敲击声, 5 就像有人轻轻敲击我的房门。 6 “有位访客,”我低声说道,“敲着我的房门—— 7仅此而已,仅此而已。” 8 快速棕色 9 狐狸跳过 10、懒狗。从前 11、在这个沉闷的午夜, 12 懒狗。从前 13、这个午夜沉闷,
好的,好的,前六行已经消失了。原来的第 7 行(“ABCDEFGHIJKLM”)现在是第 1 行。但是,这是什么?文件已从 17 行变为 13 行。应该是 11 (17−6)。最后两行(“懒狗……午夜沉闷”)出现了两次。
这是操作员的陷阱之一1<>
- 如果您不截断输出文件,则最终得到的文件不会比开始时的文件小。具体来说,这里的输出sed '1,6d' foo
是 462 字节(504−42,因为前六行包含 42 字节),因此它会覆盖输出文件的前 462 字节 - 这也是foo
.前 462 个字节foo
除了最后 42 个字节 (504−462) 外,全部都是 — 因此最后两行不会被覆盖。最后两行的两个副本(“懒惰的狗……午夜沉闷”)是 的输出sed
,后面是文件原始内容剩下的一份。
那么,接下来怎么办?
我们现在需要做的就是丢弃文件的最后 42 个字节。碰巧的是,这能只需移动指向文件末尾的指针即可完成。好吧,它实际上不是一个指针;而是一个指针。它是一个整数文件大小 - potAto、potAHto。在过去的 20 或 30 年里,Unix 允许您将文件截断到所需的大小,保持该点之前的数据不变,并丢弃该点之后的数据。
可以执行此操作的古老命令是
dd if=/dev/null bs=462 seek=1 of=foo 2> /dev/null
它从字节 462 开始复制/dev/null
。foo
是的,这有点混乱。执行此功能的较新命令是
truncate -s 462 foo
这可能并不存在于所有系统上; POSIX 没有指定它。
所以,把所有这些放在一起,
#!/bin/sh
filename="$1"
bytes_to_remove=$(sed '6q' "$filename" | wc -c)
total_size=$(stat -c '%s' "$filename")
sed '1,6d' "$filename" 1<> "$filename"
new_size=$((total_size - bytes_to_remove))
truncate -s "$new_size" "$filename"
我们用来wc -c
计算前六行(由 生成)中的字符数sed '6q'
,从文件总大小中减去该字符,然后将文件截断为该大小。您可以使用任何替代命令来输出第一个中号行或最后一行N−M行,您可以将最后一行替换为
dd if=/dev/null bs="$new_size" seek=1 of="$filename" 2> /dev/null
注意事项:
我还没有在文件上测试过这个
- CR-LF 行结尾,或
- 多字节字符,
这些可能会有问题。
答案3
看着尾巴的来源,它确实不是事实上似乎迭代了整个文件。它从末尾开始,向后读取,直到看到正确数量的换行符(加上非终止行中的任何多余内容),记下该位置,然后跳过到该位置,然后转储文件(或管道或输入的数据)。