#!/bin/bash
-# Copyright (C) 2016 Ian Kelling
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
+# Copyright (C) 2019 Ian Kelling
+# SPDX-License-Identifier: AGPL-3.0-or-later
append() {
cat >> "$1"
}
log-once() {
- local cbase c log x i out file o
+ 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: log-once [OPTION]... LOG_NAME [LOG_MESSAGE]
-
-For cronjobs, email log on repeated failure and success after failure.
+ local help="Usage: some-command-that-outputs-on-error |& log-once [OPTION]... LOG_NAME
-Meant for use in cronjobs where LOG_MESSAGE or STDIN represents an error,
+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 (an empty LOG_MESSAGE).
+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
+
-Logs LOG_MESSAGE or STDIN to /var/local/cron-errors/LOG_NAME\$error_count
-or $HOME/cron-errors if not root, and keeps
-state in the same directory.
+You can emulate how cronjobs work by doing this for example:
--ERRORS: ERRORS is the number of errors to accumulate before outputing the error"
+ 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"
# todo, make option & make them overridable based on command line or env variable
[[ -d $cbase ]] || mkdir -p $cbase
c=$cbase/$log_name
- # http://stackoverflow.com/questions/2456750/detect-presence-of-stdin-contents-in-shell-script
log=false
- if [[ $2 ]]; then
- log=true
- # read stdin for anything which is not just a newline
- elif [[ ! -t 0 ]]; then
- while read -r x; do
- output+=( "$x" )
- [[ $x ]] && log=true
- done
- fi
+ 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=($glob); [[ $file != "$glob" ]] || file=
+ # 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))
- mv $file $new_file
+ 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"
out="tee -a"
fi
fi
- $out $file <<<"log-once: $(date "+%A, %B %d, %r")"
+ $out $file <<<"log-once: $(date -R)"
if [[ $2 ]]; then
- $out $file <<<"$2"
+ $out $file <<<"$*"
else
for o in "${output[@]}"; do
$out $file <<<"$o"
if [[ $file ]]; then
rm -f $file
if [[ $file == $c$errors ]]; then
- echo "log-once success after failure for $c"
+ echo "log-once: $(date -R): success after failure for $c"
fi
fi
return 0