2

I would like to have a logging function that takes filenames as arguments and replicate the stdout to all of these files. This is what I have come up so far:

function logger() {
    exec > >(tee -ia /var/log/{log1,log2})
}

When I try to replace {log1,log2} with {$*}, I get the arguments separated by space. So, I thought I would do something like this:

function logger() {
    exec > >(IFS=,; tee -ia /var/log{"$*"}
}

This fails to do what I want as brace expansion happens before moving on to the variable substitution. So, I thought I could do this:

function logger() {
    exec > >(IFS=,; eval "tee -ia /var/log/\{$*\}")
}

But this behaves the same, ie logger one two creates one single file named {one,two}.

Why is that? How can I get brace expansion to work so that tee writes to multiple files?

Weijun Zhou
  • 3,338
  • 16
  • 42
user1371264
  • 201
  • 1
  • 8

1 Answers1

5

You can't use variable in a brace expansion in bash. See e.g. How can I use $variable in a shell brace expansion of a sequence?

If you don't want to call your logger function as

logger /var/log/log{1,2}

with the function written as

logger () {
    exec > >( tee -ia "$@" )
}

then what you could do is call it as

logger log{1,2}

or

logger log1 log2

and write the function as

logger () {
    for fname do
        set -- "$@" "/var/log/$fname"
        shift
    done
    exec > >( tee -ia "$@" )
}

or shorter (but more or less unreadable),

logger () {
    set -- "${@/#//var/log/}"
    exec > >( tee -ia "$@" )
}

or, if you wish,

logger () {
    exec > >( tee -ia "${@/#//var/log/}" )
}

This rewrites each element of the list of positional parameters by adding /var/log/ to the beginning of each of them. tee is then invoked with the list of modified arguments.

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • Thank you for the suggestions, this is what I will probably end up doing. However, in the linked question, they explain that `eval rm foo.{$ext0..$extN}` can be used. In my case, why isn't brace expansion working even after adding the eval? – user1371264 Mar 21 '19 at 11:34
  • 1
    @user1371264 You're escaping the braces. Note also that I would not suggest using `eval` on external data. The log file names are external (to the function). If you have full control over these filenames, then it may be ok, but I would still rather use the method I showed here, as it additionally does not rely on the filenames being single words with no embedded filename globbing characters etc. – Kusalananda Mar 21 '19 at 11:39
  • Right! I thought I had to escape the braces in any case, but I was wrong. Could you update the answer to include this information as well? – user1371264 Mar 21 '19 at 11:44
  • @user1371264 I'll let someone else write a solution based on `eval` as it's not a a solution I would personally support using. It's fragile and difficult to get completely right. – Kusalananda Mar 21 '19 at 11:47
  • 1
    Didn't know the `set -- "$@" ...; shift` trick. Something new learned every day. – Weijun Zhou Mar 21 '19 at 12:05
  • 1
    @WeijunZhou, yeah, that's a useful trick if you're restricted to a standard `sh`. Though in all ksh-like shells you could use a named array and `local array=(); for x do; array+=("$newitem")`. Which might be simpler to read, maybe. – ilkkachu Mar 21 '19 at 12:19
  • @ilkkachu Oh, I wouldn't call it being "restricted"... But I'd be the first to admit I'm partial to the POSIX syntax. – Kusalananda Mar 21 '19 at 12:38