11

I'd like to log standard output and standard error separately in .xprofile using logger. In Bash I think that would look something like this:

exec 1> >(logger --priority user.notice --tag $(basename $0)) \
     2> >(logger --priority user.error --tag $(basename $0))

How would I do that in a POSIX /bin/sh compatible manner?

l0b0
  • 51,350

2 Answers2

11

POSIX command/process substitution


_log()( x=0
    while  [ -e "${TMPDIR:=/tmp}/$$.$((x+=1))" ]
    do     continue; done        &&
    mkfifo -- "$TMPDIR/$$.$x"    &&
    printf %s\\n "$TMPDIR/$$.$x" || exit
    exec >&- >/dev/null
    {  rm -- "$TMPDIR/$$.$x"
       logger --priority user."$1" --tag "${0##*/}"
    }  <"$TMPDIR/$$.$x" &
)   <&- </dev/null

You should be able to use that like:

exec >"$(_log notice)" 2>"$(_log error)"

Here's a version which makes use of the mktemp command:

_log()( p=
    mkfifo "${p:=$(mktemp -u)}"    &&
    printf %s "$p"                 &&
    exec  <&- >&- <>/dev/null >&0  &&
    {   rm "$p"
        logger --priority user."$1" --tag "${0##*/}"
    }   <"$p" &
)

...which does much the same, except that it allows mktemp to select the filename for you. This works because process substitution is by no means magical and works in a very similar way to command substitution. Instead of replacing the expansion with the value of the command run within it as command substitution does, process substitution replaces it with the name of a filesystem link where the output can be found.

While the POSIX shell does not provide a direct corollary to such a thing, emulating it is very simply done. All you need to do is make a file, print its name to the standard out of a command substitution, and in the background of same run your command which will output to that file. Now you can just redirect into the value of that expansion - exactly as you do with process substitution. And so the POSIX shell provides all of the tools you need of course - all that is required is that you put them to use in a way which suits you.

Both of the above versions ensure that they destroy the filesystem link to the pipes they create/use before ever making use of them. This means there is no cleanup required after the fact, and, more importantly, their streams are only available to the processes which initially open them - and so their filesystem links cannot be used as a means to snoop/hijack your logging activity. To leave their fs-links in the filesystem is a potential security hole.


Another way is to wrap it. It can be done from within the script.

x=${x##*[!0-9]*}
_log(){ 
    logger --priority user."$1" --tag "${0##*/}"
}   2>/dev/null >&2

cd ../"$PPID.$x" 2>/dev/null &&
trap 'rm -rf -- "${TMPDIR:-/tmp}/$PPID.$x"' 0 || 
{   until cd -- "${TMPDIR:=/tmp}/$$.$x"
    do    mkdir -- "$TMPDIR/$$.$((x+=1))"
    done  && 
    x=$x "$0" "$@" | _log notice
    exit
}   2>&1 | _log error

That would basically allow your script to call itself if it hasn't yet and get you a work directory in temp to boot.

mikeserv
  • 58,310
  • 1
    Done, thank you! – l0b0 Jul 27 '15 at 07:37
  • @l0b0 - I'm just about to update another one kind of like this... It's more general purposed, and I finally got around to giving it the ability to handle some options as are related to i/o descriptors/files. – mikeserv Jul 27 '15 at 07:40
  • @l0b0 - if you wanted to use mktemp and other non-standard commands, it could be: _log()( p=; mkfifo "${p:=$(mktemp -u)}"; printf %s "$p"; exec <&- >&- <>/dev/null >&0; { rm "$p"; logger --priority user."$1" --tag "${0##*/}"; } <"$p" &). I wrote it using fully portable command language in all ways - with the exception of your own. Also, basename is not a very useful utility. "${0##*/}" is both more robust and faster. – mikeserv Jul 27 '15 at 18:03
  • I changed it because I only need it to work on modern platforms; the main issue was that .xprofile has to be executed with sh. – l0b0 Jul 27 '15 at 18:07
  • @l0b0 - in that case you could just symlink your shell of choice somewhere to sh and modify $PATH to match in .xprofile. Maybe reconsider your edit though - /bin/sh is, as I think, maybe a little more ambiguous. – mikeserv Jul 27 '15 at 18:09
  • The instant I saw the first code block I thought, "mikeserv wrote that." :) – Wildcard Jan 22 '16 at 21:31
  • This is actually completely brilliant code; I think it's great. One issue, though: POSIX logger doesn't recognize any options. But, since POSIX logger doesn't even have a way to log standard input (only command line arguments), the whole effort comes to pieces without an extended version of logger. Perhaps the same effect could be achieved with tee -a but then we're chest-deep in hypotheticals; I don't personally know any implementations of logger that don't accept any options. – Wildcard Jan 22 '16 at 21:40
  • @Wildcard - yeah, I did. What about the options? don't handle any here becuase I didn't really care about the logger thing - this was just intended to demonstrate a few simply implemented and highly customizable library functions to handle process substitution with little fuss and much speed in tiny POSIX shells so people wouldn't have to turn to giant, buggy sloths like bash. A lot of my other stuff is written with similar spirit. – mikeserv Jan 22 '16 at 22:00
  • @mikeserv, one thing I'm not getting in your first example, is how do you ensure the exec command in the top level actually opens the fifo for writing before the rm command is run? (Also, is there some edge case breakage that is prevented by closing stdin/stdout before redirecting them from/to /dev/null?) – Wildcard Mar 14 '16 at 09:01
  • 1
    @Wildcard - just a minute and i'll try to sort out the first part of the question, but the answer to the second is yes: the edge case is a zsh w/ multios enabled. As for the first part: { this; can\'t; happen; before; } < this. Does that help? – mikeserv Mar 15 '16 at 12:53
10

There's no POSIX equivalent. You can only perform a redirection with exec, not a fork. A pipe requires a fork, and the shell waits for the child to finish.

One solution is to put all your code in a function.

all_my_code () {
  …
}
{ all_my_code |
  logger --priority user.notice --tag "$(basename "$0")"; } 2>&1 | 
  logger --priority user.error --tag "$(basename "$0")"

(This also logs any error from the stdout instance of logger to the stderr instance. You can avoid this with more file descriptor shuffling.)

If you want the parent shell to exit even if the logger processes are still running, put & at the end of the logger invocations.

{ all_my_code |
  logger --priority user.notice --tag "$(basename "$0")" & } 2>&1 | 
  logger --priority user.error --tag "$(basename "$0")" &

Alternatively, you can use named pipes.

pipe_dir=$(mktemp -d)
mkfifo "$pipe_dir/out" "$pipe_dir/err"
<"$pipe_dir/out" logger --priority user.notice --tag "$(basename "$0")" &
<"$pipe_dir/err" logger --priority user.error --tag "$(basename "$0")" &
exec >"$pipe_dir/out" 2>"$pipe_dir/err" 
…
rm -r "$pipe_dir"