#!/bin/bash # Copyright (C) 2019 Ian Kelling # SPDX-License-Identifier: AGPL-3.0-or-later append() { cat >> "$1" } log-once() { local cbase c log line i out file o tmp mvout mverr cbase=/var/local/cron-errors [[ $EUID == 0 ]] || cbase=$HOME/cron-errors local help="Usage: some-command-that-outputs-on-error |& log-once [OPTION]... LOG_NAME Main use case: in cronjobs where STDIN represents an error, but we only want to output that to STDOUT if we've seen this type of error ERRORS(default 3) number of times in a row, then we don't want to output anything again until we've seen a success: no standard input. Logs STDIN to /var/local/cron-errors/LOG_NAME\$error_count or $HOME/cron-errors if not root, and keeps state in the same directory. -ERRORS: ERRORS is the number of errors to accumulate before outputing the error You can emulate how cronjobs work by doing this for example: cmdname |& log-once | ifne mail -s 'cmdname failed' root@localhost I could imagine a similar command that considers its non-option args to be an error, but I haven't written it yet. " errors=3 mverr=0 while true; do if [[ $1 == --help ]]; then echo "$help" return elif [[ $1 == -[0-9]* ]]; then errors=${1#-} shift elif [[ $1 == -- ]]; then shift break else break fi done log_name=$1 # todo, make option & make them overridable based on command line or env variable [[ -d $cbase ]] || mkdir -p $cbase c=$cbase/$log_name log=false while read -r line; do output+=( "$line" ) # If we find something that is not just a newline: if [[ $line ]]; then log=true break fi done glob="$c[0-9]*" # file is error file indicating previous error tmp=($glob); file="${tmp[0]}" if [[ $file == "$glob" ]]; then file= fi if $log; then out=append if [[ $file ]]; then i="${file#$c}" if (( i < errors )); then new_file=$c$((i+1)) mvout=$(mv $file $new_file 2>&1) || mverr=$? if (( mverr )); then if [[ $mvout == *": No such file or directory" ]]; then # We've very likely hit a race condition, where another # log-once did the mv before us. return 0 else echo "log-once: error on mv $file $new_file" return $mverr fi fi file=$new_file if [[ $file == $c$errors ]]; then out="tee -a" fi fi else file=${c}1 if (( errors == 1 )); then out="tee -a" fi fi $out $file <<<"log-once: $(date -R)" if [[ $2 ]]; then $out $file <<<"$*" else for o in "${output[@]}"; do $out $file <<<"$o" done $out $file fi return 0 fi if [[ $file ]]; then rm -f $file if [[ $file == $c$errors ]]; then echo "log-once: $(date -R): success after failure for $c" fi fi return 0 } log-once "$@"