Bash读/写文件描述符--定位到文件开头

22

我试图在Bash中使用读/写文件描述符,以便稍后可以删除文件描述符所指向的文件,例如:

F=$(mktemp)
exec 3<> "$F"
rm -f "$F"

echo "Hello world" >&3
cat <&3

但是cat命令没有输出。如果我使用不同的文件描述符进行读写,就可以实现我的目标:

F=$(mktemp)
exec 3> "$F"
exec 4< "$F"
rm -f "$F"

echo "Hello world" >&3
cat <&4

它输出Hello world

我怀疑当你从写入模式切换到读取模式时,bash不会自动将文件描述符寻找到开头。下面的bash和python代码组合证实了这一点:

fdrw.sh

exec 3<> tmp
rm tmp

echo "Hello world" >&3
exec python fdrw.py

fdrw.py

import os  

f = os.fdopen(3)
print f.tell()
print f.read()

这将给出:

$ bash fdrw.sh
12

$ # This is the prompt reappearing

有没有一种仅使用bash就可以实现我想要的功能的方法?


1
为什么在读写文件之前要删除它? - unhammer
6
在Unix系统中,当你删除一个文件时,只有当所有打开的文件描述符关闭后该文件才会被真正删除。因此,打开临时文件后立即将其删除是常见做法,因为这可以确保没有其他进程恶意更改该文件,并且在你的进程关闭文件或退出后该文件也会被关闭。 - telotortium
1
为什么你不喜欢使用分离的读写描述符的方法?这似乎是最简单的方式。 - Kelvin
9个回答

12

我找到了一种在bash中实现它的方法,但它依赖于一个晦涩的特性exec < /dev/stdin,它可以根据http://linux-ip.net/misc/madlug/shell-tips/tip-1.txt重新定位标准输入文件描述符:

F=$(mktemp)
exec 3<> "$F"
rm -f "$F"

echo "Hello world" >&3
{ exec < /dev/stdin; cat; } <&3

写描述符不会受到影响,因此您仍然可以在将输出附加到第3个描述符之前添加输出。

可悲的是,这只在Linux下工作,而不是在MacOS(BSD)下,即使使用最新的bash版本也是如此。因此,它似乎不太便携。


请查看我下面扩展的测试。 - anthony

10

如果你想要在Bash文件描述符上进行查找,你可以使用子进程,因为它会继承父进程的文件描述符。这是一个示例C程序。

seekfd.c

#define _FILE_OFFSET_BITS 64
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    /* Arguments: fd [offset [whence]]
     * where
     * fd: file descriptor to seek
     * offset: number of bytes from position specified in whence
     * whence: one of
     *  SEEK_SET (==0): from start of file
     *  SEEK_CUR (==1): from current position
     *  SEEK_END (==2): from end of file
     */
    int fd;
    long long scan_offset = 0;
    off_t offset = 0;
    int whence = SEEK_SET;
    int errsv; int rv;
    if (argc == 1) {
        fprintf(stderr, "usage: seekfd fd [offset [whence]]\n");
        exit(1);
    }
    if (argc >= 2) {
        if (sscanf(argv[1], "%d", &fd) == EOF) {
            errsv = errno;
            fprintf(stderr, "%s: %s\n", argv[0], strerror(errsv));
            exit(1);
        }
    }
    if (argc >= 3) {
        rv = sscanf(argv[2], "%lld", &scan_offset);
        if (rv == EOF) {
            errsv = errno;
            fprintf(stderr, "%s: %s\n", argv[0], strerror(errsv));
            exit(1);
        }
        offset = (off_t) scan_offset;
    }
    if (argc >= 4) {
        if (sscanf(argv[3], "%d", &whence) == EOF) {
            errsv = errno;
            fprintf(stderr, "%s: %s\n", argv[0], strerror(errsv));
            exit(1);
        }
    }

    if (lseek(fd, offset, whence) == (off_t) -1) {
        errsv = errno;
        fprintf(stderr, "%s: %s\n", argv[0], strerror(errsv));
        exit(2);
    }

    return 0;
}

1
我相信会有一个 Perl 命令行可以做同样的事情,并且更具可移植性。 - anthony
@anthony 我不确定"更可移植"的说法。编写符合标准且没有扩展的C程序进行交叉编译比交叉编译Perl解释器要容易得多(在过去,这曾经是我的工作描述之一 - 在一个嵌入式系统商店中,我们针对的硬件不足以本地编译perl;移植Perl解释器并不是一件有趣的事情)。 - Charles Duffy
我想这取决于你的观点。对我来说,可移植意味着不需要为多个系统重新编译程序,而是直接使用它,包括多个Linux、Mac、Solaris等等。但我也能理解你的观点。在我的答案中提供了一个Perl版本的链接(非常简单)。这个版本还可以让你使用“tell”或“truncate”,并且可以使用名称而不是数字。 - anthony

5
尝试更改命令的顺序:
F=$(mktemp tmp.XXXXXX)
exec 3<> "$F"
echo "Hello world" > "$F"
rm -f "$F"

#echo "Hello world" >&3
cat <&3

7
@Dennis 这个解决方案实际上是有效的。猫没有从明显已删除的文件中读取,而是从仍然打开的描述符中读取。即使最后一个(硬)链接已被删除,你仍然可以使用该描述符访问文件的内容。 - Kelvin
1
这个解决方案的问题在于 echo 可能需要很长时间,这意味着临时文件会在文件系统上停留很长时间。如果可以接受这一点,那么您可以仅使用文件名而不是 fd 3 进行重定向。 - Cyker

5

不,Bash在重定向时没有“查找”的概念。它(大多数情况下)从头到尾以一个长流的方式进行读写。


2
那么,基本上在bash中使用读/写描述符的唯一原因是将它们传递给exec'ed进程? - telotortium
1
为了提供除标准输入、标准输出和标准错误外的更多通道,是的。 - Ignacio Vazquez-Abrams
它也常与网络连接一起使用... exec {w}<>/dev/tcp/www.google.com/80,你可以用它来写入文件的中间部分。你打开它,读/写,读取N个字符,然后写入。在https://antofthy.gitlab.io/info/shell/file_handles.txt 中搜索“读写”以获得示例。 - anthony

5
当你在bash中打开一个文件描述符时,它就变成了一个在/dev/fd/下的可访问文件。你可以使用cat从头开始读取,或者追加(echo "something" >> /dev/fd/3),这将把内容添加到末尾。至少在我的系统上是这样的。(另一方面,即使我没有对描述符进行任何写操作,似乎也无法使"cat <&3"正常工作)。

3
#!/bin/bash
F=$(mktemp tmp.XXXXXX)
exec 3<> $F
rm $F

echo "Hello world" >&3
cat /dev/fd/3

如其他答案中所建议的cat会在从文件中读取数据之前为您倒回文件描述符,因为它认为这只是一个普通文件。


1
要“倒回”文件描述符,你可以简单地使用/proc/self/fd/3
测试脚本:
#!/bin/bash

# Fill data
FILE=test
date +%FT%T >$FILE

# Open the file descriptor and delete the file
exec 5<>$FILE
rm -rf $FILE

# Check state of the file
# should return an error as the file has been deleted
file $FILE

# Check that you still can do multiple reads or additions
for i in {0..5}; do
    echo ----- $i -----

    echo . >>/proc/self/fd/5
    cat /proc/self/fd/5

    echo
    sleep 1
done

尝试在脚本运行时kill -9它,你会发现与使用trap方法不同的是,该文件实际上已被删除。

0

对@sanmai的回答进行扩展...

并确认正在发生的事情...

#/bin/bash
F=$(mktemp tmp.XXXXXX)
exec 3<>$F     # open the temporary file for read and write
rm $F          # delete file, though it remains on file system

echo "Hello world!" >&3    # Add a line to a file
cat /dev/fd/3              # Read the whole file
echo "Bye" >>/dev/fd/3     # Append another line
cat /dev/fd/3              # Read the whole file
echo "Goodbye" >&3         # Overwrite second line
cat /dev/fd/3              # Read the whole file

cat <&3                    # Try to Rewind (no output)
echo "Cruel World!" >&3    # Still adds a line on end
cat /dev/fd/3              # Read the whole file

shell_seek 3 6 0           # seek fd 3 to position 6
echo -n "Earth" >&3        # Overwrite 'World'
shell_seek 3               # rewind fd 3
cat <&3                    # Read the whole file put 3 at end

请注意,echo Goodbye 会覆盖第二行,因为文件描述符&3没有被cat更改!所以我尝试使用 cat <&3,但没有输出任何内容,可能是因为文件描述符在文件末尾。为了查看它是否倒回了给定的描述符,它并没有这样做。最后一部分是使用提供的“C”程序,编译并命名为shell_seek,是的,它似乎可以工作,因为第一个“World”被替换为“Earth”,倒带(寻找到开头)起作用,允许最后的cat再次读取整个文件。它会再次将fd放在文件末尾!使用perl而不是C也不是很难。例如,perl -e 'open(FD,">&3"); seek(FD,0,0);'将文件描述符3倒回到文件开头。

我现在已经制作了一个perl版本的shell_seek,这样我就不必为不同的系统重新编译它。此外,脚本还可以“告诉”您当前文件描述符偏移量,并且还可以“截断”该文件描述符指向的文件。这两个操作在使用seek时通常会用到,因此将这些功能包含在内似乎是一个好主意。您可以从以下链接下载脚本... https://antofthy.gitlab.io/software/#shell_seek


在macOS(BSD?)中,cat /dev/fd/3不会倒回描述符,因此也不可移植(我甚至使用GNU cat 进行了测试,它仍然无法工作)。奇怪的是,即使对于<&3,它也可以与tail -r一起使用,因此可以做类似于tail -r <&3 | tail -r的事情,但这看起来很丑陋,并且GNU tail没有-r选项。或者,可以使用tail -100000 <&3,但在这里选择任意数字也很丑陋(或者必须知道预期行的最大数量)。tail +0 <&3由于某种原因不起作用。另外,在macOS下使用GNU tail也不起作用,因此请确保使用/usr/bin/tail - David Ongaro
感谢关于MacOS的更新。 最好使用shell_seek C程序或扩展的perl脚本来更精确地控制文件描述符。 - anthony

0

使用命名管道
mkfifo tubeA exec 3 <>tubeA rm tubeA
echo "Hello world" >&3 read x <&3 echo $x
echo "Hello world" >&3 cat <&3

^C


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接