satisfy shellcheck
[log-quiet] / log-once
index 040e2ba72d634a76c3363cc855d3997fd9f72967..8cfe388b85c88123313f91614e125d521d6783c4 100755 (executable)
--- a/log-once
+++ b/log-once
@@ -1,5 +1,12 @@
 #!/bin/bash
-# Copyright (C) 2016 Ian Kelling
+# 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.
 # 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 <http://www.gnu.org/licenses/>.
+
+# SPDX-License-Identifier: GPL-3.0-or-later
+
 append() {
-    cat >> "$1"
+  cat >> "$1"
 }
 log-once() {
-    local cbase c log x i out file
-    cbase=/var/local/cron-errors
-    [[ $EUID == 0 ]] || cbase=$HOME/cron-errors
-    local help="Usage: log-once [OPTION]... LOG_NAME [LOG_MESSAGE]
+  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
 
-For cronjobs, email log on repeated failure and success after failure.
-
-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).
-
-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.
-
--ERRORS:  ERRORS is the number of errors to accumulate before outputing the error"
-    errors=3
-    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
-    # 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
+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
-    glob="$c[0-9]*"
-    file=($glob); file="${file[0]}"; [[ $glob != $file ]] || file=
-    if $log; then
-        out=append
-        if [[ $file ]]; then
-            i=${file#$c}
-            if (( i < errors )); then
-                new_file=$c$((i+1))
-                mv $file $new_file
-                file=$new_file
-                if [[ $file == $c$errors ]]; then
-                    out="tee -a"
-                fi
-            fi
-        else
-            file=${c}1
-        fi
-        $out $file <<<"log-once: $(date "+%A, %B %d, %r")"
-        if [[ $2 ]]; then
-            $out $file <<<"$2"
-        else
-            $out $file <<<"${output[@]}"
-            $out $file
-        fi
-        return 1
+  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
-        rm -f $file
-        if [[ $file == $c$errors ]]; then
-            echo "log-once success after failure for $c"
+      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 "$@"