I've been struggling and reading quite a bit about this, and clearly bash is probably not the best tool for this, but since I'm already dug this deep into this hole (too much of the code is already there), I at least want to know if a solution exists how it will look like.

My Goal: A function named 'my_run', that wraps a command-line call, that call can be anything - builtin commands, functions or other scripts, and does the following:

  1. Call the command line.
  2. Tee stderr to a file (file_e), adding timestamps to each line
  3. Tee stdout to a separate file (file_s), adding timestamps to each line
  4. Presenting the output on the console (hence the tees above)
  5. Returns with the exit return code of the command.

I got 1-4 to work, I actually got 5 to work as well, but I'm wondering if there is a different/better way to achieve the same goal.

This currently works by saving the value in a file, and reading it into a variable. As I said - it works.

My ask

Is there a way to assign a value to a variable 'inside' (line 18 below) and make it available outside without using an intermediate file?

I understand that the pipe creates sub-shells, and environment doesn't pass back from that sub-shell, and that shopt -s lastpipe (with job management disabled in interactive shells set +m) should help out, but since I'm not assigning in the 'last pipe', this never worked for me.

Has anyone ever done anything like this?

The piece of code I'm focused on is this:

01  EXIT_CODE_TMP=$(mktemp)
02  unset MY_RUN_EXIT_CODE
05  # This is my failed attempt to try and capture the return code.
06  #shopt -s lastpipe
08  # This is where all the work is done:
09  # * Redirect stderr(2>) into stdout(&1), stdout(1>) in to
10  #   stream:3(&3)
11  # * grabs the code and dumps into a temp file
13  # * Tees while adding a timestamp into the *.2err log file
14  # * Redirects stream:3(3>) into stdin(&1), and stdin(1>) into
15  #   stderr(&2), basically reverting back
16  # * Tees while adding a timestamp into the *.1std log file
17  { {
18      2>&1 1>&3 "${1}" "${@:2}" || echo $? > $EXIT_CODE_TMP
19  } | tee >(_ts "E" > "/tmp/log.${MY_RUN_NAME}.2err")
20  } 3>&1 1>&2 \
21  | tee >(_ts > "/tmp/log.${MY_RUN_NAME}.1std")
23  # Reads the exit code form the temp file (and deletes the file)
24  MY_RUN_EXIT_CODE=$(( 0 + 0$(cat $EXIT_CODE_TMP; rm $EXIT_CODE_TMP) ))

If you want to test out the code, I've place a complete functional copy in my personal hastebin.

UPDATE: Clarification of scope

This question is bash specific, I understand that there are solutions elsewhere, but this will need to run in environments where installing a different shell might not be an option.

  • 1
    Bash has an array variable named PIPESTATUS which holds the exit status values from the most recently executed pipeline (foreground, not background). I've seen it in the man page, but haven't used it myself. Perhaps this could be useful if you really, really must use bash.
    – Sotto Voce
    Commented Oct 31, 2022 at 19:58
  • See also the pipefail option from ksh. Commented Nov 1, 2022 at 6:29
  • Thanks @SottoVoce, I'll check that out.
    – Lockszmith
    Commented Nov 1, 2022 at 16:35

1 Answer 1


It's easier with zsh. Switching from bash to zsh will likely remove many more headaches.

} > >(ts "%FT%T%z OUT:" >&1 > file_s) 2> >(ts "%FT%T%z ERR:" >&2 > file_e)

Here using moreutil's ts to do the timestamping.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .