7

I have a pretty long bash script that invokes quite a few external commands (git clone, wget, apt-get and others) that print a lot of stuff to the standard output.

I want the script to have a few verbosity options so it prints everything from the external commands, a summarized version of it (e.g. "Installing dependencies...", "Compiling...", etc.) or nothing at all. But how can I do it without cluttering up all my code?

I've thought about to possible solutions to this: One is to create a wrapper function that runs the external commands and prints what's needed to the standard output, depending on the options set at the start. This ones seems easier to implement, but it means adding a lot of extra clutter to the code.

The other solution is to send all the output to a couple of external files and, when parsing the arguments at the start of the script, running tail -f on that file if verbosity is specified. This would be very easy to implement, but seems pretty hacky to me and I'm concerned about the performance impact of it.

Which one is better? I'm also open to other solutions.

codeforester
  • 28,846
  • 11
  • 78
  • 104
Ocab19
  • 448
  • 7
  • 12
  • 1
    Have you considered using `set -x`? Call this command at the top of your script and every following command will be echoed. You can turn it off with `set +x`. – Mateusz Piotrowski Feb 22 '17 at 22:38
  • 1
    Additionally, you might use `set -e` as well to abort the script when any command exits with non-zero status. – Mateusz Piotrowski Feb 22 '17 at 22:41
  • No, that's not what I need. I know which commands I am running, I just want a way to make my script print everything they output or not depending on some options. It's more of a user thing than the actual code. – Ocab19 Feb 22 '17 at 22:47
  • 1
    For logging certain commands, I use a wrapper function, which receives the command to be executed as parameter, and then runs it. If verbosity is on, it also prints the command. For optional output of additional information, I use another wrapper functions which simply echoes its arguments if verbosity is on, and doesn't do anything if verbosity is off. – user1934428 Feb 23 '17 at 07:50
  • The `cronutils` package has useful programs, including this one: *`chronic` runs a command, and arranges for its standard out and standard error to only be displayed if the command fails (exits nonzero or crashes). If the command succeeds, any extraneous output will be hidden.* – Toby Speight Feb 24 '17 at 08:49

4 Answers4

7

Improving on @Fred's idea a little bit more, we could build a small logging library this way:

declare -A _log_levels=([FATAL]=0 [ERROR]=1 [WARN]=2 [INFO]=3 [DEBUG]=4 [VERBOSE]=5)
declare -i _log_level=3
set_log_level() {
  level="${1:-INFO}"
  _log_level="${_log_levels[$level]}"
}

log_execute() {
  level=${1:-INFO}
  if (( $1 >= ${_log_levels[$level]} )); then
    "${@:2}" >/dev/null
  else
    "${@:2}"
  fi
}

log_fatal()   { (( _log_level >= ${_log_levels[FATAL]} ))   && echo "$(date) FATAL  $*";  }
log_error()   { (( _log_level >= ${_log_levels[ERROR]} ))   && echo "$(date) ERROR  $*";  }
log_warning() { (( _log_level >= ${_log_levels[WARNING]} )) && echo "$(date) WARNING  $*";  }
log_info()    { (( _log_level >= ${_log_levels[INFO]} ))    && echo "$(date) INFO   $*";  }
log_debug()   { (( _log_level >= ${_log_levels[DEBUG]} ))   && echo "$(date) DEBUG  $*";  }
log_verbose() { (( _log_level >= ${_log_levels[VERBOSE]} )) && echo "$(date) VERBOSE $*"; }

# functions for logging command output
log_debug_file()   { (( _log_level >= ${_log_levels[DEBUG]} ))   && [[ -f $1 ]] && echo "=== command output start ===" && cat "$1" && echo "=== command output end ==="; }
log_verbose_file() { (( _log_level >= ${_log_levels[VERBOSE]} )) && [[ -f $1 ]] && echo "=== command output start ===" && cat "$1" && echo "=== command output end ==="; }

Let's say the above source is in a library file called logging_lib.sh, we could use it in a regular shell script this way:

#!/bin/bash

source /path/to/lib/logging_lib.sh

set_log_level DEBUG

log_info  "Starting the script..."

# method 1 of controlling a command's output based on log level
log_execute INFO date

# method 2 of controlling the output based on log level
date &> date.out
log_debug_file date.out

log_debug "This is a debug statement"
...
log_error "This is an error"
...
log_warning "This is a warning"
...
log_fatal "This is a fatal error"
...
log_verbose "This is a verbose log!"

Will result in this output:

Fri Feb 24 06:48:18 UTC 2017 INFO    Starting the script...
Fri Feb 24 06:48:18 UTC 2017
=== command output start ===
Fri Feb 24 06:48:18 UTC 2017
=== command output end ===
Fri Feb 24 06:48:18 UTC 2017 DEBUG   This is a debug statement
Fri Feb 24 06:48:18 UTC 2017 ERROR   This is an error
Fri Feb 24 06:48:18 UTC 2017 ERROR   This is a warning
Fri Feb 24 06:48:18 UTC 2017 FATAL   This is a fatal error

As we can see, log_verbose didn't produce any output since the log level is at DEBUG, one level below VERBOSE. However, log_debug_file date.out did produce the output and so did log_execute INFO, since log level is set to DEBUG, which is >= INFO.

Using this as the base, we could also write command wrappers if we need even more fine tuning:

git_wrapper() {
  # run git command and print the output based on log level
}

With these in place, the script could be enhanced to take an argument --log-level level that can determine the log verbosity it should run with.


Here is a complete implementation of logging for Bash, rich with multiple loggers:

https://github.com/codeforester/base/blob/master/lib/stdlib.sh


If anyone is curious about why some variables are named with a leading underscore in the code above, see this post:

codeforester
  • 28,846
  • 11
  • 78
  • 104
4

You already have what seems to be the cleanest idea in your question (a wrapper function), but you seem to think it would be messy. I would suggest you reconsider. It could look like the following (not necessarily a full-fledged solution, just to give you the basic idea) :

#!/bin/bash

# Argument 1 : Logging level for that command
# Arguments 2... : Command to execute
# Output suppressed if command level >= current logging level 
log()
{
if
  (($1 >= logging_level))
then
  "${@:2}" >/dev/null 2>&1
else
  "${@:2}"
fi
}

logging_level=2

log 1 command1 and its args
log 2 command2 and its args
log 3 command4 and its args

You can arrange for any required redirection (with file descriptors if you want) to be handled in the wrapper function, so that the rest of the script remains readable and free from redirections and conditions depending on the selected logging level.

Fred
  • 5,944
  • 6
  • 18
  • 1
    I'm still not sure if adding 'log' to every command I run is the cleanest solution. I'm worried other people will have trouble reading the code this way. but there seems to be no better way so I think I'll be going with this one in the end – Ocab19 Feb 23 '17 at 18:45
  • @Ocab19 - please see the `log_debug_file` and `log_verbose_file` functions in my [answer](http://stackoverflow.com/a/42426680/6862601). With these, we can eliminate the clutter / readability loss caused by a construct like "log 1" etc., prefixed to every command whose output we need to throttle. But redirecting the output and then calling the log function is clutter as well. – codeforester Feb 24 '17 at 06:54
  • See https://github.com/codeforester/base/blob/master/lib/stdlib.sh for a complete Bash logging solution. – codeforester Apr 09 '19 at 05:42
2

Solution 1. Consider using additional file descriptors. Redirect required file descriptors to STDOUT or /dev/null depending on selected verbosity. Redirect output of every statement in your script to a file descriptor corresponding to its importance. Have a look at https://unix.stackexchange.com/a/218355 .

Community
  • 1
  • 1
dr_agon
  • 325
  • 2
  • 8
1

Solution 2.

Set $required_verbosity and pipe STDOUT of every statement in your script to a helper script with two parameters, something like this:

statement | logger actual_verbosity $required_verbosity

In a logger script echo STDIN to STDOUT (or log file, whatever) if $actual_verbosity >= $required_verbosity.

dr_agon
  • 325
  • 2
  • 8