Simple log function in Bash I am just getting a little more comfortable with Bash.
I wanted a super simple log function to write to a file and output to the console for messages and errors. I also wanted the terminal and the file to interpret newline characters rather than printing them out.
Here is what I came up with (it works fine):
#!/bin/bash

log () {
  if [[ &quot;$3&quot; == '-e' || &quot;$3&quot; == '--error' ]]; then
    &gt;&amp;2 echo -e &quot;ERROR: $1&quot; &amp;&amp; printf &quot;ERROR: $1\n&quot; &gt;&gt; &quot;$2&quot;
  else
    echo -e &quot;$1&quot; &amp;&amp; printf &quot;$1\n&quot; &gt;&gt; &quot;$2&quot;
  fi
}

# Example implementation
log_file=snip/init.log
msg=&quot;\nthis is an error message with leading newline character&quot;
log &quot;$msg&quot; $log_file -e
msg=&quot;this is success message with no newline characters&quot;
log &quot;$msg&quot; $log_file

I don't need any more functionality however I have some questions:

Is an efficient and readable piece of code that won't break when passed strings with special characters?
Syntactically, could I have done this 'better' such as using pipes instead of &amp;&amp;. tee instead of printf etc.?
How can I refactor this code to allow for the optional -e flag to be passed in first?
There's quite a bit of repetition:

echo &amp;&amp; print with roughly the same arguments - tee is probably better here
error and non-error paths - perhaps use one function to prepend the ERROR: tag and call the other?

It might be best to have the filename first, then any number of message words?  That would make the interface more like echo, for example.
There's also a problem with our use of printf - any % characters in the error message will be interpreted as replacement indicators.  Not what we want.  We could use printf %b to expand escape sequences instead:
# log FILE MESSAGE...
log () {
    local file=&quot;$1&quot;; shift
    printf '%b ' &quot;$@&quot; '\n' | tee -a &quot;$file&quot;
}

# log_err FILE MESSAGE...
log_error () {
    local file=&quot;$1&quot;; shift
    local message=&quot;ERROR: $1&quot;; shift
    log &quot;$file&quot; &quot;$message&quot; &quot;$@&quot; &gt;&amp;2
}

If we do want to have a single function with arguments (e.g. to add more options), then we might want to use the while/case approach of option parsing that's often used at the program level:
#!/bin/bash

set -eu

# log [-e] [-f FILE] MESSAGE...
log() {
    local prefix=&quot;&quot;
    local stream=1
    local files=()
    # handle options
    while ! ${1+false}
    do case &quot;$1&quot; in
        -e|--error) prefix=&quot;ERROR:&quot;; stream=2 ;;
        -f|--file) shift; files+=(&quot;${1-}&quot;) ;;
        --) shift; break ;; # end of arguments
        -*) log -e &quot;log: invalid option '$1'&quot;; return 1;;
        *) break ;; # start of message
       esac
       shift
    done
    if ${1+false}
    then log -e &quot;log: no message!&quot;; return 1;
    fi
    # if we have a prefix, update our argument list
    if [ &quot;$prefix&quot; ]
    then set -- &quot;$prefix&quot; &quot;$@&quot;
    fi
    # now perform the action
    printf '%b ' &quot;$@&quot; '\n' | tee -a &quot;${files[@]}&quot; &gt;&amp;$stream
}

There's a non-obvious trick in there that might need explanation: I've used ${1+false} to expand to false when there's at least one argument remaining, or to an empty command when there are no arguments (empty command is considered &quot;true&quot; by if and while).