In
{ err=$(exec 2>&1 >&3; ls -ld /x /bin); exec 0<&3; out=$(cat); } 3>&1
The { ... } 3>&1
clones the fd 1 to fd 3. That just means that fd 3 now points to the same resource (same open file description) as what fd 1 pointed to. If you ran that from a terminal, that will probably be a fd open in read+write mode to a terminal device.
After exec 0<&3
, fds 0, 1, and 3 are all pointing to that same open file description (created when your terminal emulator opened the slave side of the pseudo-terminal pair it created before executing your shell in the case of the command run in the terminal case above).
Then in out=$(cat)
, for the process executing cat
the $(...)
changes fd 1 to the writing end of a pipe, while 0 is still the tty device. So cat
will read from the terminal device, so things you're typing on the keyboard (and if it wasn't a terminal device, you would probably get an error as the fd was probably open in write-only mode).
For cat
to read what ls
writes on its stdout, you'd need ls
stdout and cat
stdin to be two ends on an IPC mechanism like pipe, socketpair or pseudo-terminal pair. For instance ls
stdout to be the writing end of a pipe and cat
stdin to be the reading end.
But you'd also need ls
and cat
to run concurrently, not one after the other, as that's an IPC (inter-process communication) mechanism.
Since pipes can hold some data (64 KiB by default on current versions of Linux), you would get away with short outputs if you managed to create that second pipe, but for larger outputs, you'd run into deadlocks, ls
would hang when the pipe is full and would hang until something empties the pipe, but cat
can only empty the pipe when ls
returns.
Also, only yash
has a raw interface to pipe()
which you'd need to create that second pipe to read from ls
stdout (the other pipe for stderr being created by the $(...)
construct).
In yash, you'd do:
{ out=$(ls -d / /x 2>&3); exec 3>&-; err=$(exec cat <&4); } 3>>|4
Where 3>>|4
(a yash-specific feature) creates that second pipe with the writing end on fd 3 and the reading end on fd 4.
But again, if the stderr output is greater than the size of the pipe, that will hang. We're effectively using the pipe as a temporary file in memory, not a pipe.
To really use pipes, we'd need to start ls
with stdout being the writing end of one pipe and stderr being the writing end of another pipe, and then the shell read the other ends of those pipes concurrently, as the data comes (not one after the other or again you'd run into dead-locks) to store into the two variables.
To be able to read from those two fds as the data comes, you'd need a shell with select()
/poll()
support. zsh
is such a shell, but it doesn't have yash
's pipeline redirection feature¹, so you'd need to use named pipes (so manage their creation, permissions, and cleanup) and use a complex loop with zselect
/sysread
...
¹ If on Linux though, you would be able to use the fact that /proc/self/fd/x
on a pipe behaves like a named pipe though, so you could do:
#! /bin/zsh
zmodload zsh/zselect
zmodload zsh/system
(){exec {wo}>$1 {ro}<$1} <(:) # like yash's wo>>|ro (but on Linux only)
(){exec {we}>$1 {re}<$1} <(:)
ls -d / /x >&$wo 2>&$we &
exec {wo}>&- {we}>&-
out= err=
o_done=0 e_done=0
while ((! (o_done && e_done))) && zselect -A ready $ro $re; do
if ((${#ready[$ro]})); then
sysread -i $ro && out+=$REPLY || o_done=1
fi
if ((${#ready[$re]})); then
sysread -i $re && err+=$REPLY || e_done=1
fi
done
Best Answer
This is a bit of an XY problem but fortunately you've explained your real problem so it's possible to give a meaningful answer.
Sure, there are commands that can write text to a file without relying on their environment to open the file. For example,
sh
can do that: pass it the arguments-c
andecho text >filename
.Note that this does meet the requirement of “without redirection” here, since the output of
sh
is not redirected anywhere. There's a redirection inside the program thatsh
runs, but that's ok, it's just an internal detail of howsh
works.But does this solve your actual problem? Your actual problem is to write text to a file from a yad action. In order to resolve that, you need to determine what a yad action is. Unfortunately, the manual does not document this. All it says is
The action is a string, but a Unix command is a list of strings: a command name and its arguments. There are several plausible ways to turn a string into a command name and its arguments, including:
echo foo
printsfoo
, rather than attempting to execute the programecho foo
, this is not what yad does.echo >filename
prints>filename
, rather than writing tofilename
, this is not what yad does.Looking at the source code, the action is passed to
popup_menu_item_activate_cb
which calls the Glib functiong_spawn_command_line_async
. This function splits the given string usingg_shell_parse_argv
which has the following behavior, which is almost never what is desirable but can be worked around:So you can run a shell command by prefixing it with
sh -c '
and terminating with'
. If you need a single quote inside the shell command, write'\''
. Alternatively, you can run a shell command by prefixing it withsh -c "
, terminating with"
, and adding a backslash before any of the characters"$\`
that appear in the command. Take care of the nested quoting since the action is itself quoted in the shell script that calls yad.