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,368

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
  • 333,661
  • 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