4

Environment: GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin20)

Try 0:

exec 3<> "$(mktemp)"    # open file descriptor 3 to a temp file for read/write
echo 'foo' >&3          # write 'foo' to descriptor 3
read bar <&3            # read contents of descriptor 3 to variable named bar (not using -u 3)
echo $?                 # exit status returns "1"
exec 3>&-               # close file descriptor 3

In the above snippet I'm expecting $bar to be set to "foo" but it does not work because the position in the file descriptor 3 is at the end of the file descriptors file.

Try 1:

exec 3<> "$(mktemp)"    # open file descriptor 3 to a temp file for read/write
echo 'foo' >&3          # write 'foo' to descriptor 3
cat <&3                 # echo contents of of descriptor 3 to STDOUT
echo $?                 # exit status returns "0"
exec 3>&-               # close file descriptor 3

This also fails because the file location in descriptor 3 is at the end.

In C/C++ you can rewind(...) and fseek(...) to reset the file pointer of the file temp file created by mktemp to the beginning of the file.

Known workaround from here: (use second descriptor)

myTempFile="$(mktemp)"  # create temp file and assign to $myTempFile
exec 3> "$myTempFile"   # open file descriptor 3 for writing to $myTempFile
exec 4< "$myTempFile"   # open file descriptor 4 for reading of $myTempFile
echo 'foo' >&3          # write 'foo' to descriptor 3
read bar <&4            # read contents of descriptor 4 to variable named bar
echo $?                 # exit status returns "0"
echo "$bar"             # echo "foo"
exec 3>&-               # close file descriptor 3
exec 4>&-               # close file descriptor 4

The Question™ (finally):

How do you rewind a file descriptor in bash without using a second file descriptor to read from the beginning?

NOTE: I do not want to install ksh93 to use its rewind feature.

exec 3<> "$(mktemp)"
echo 'foo' >&3
# Magic Happens here
read bar <&3             # expect $bar == "foo"
echo $?                  # expect "0"
exec 3>&- 
Yzmir Ramirez
  • 155
  • 1
  • 8

3 Answers3

3

While it is an interesting question (but also something that I have never in 30 years needed to do with a shell). You may be trying to use the wrong tool.

Unix (UNIX, BSD, MacOS, Gnu/Linux) has pipes. A pipe is a special file (not in secondary storage / disk ), that has two file descriptors. One for writing and one for reading. The read descriptor always follows the write. After reading data is deposed of.

echo a_command | another_command

There are times when you can't create the pipe at the time that you launch the commands. In these situations you can use named-pipes, or unix-sockets.

A unix-socket is created and destroyed with one of the processes. It is also two way.

A named-pipe can be created independently of both processes.

2

zsh and ksh93 have builtin wrappers for the lseek() system call, but not bash, so you'd need to invoke a separate utility that can do the lseek() on the open file description attached to the fd. zsh is installed by default on macos, so, if you have to use bash, you could define a sysseek function that wraps zsh's sysseek builtin:

sysseek() {
  zsh -c '
    zmodload zsh/system
    sysseek "$@" || syserror -p "$0: "
  ' sysseek "$@"
}

And use:

sysseek -u 3 0

To rewind fd 3 to the beginning.

Another option for systems that don't have zsh is to use perl which is more widely available:

sysseek() { # args: fd, mode, offset
  perl -e '
    my ($fd, $mode, $offset) = @ARGV;
    open FD, ">&", $fd or die "fd $fd: $!\n";
    seek FD, $mode, $offset or die "seek: $!\n";
  ' -- "$@"
}

And use:

sysseek 3 0 0

To rewind fd 3 to the beginning.

The second argument can be:

  1. seek relative to the start (like sysseek -w start in zsh, the default there).
  2. seek relative to the current position (zsh's sysseek -w current)
  3. seek relative to the end of the file (zsh's sysseek -w end).
  • This is useful for those able to use zsh and ksh93, so thank you. In the environment I'm using this case for I don't have the luxury to use a different shell, so I can't accept your answer, but doesn't mean it can't help others. Thank you. – Yzmir Ramirez Feb 22 '22 at 05:18
1

Separate the creation of a read descriptor from a write descriptor:

exec 3>file
echo foo >&3
exec 3<file
cat <&3
exec 3<&-

The table for 3 descriptor is overwritten, and it becomes read-only. It makes no sense to use a universal descriptor.

Hide the descriptor number in a variable:

{ echo foo >&$fd; exec {fd}<&-; } {fd}<>file
{ cat <&$fd; } {fd}<>file
exec {fd}<&-

Since the redirection is performed first, the $fd variable is written and already has a value inside the block. It should be noted here that the handle is not automatically closed as in ksh93 and must be closed explicitly, at the end of the block or with the next command (both options are shown). Preparing to introduce a built-in variable into the next bash releases that can change it to auto-close behavior. With this form of call, there is no need for a command exec {fd}<&-

nezabudka
  • 2,428
  • 6
  • 15
  • This is the solution I'm using because I wanted to use the same file descriptor, in this case 3. Thank you. I see you had to run exec 3<file to read from the file descriptor and in doing so it set the file pointer to the beginning on file. – Yzmir Ramirez Feb 22 '22 at 05:17