Bash, overwrite using file descriptor?

一曲冷凌霜 提交于 2019-12-06 08:31:18

That's correct. The mode in which a file is opened is determined when the shell calls open(2). When you DUP2 an FD (in any language), the flags that were set when the file was opened are shared between open FDs. In your case, O_TRUNC can only be specified when the file is actually opened.

The important thing to know is that the mode and various flags are determined only when the file is opened using <file, >file, or similar. Copying a FD with the & modifier essentially creates an "alias" that points to the original FD, and retains all the same state as the original. Truncating the file requires re-opening it.

This is my debugging function if you'd like to play around with file descriptors easily:

lsfd() {
    local ofd=${ofd:-2} target=${target:-$BASHPID}

    while [[ $1 == -* ]]; do
        if [[ -z $2 || $2 == *[![:digit:]]* ]]; then
            cat
            return 1
        fi
        case ${1##+(-)} in
            u)
                shift
                ofd=$1
                shift
                ;;
            t)
                shift
                target=$1
                shift
                ;;
            h|\?|help)
                cat
                return
        esac
    done <<EOF
USAGE: ${FUNCNAME} [-h|-?|--help] [-u <fd>] [ -t <PID> ] [<fd1> <fd2> <fd3>...]

This is a small lsof wrapper which displays the open
file descriptors of the current BASHPID. If no FDs are given,
the default FDs to display are {0..20}. ofd can also be set in the
environment.

    -u <fd>: Use fd for output. Defaults to stderr. Overrides ofd set in the environment.
    -t <PID>: Use PID instead of BASHPID. Overrides "target" set in the environment.
EOF

    IFS=, local -a 'fds=('"${*:-{0..20\}}"')' 'fds=("${fds[*]}")'
    lsof -a -p $target -d "$fds" +f g -- >&${ofd}
}

I like to not close stdin, because this can sometimes cause problems, so it gets saved first.

 $ ( { lsfd 3; cat <&3; } {savefd}<&0 <<<'hi' 3>&0- <&"${savefd}" )
COMMAND PID  USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF     NODE NAME
bash    920 ormaaj   3r   REG        LG   0,22        3 59975426 /tmp/sh-thd-8305926351 (deleted)
hi

As you can see, even if FD 0 is moved to 3 using the 3>&0- operator, the file remains opened O_RDONLY. The choice of > or < is arbitrary with the copy descriptor and only serves to determine the default if the FD on the left of the operator is omitted.

If you did want to open a new independent FD, something like this could work:

$ ( {
cat <&4 >/dev/null; lsfd 3 4; echo there >&4; cat </dev/fd/3
} {savefd}<&0 <<<'hi' 3>&0- 4<>/dev/fd/3 <&"${savefd}"
)
COMMAND  PID  USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF     NODE NAME
bash    2410 ormaaj   3r   REG        LG   0,22        3 59996561 /tmp/sh-thd-8305914274 (deleted)
bash    2410 ormaaj   4u   REG     RW,LG   0,22        3 59996561 /tmp/sh-thd-8305914274 (deleted)
hi
there

Now FD 4 is really "re-opened" to the file FD 3 is pointed at, not just duplicated (even if the file has already been unlink(2)'d, as above). First FD 4 is opened and seeked to the end using cat, then an additional line is written to the file. Meanwhile the seek position of FD 3 is still at the beginning, so the entire resulting file can be cated to the terminal.

The same principle could be applied to your case.

$ { echo 'Hello world'; echo 'hi' >/dev/fd/1; } >hosts; cat hosts
hi

Or probably even better would be to just open and close the file twice using two separate commands, without exec.

I prefer to avoid using exec just to open file descriptors unless it's absolutely necessary. You have to remember to explicitly close the file. If you use command grouping instead, files will automatically close themselves, in effect giving them a "scope". This is similar in principle to Python's with statement. That might have prevented some confusion.

See also:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!