tne
tne

Reputation: 7261

Time exec in a subshell

$ time (exec -a foo echo hello)
hello

It seems as though stderr (where time writes its output) leaks somewhere; obviously this is not what I intended.

My question could be phrased in generic terms as "why isn't the standard error stream written on the terminal when a subshell executes another program?".

A few notes:

  1. I need to use exec for its -a switch, which changes the zeroth argument of the process. I would appreciate an alternative to exec to do just this, but I don't know of any, and now this behavior got me curious.
  2. Of course, I need a subshell because I want my script to continue. Again, any alternative would be welcome. Is exec in a subshell even a good thing to do?
  3. time'ing a subshell in general works fine, so it really has to do with exec.

Could somebody point me in the right direction? I'm not sure where to begin in any of the reference materials, exec descriptions are pretty terse.

Update: Actually, I was just "lucky" with time here being the bash builtin. It doesn't parse at all with /usr/bin/time or with any other process:

$ env (exec -a foo echo hello)
bash: syntax error near unexpected token `exec'

Actually this makes sense, we can't pass a subshell as an argument. Any idea how to do this any other way?

Update: To summarize, we have four good answers here, all different, and potentially something lacking:

  1. Use actual filesystem links (hard or symbolic) that bash will use by default and time normally. Credits to hek2mgl.

    ln $(which echo) foo && time ./foo hello && rm foo

  2. fork for time using bash and exec using a bash subshell without special syntax.

    time bash -c 'exec -a foo echo hello'

  3. fork for time using bash but exec using a tiny wrapper.

    time launch -a foo echo hello

  4. fork and exec for time using bash with special syntax. Credits to sjnarv.

    time { (exec -a foo echo hello); }

I think that solution 1 has the less impact on time as the timer doesn't have to count the exec in the "proxy" program, but isn't very practical (many filesystem links) nor technically ideal. In all other cases, we actually exec two times: once to load the proxy program (subshell for 2 and 4, wrapper for 3), and once to load the actual program. This means that time will count the second exec. While it can be extremely cheap, exec actually does filesystem lookups which can be pretty slow (especially if it searches through PATH, either itself with exec*p or if the proxy process does).

So, the only clean way (as far as what the answers of this question covered) would be to patch bash to modify its time keyword so that it can exec while setting the zeroth argument to a non-zero value. It would probably look like time -a foo echo hello.

Upvotes: 2

Views: 2539

Answers (3)

sjnarv
sjnarv

Reputation: 2384

I don't think that the timer's output disappears. I think it (the timer) was running in the sub-shell overlaid by the exec.

Here's a different invocation. Perhaps this produces what you expected initially:

$ time { (exec -a foo echo hello); }

Which for me emits:

hello

real    0m0.002s
user    0m0.000s
sys     0m0.001s

Upvotes: 2

tne
tne

Reputation: 7261

So, I ended up writing that tiny C wrapper, which I call launch:

#include <stdlib.h>
#include <unistd.h>

int main(const int argc, char *argv[])
{
    int opt;
    char *zeroth = NULL;

    while ((opt = getopt(argc, argv, "a:")) != -1)
        if (opt == 'a')
            zeroth = optarg;
        else
            abort();

    if (optind >= argc) abort();
    argv += optind;
    const char *const program = *argv;
    if (zeroth) *argv = zeroth;
    return execvp(program, argv);
}

I obviously simplified it to emphasize only what's essential. It essentially works just like exec -a, except that since it is not a builtin, the shell will fork normally to run the launch program as a separate process. There is thus no issue with time.

The test program in the following sample output is a simple program that only outputs its argument vector, one argument per line.

$ ./launch ./test hello world
./test
hello
world
$ ./launch -a foo ./test hello world
foo
hello
world
$ time ./launch -a foo ./test hello world
foo
hello
world

real    0m0.004s
user    0m0.001s
sys     0m0.002s
$ ./launch -a foo -- ./test -g hello -t world
foo
-g
hello
-t
world

The overhead should be minimal: just what's necessary to load the program, parse its single and optional argument, and manipulate the argument vector (which can be mostly reused for the next execvp call).

The only issue is that I don't know of a good way to signal that the wrapper failed (as opposed to the wrapped program) to the caller, which may happen if it was invoked with erroneous arguments. Since the caller probably expects the status code from the wrapped program and since there is no way to reliably reserve a few codes for the wrapper, I use abort which is a bit more rare, but it doesn't feel appropriate (nor does it make it all OK, the wrapped program may still abort itself, making it harder for the caller to diagnose what went wrong). But I digress, that's probably not interesting for the scope of this question.

Edit: just in case, the C compiler flags and feature test macros (gcc/glibc):

CFLAGS=-std=c11 -pedantic -Wall -D_XOPEN_SOURCE=700

Upvotes: 1

hek2mgl
hek2mgl

Reputation: 158100

Time is based on the wait system call. From the time man page

Most information shown by time is derived from the wait3(2) system call.

This will only work if time is the father process of the command to be executed. But exec creates a completely new process.

As time requires fork() and wait() I would not attach too much attention on that zeroth argument of exec (what is useful, of course). Just create a symbolic link and then call it like:

time link_name > your.file 2>&1 &

Upvotes: 1

Related Questions