If you want to overwrite a file with Bash, this is easy
echo "Hello world" > hosts
This does not seem to work with a file descriptor
$ exec 3<> hosts
$ echo "Hello world" >&3
$ cat hosts
Hello world
$ echo "Hello world" >&3
$ cat hosts
Hello world
Hello world
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:
来源:https://stackoverflow.com/questions/11068741/bash-overwrite-using-file-descriptor