6
\$\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?
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Feb 5, 2021 at 21:28
\$\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\$ 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\$ 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).

answered Feb 5, 2021 at 22:56
\$\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\$ 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

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.