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:
- 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
&&
.tee
instead ofprintf
etc.? - How can I refactor this code to allow for the optional
-e
flag to be passed in first?
1 Answer 1
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
).
-
\$\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– apena2021年02月05日 23:52:15 +00:00Commented Feb 5, 2021 at 23:52 -
1\$\begingroup\$ Yes,
local
andtee -a
and the newline are all good improvements (I should have tried the code before publishing!). Edited. \$\endgroup\$Toby Speight– Toby Speight2021年02月06日 11:48:07 +00:00Commented 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\$Toby Speight– Toby Speight2021年02月06日 11:48:40 +00:00Commented Feb 6, 2021 at 11:48
log [OPTIONS] LOG_FILE MESSAGE...". So when called it would look like this
log "$log_file" "Part1" "Part 2" "and so on"` orlog --error "$log_file" "Part1" "Part 2" "and so on"
. \$\endgroup\$-e
first, usingif
andshift
. But it might be better to have two different functions, and that's what I propose in my answer. \$\endgroup\$