#!/bin/bash # I, Ian Kelling, follow the GNU license recommendations at # https://www.gnu.org/licenses/license-recommendations.en.html. They # recommend that small programs, < 300 lines, be licensed under the # Apache License 2.0. This file contains or is part of one or more small # programs. If a small program grows beyond 300 lines, I plan to switch # its license to GPL. # Copyright 2024 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 # Log errors once or so instead of many times. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # SPDX-License-Identifier: GPL-3.0-or-later append() { cat >> "$1" } log-once() { local cbase c log line i out file o tmp mvout mverr verbose 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 -v: Output the errors along the way to ERRORS 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 verbose=false while true; do if [[ $1 == --help ]]; then echo "$help" return elif [[ $1 == -v ]]; then verbose=true shift 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 ]] || $verbose; 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 "$@"