Bash: assigning the output of 'times' builtin to a variable - bash

In a bash script I would like to assign the output of the times builtin to an array variable, but I found no better way than
tempnam=/tmp/aaa_$$_$RANDOM
times > ${tempnam}
mapfile -t times_a < ${tempnam}
I write the output to a temp file and read it back in array times_a because pipelines or $(times) would execute in a subshell and return the wrong values.
Any better solution without the temp file?

The fundamental problem that you need to solve is how to get both the execution of time and the variable assignment to happen in the same shell, without a temporary file. Almost every method Bash provides of piping the output of one thing to another, or capturing the output of a command, has one side working in a subshell.
Here is one way you can do it without a temporary file, but I'll warn you, it's not pretty, it's not portable to other shells, and it requires at least Bash 4:
coproc co { cat; }; times 1>&${co[1]}; eval "exec ${co[1]}>&-"; mapfile -tu ${co[0]} times_a
I'll break this down for you:
coproc co { cat; }
This creates a coprocess; a process which runs in the background, but you are given pipes to talk to its standard input and standard output, which are the FDs ${co[0]} (standard out of cat) and ${co[1]} (standard in of cat). The commands are executed in a subshell, so we can't do either of our goals in there (running times or reading into a variable), but we can use cat to simply pass the input through to the output, and then use that pipe to talk to times and mapfile in the current shell.
times >&${co[1]};
Run times, redirecting its standard out to the standard in of the cat command.
eval "exec ${co[1]}>&-"
Close the input end of the cat command. If we don't do this, cat will continue waiting for input, keeping its output open, and mapfile will continue waiting for that, causing your shell to hang. exec, when passed no commands, simply applies its redirections to the current shell; redirecting to - closes an FD. We need to use eval because Bash seems to have trouble with exec ${co[1]}>&-, interpreting the FD as the command instead of part of the redirection; using eval allows that variable to be substituted first, and then then executed.
mapfile -tu ${co[0]} times_a
Finally we actually read the data from the standard out of the coprocess. We've managed to run both the times and the mapfile command in this shell, and used no temporary files, though we did use a temporary process as a pipeline between the two commands.
Note that this has a subtle race. If you execute these commands one by one, instead of all as one command, the last one fails; because when you close cats standard in, it exits, causing the coprocess to exit and FDs to be closed. It appears that when executed all on one line, mapfile is executed quickly enough that the coprocess is still open when it runs, and thus it can read from the pipe; but I may be getting lucky. I haven't figured out a good way around this.
All told, it's much simpler just to write out the temp file. I would use mktemp to generate a filename, and if you're in a script, add a trap to ensure that you clean up your tempfile before exiting:
tempnam=$(mktemp)
trap "rm '$tempnam'" EXIT
times > ${tempnam}
mapfile -t times_a < ${tempnam}

Brian's answer got me very interested in this problem, leading to this solution which has no race condition:
coproc cat;
times >&${COPROC[1]};
{
exec {COPROC[1]}>&-;
mapfile -t times_a;
} <&${COPROC[0]};
This is quite similar in underlying structure to Brian's solution, but there are some key differences that ensure no funny-business occurs due to timing issues. As Brian stated, his solution typically works because the bash interpreter starts running the mapfile command before the coprocess' file descriptors have been fully closed and cleaned up, so any unexpected delay before or during mapfile would break it.
Essentially, the coprocess' stdout file descriptor gets closed a moment after we close the coprocess' stdin file descriptor. We need a way to preserve the coprocess' stdout.
In the man pages for pipe, we find:
If all file descriptors referring to the read end of a pipe have been closed, then a write(2) will cause a SIGPIPE signal to be generated for the calling process.
Thus, we must preserve at least one file descriptor to the coprocess' stdout. This is easy enough to accomplish with Redirections. We can do something like exec 3<&${COPROC[0]}- to move the coprocess' stdout file descriptor to a newly created fd 31. When the coprocess terminates, we will still have a file descriptor for its stdout and be able to read from it.
Now, we can do the following:
Create a coprocess that does nothing but cat.
coproc cat;
Redirect times's stdout to the coprocess' stdin.
times >&${COPROC[1]};
Get a temporary copy of the coprocess' stdout file descriptor.
Close the coprocess' stdin file descriptor. (If using a version before Bash-4.3, you can use the eval trick Brian used.)
exec 3<&${COPROC[0]} {COPROC[1]}>&-;
Read from our temporary file descriptor into a variable.
mapfile -tu 3 times_a;
Close our temporary file descriptor (not necessary, but good practice).
exec 3<&-;
And we're done! However, we still have some opportunities to restructure to make things neater. Thanks to the nature of redirection syntax, this code:
coproc cat;
times >&${COPROC[1]};
exec 3<&${COPROC[0]} {COPROC[1]}>&-;
mapfile -tu 3 times_a;
exec 3<&-;
behaves identically to this code:
coproc cat;
times >&${COPROC[1]};
{
exec {COPROC[1]}>&-;
mapfile -tu 3 times_a;
} 3<&${COPROC[0]};
From here, we can remove the temporary file descriptor altogether, leading to our solution:
Bash 4.3+:
coproc cat; times >&${COPROC[1]}; { exec {COPROC[1]}>&-; mapfile -t times_a; } <&${COPROC[0]}
Bash 4.0+:
coproc cat; times >&${COPROC[1]}; { eval "exec ${COPROC[1]}>&-"; mapfile -t times_a; } <&${COPROC[0]}
1We do not need to close the original file descriptor here, and can just duplicate it, as the original gets closed when the stdin descriptor gets closed

Wow, good question.
An improvment would be to use mktemp, so that you are not relying on randomness to keep your file unique.
TMPFILE=$(mktemp aaa_XXXXXXXXXX)
times > "$TMPFILE"
mapfile -t times_a < ${tempnam}
rm "$TMPFILE"
Also, I use for instead of mapfile (becuase I don't have mapfile).
a=0; for var in $(cat "$TMPFILE"); do ((a++)); TIMES_A[$a]=$var; done
But, yeah, I do not see how you can do it without files or named pipes.

One possibility to do something similar would be
times > >(other_command)
It's an output redirection combided with a process substitution. This way times is executed in the current shell and the output redirected to a new subprocess. Therefore however doing this with mapfile doesn't make much sense, as that command wouldn't be executed in the same shell, and that's probably not what you'll want. The situation is a bit tricky, as you can't have either call to the shell builtins executed in a subshell.

Related

Launch process from Bash script in the background, then bring it to foreground

The following is a simplified version of some code I have:
#!/bin/bash
myfile=file.txt
interactive_command > $myfile &
pid=$!
# Use tail to wait for the file to be populated
while read -r line; do
first_output_line=$line
break # we only need the first line
done < <(tail -f $file)
rm $file
# do stuff with $first_output_line and $pid
# ...
# bring `interactive_command` to foreground?
I want to bring interactive_command to the foreground after its first line of output has been stored to a variable, so that a user can interact with it via calling this script.
However, it seems that using fg %1 does not work in the context of a script, and I cannot use fg with the PID. Is there a way that I can do this?
(Also, is there a more elegant way of capturing the first line of output, without writing to a temp file?)
Job control using fg and bg are only available on interactive shells (i.e. when typing commands in a terminal). Usually the shell scripts run in non-interactive shells (same reason why aliases don't work in shell scripts by default)
Since you already have the PID stored in a variable, foregrounding the process is same as waiting on it (See Job Control Builtins). For example you could just do
wait "$pid"
Also what you have is a basic version of coproc bash built-in which allows you get the standard output messages captured from background commands. It exposes two file descriptors stored in an array, using which one can read outputs from stdout or feed inputs to its stdin
coproc fdPair interactive_command
The syntax is usually coproc <array-name> <cmd-to-bckgd>. The array is populated with the file descriptor id's by the built-in. If no variable is used explicitly, it is populated under COPROC variable. So your requirement can be written as
coproc fdPair interactive_command
IFS= read -r -u "${fdPair[0]}" firstLine
printf '%s\n' "$firstLine"

Get bash sub shell output immediately from named pipe

I have a few commands i run between brackets which i then redirect to a named pipe and tail the pipe however it looks like the redirection happens only after the block has finished executing as i don't see any output from the tail command for a while and it only shows the last command ouput when i do. Any ideas how view the output of the block in realtime?
Example Script
#!/usr/bin/env bash
mkfifo /tmp/why_you_no_out;
trap "rm /tmp/why_you_no_out" 0;
{
for ((i=1;i<=100;i++)); do
printf "$i";
done
sleep 10s;
printf "\n12356";
} >> /tmp/why_you_no_out &
printf "here";
tail -n 1 -f /tmp/why_you_no_out
Sounds like the issue is buffering. Most shells don't want to write data a byte at a time because it's wasteful. Instead, they wait until they have a sizable chunk of data before committing it unless the output is connected to your terminal.
If you're looking to unbuffer the output of an arbitrary command, you may find the "unbuffer" utility helpful or any of the solutions mentioned in this question: How to make output of any shell command unbuffered?
If you're dealing with specific applications, they may have options to reduce buffering. For example, GNU's grep includes the --line-buffered option.

How can I duplicate standard input (stdin) to multiple subprocesses in a bash script?

I want to redirect stdin to multiple scripts, in order to test an in-development git hook while leaving the old one in place. I know I should use tee somehow, I don't see how I can use the basic >, < and pipe | redirection features of bash to do this. Furthermore, how can I redirect the stdin of a script? I don't want to use read, because that only reads one line at a time and I'd have to re-execute all subprocesses for each line.
You could use tee with normal files (possibly temp files via mktemp), then cat those files to your various scripts. More directly, you could replace those normal files with Named Pipes created with mkfifo. But you can do it in one pipe using Bash's powerful Process Substitution >( cmd ) and <( cmd ) features to replace the file tee expects with your subprocesses.
Use <&0 for the first tee to get the script's stdin. Edit: as chepner pointed out, tee by default inherits the shell's stdin.
The final result is this wrapper script:
#!/bin/bash
set +o pipefail
tee >(testscipt >> testscript.out.log 2>> testscript.err.log) | oldscript
Some notes:
use set +o pipefail to disable Bash's pipefail feature if it was previously enabled. When enabled, Bash will report errors from within the pipe. When disabled, it will only report errors of the last command, which is what we want here to keep our testscript invisible to the wrapper (we want it to behave as if it was just calling oldscript to avoid disruption.
redirect the stdout of testscript, otherwise it'll be forwarded to the the next command in the pipeline which is probably not what you want. Redirect stderr too while you're at it.
Any number of tees can be pipe chained like this to duplicate your input (but don't copy the <&0 stdin redirection from the initial one) (initial <&0 has been removed)

Bash shell read error: 0: Resource temporarily unavailable

When writing a bash script. Sometimes you are running a command which opens up another program such as npm, composer.. etc. But at the same time you need to use read in order to prompt the user.
Inevitable you hit this kind of error:
read: read error: 0: Resource temporarily unavailable
After doing some research there seems to be a solution by piping the STDIN of those programs which manipulate the STDIN of your bash script to /dev/null.
Something like:
npm install </dev/null
Other research has shown it has something to do with the fact that the STDIN is being set to some sort of blocking/noblocking status and it isn't being reset after the program finishes.
The question is there some sort of fool proof, elegant way of reading user prompted input without being affected by those programs that manipulate the STDIN and not having to hunt which programs need to have their STDIN redirected to /dev/null. You may even need to use the STDIN of those programs!
Usually it is important to know what input the invoked program expects and from where, so it is not a problem to redirect stdin from /dev/null for those that shouldn't be getting any.
Still, it is possible to do it for the shell itself and all invoked programs. Simply move stdin to another file descriptor and open /dev/null in its place. Like this:
exec 3<&0 0</dev/null
The above duplicates stdin file descriptor (0) under file descriptor 3 and then opens /dev/null to replace it.
After this any invoked command attempting to read stdin will be reading from /dev/null. Programs that should read original stdin should have redirection from file descriptor 3. Like this:
read -r var 0<&3
The < redirection operator assumes destination file descriptor 0, if it is omitted, so the above two commands could be written as such:
exec 3<&0 </dev/null
read -r var <&3
When this happens, run bash from within your bash shell, then exit it (thus returning to the original bash shell). I found a mention of this trick in https://github.com/fish-shell/fish-shell/issues/176 and it worked for me, seems like bash restores the STDIN state. Example:
bash> do something that exhibits the STDIN problem
bash> bash
bash> exit
bash> repeat something: STDIN problem fixed
I had a similar issue, but the command I was running did need a real STDIN, /dev/null wasn't good enough. Instead, I was able to do:
TTY=$(/usr/bin/tty)
cmd-using-stdin < $TTY
read -r var
or combined with spbnick's answer:
TTY=$(/usr/bin/tty)
exec 3<&0 < $TTY
cmd-using-stdin
read -r var 0<&3`
which leaves a clean STDIN in 3 for you to read and 0 becomes a fresh stream from the terminal for the command.
I had the same problem. I solved by reading directly from tty like this, redirecting stdin:
read -p "Play both [y]? " -n 1 -r </dev/tty
instead of simply:
read -p "Play both [y]? " -n 1 -r
In my case, the use of exec 3<&0 ... didn't work.
Clearly (resource temporarily unavailable is EAGAIN) this is caused by programs that exits but leaves STDIN in nonblocking mode.
Here is another solution (easiest to script?):
perl -MFcntl -e 'fcntl STDIN, F_SETFL, fcntl(STDIN, F_GETFL, 0) & ~O_NONBLOCK'
The answers here which suggest using redirection are good. Fortunately, Bash's read should soon no longer need such fixes. The author of Readline, Chet Ramey, has already written a patch: http://gnu-bash.2382.n7.nabble.com/read-may-fail-due-to-nonblocking-stdin-td18519.html
However, this problem is more general than just the read command in Bash. Many programs presume stdin is blocking (e.g., mimeopen) and some programs leave stdin non-blocking after they exit (e.g., cec-client). Bash has no builtin way to turn off non-blocking input, so, in those situations, you can use Python from the command line:
$ python3 -c $'import os\nos.set_blocking(0, True)'
You can also have Python print the previous state so that it may be changed only temporarily:
$ o=$(python3 -c $'import os\nprint(os.get_blocking(0))\nos.set_blocking(0, True)')
$ somecommandthatreadsstdin
$ python3 -c $'import os\nos.set_blocking(0, '$o')'

Why wont Bash wait for read when used with Curl?

I wrote a Bash script to congfigure Git. It uses the read builtin, but when I do:
bash < <(curl -s https://raw.github.com/gist/419201/gitconfig.bash)
It doesn't wait for me to enter input. How do I get it to wait?
I tested it whitout the < as jcomeau_ictx suggested and it worked.
bash <(curl -s https://raw.github.com/gist/419201/gitconfig.bash | head -n 3)
Note: I used head -3 to stop execution after the read.
You may try to read directly from the controlling terminal /dev/tty to re-enable user input in case stdin is already redirected, i.e. file descriptor 0 is not opened on a terminal.
You may even use the -t option to the test command to handle such a situation programmatically (see help test or man test).
read git_name < /dev/tty # per-command I/O redirection
#read git_name < /dev/console # alternative
exec 0</dev/tty # script-wide I/O redirection
read git_name
In order to use stdin, you'd need to fetch the file, say to /tmp, then bash /tmp/gitconfig.bash. The way you're doing it now, you're redirecting stdin, and Unix doesn't have a separate file descriptor for command input like VMS does.

Resources