5
\$\begingroup\$

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 [[ "$3" == '-e' || "$3" == '--error' ]]; then
    >&2 echo -e "ERROR: $1" && printf "ERROR: $1\n" >> "$2"
  else
    echo -e "$1" && printf "$1\n" >> "$2"
  fi
}

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

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

  1. Is an efficient and readable piece of code that won't break when passed strings with special characters?
  2. Syntactically, could I have done this 'better' such as using pipes instead of &&. tee instead of printf etc.?
  3. How can I refactor this code to allow for the optional -e flag to be passed in first?
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Just as a small note, I'd switch the arguments around to log [OPTIONS] LOG_FILE MESSAGE...". So when called it would look like this log "$log_file" "Part1" "Part 2" "and so on"` or log --error "$log_file" "Part1" "Part 2" "and so on". \$\endgroup\$
    – Bobby
    Commented Feb 5, 2021 at 21:59
  • \$\begingroup\$ @Bobby yeah I should swap the FILE and MSG arguments around. I do not know how to allow for the optional -e flag to be the first argument though since it may or may not be passed into the log function. \$\endgroup\$
    – apena
    Commented Feb 5, 2021 at 22:48
  • 2
    \$\begingroup\$ You could easily pass -e first, using if and shift. But it might be better to have two different functions, and that's what I propose in my answer. \$\endgroup\$ Commented Feb 5, 2021 at 22:57

1 Answer 1

6
\$\begingroup\$

There's quite a bit of repetition:

  • echo && 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="$1"; shift
    printf '%b ' "$@" '\n' | tee -a "$file"
}

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

\$\endgroup\$
3
  • \$\begingroup\$ I think I need to keep this code in one single function though since I will have another log function for logging to only a file (no console), I call that function log_silent. The thing is I dont really want 4 functions to get this job done. I would like to do this with just two functions. \$\endgroup\$
    – apena
    Commented Feb 5, 2021 at 23:52
  • 1
    \$\begingroup\$ Yes, local and tee -a and the newline are all good improvements (I should have tried the code before publishing!). Edited. \$\endgroup\$ Commented Feb 6, 2021 at 11:48
  • 1
    \$\begingroup\$ I've updated to show how you can parse options in a single function. It should be easy to add a "silent" option that sets stream=/dev/null. \$\endgroup\$ Commented Feb 6, 2021 at 11:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.