diff --git a/share/mk/Makefile b/share/mk/Makefile --- a/share/mk/Makefile +++ b/share/mk/Makefile @@ -69,8 +69,10 @@ meta.sys.mk \ meta2deps.py \ meta2deps.sh \ + newlog.sh \ ${SRCTOP}/contrib/bmake/mk/posix.mk \ stage-install.sh \ + setopts.sh \ sys.mk \ sys.dependfile.mk \ sys.dirdeps.mk \ diff --git a/share/mk/local.sys.env.mk b/share/mk/local.sys.env.mk --- a/share/mk/local.sys.env.mk +++ b/share/mk/local.sys.env.mk @@ -43,6 +43,12 @@ # error spam and show a proper error. Mkdirs= Mkdirs() { mkdir -p $$* || :; } +# jobs.mk wants this +.if empty(NEWLOG_SH) +NEWLOG_SH:= ${.PARSEDIR:tA}/newlog.sh +.export NEWLOG_SH +.endif + .if !empty(.MAKEFLAGS:M-s) ECHO_TRACE?= true .endif diff --git a/share/mk/newlog.sh b/share/mk/newlog.sh new file mode 100755 --- /dev/null +++ b/share/mk/newlog.sh @@ -0,0 +1,414 @@ +#!/bin/sh + +# NAME: +# newlog - rotate log files +# +# SYNOPSIS: +# newlog.sh [options] "log"[:"num"] ... +# +# DESCRIPTION: +# This script saves multiple generations of each "log". +# The "logs" are kept compressed except for the current and +# previous ones. +# +# Options: +# +# -C "compress" +# Compact old logs (other than .0) with "compress" +# (default is 'gzip' or 'compress' if no 'gzip'). +# +# -E "ext" +# If "compress" produces a file extention other than +# '.Z' or '.gz' we need to know. +# +# -G "gens" +# "gens" is a comma separated list of "log":"num" pairs +# that allows certain logs to handled differently. +# +# -N Don't actually do anything, just show us. +# +# -R Rotate rather than save logs by default. +# This is the default anyway. +# +# -S Save rather than rotate logs by default. +# Each log is saved to a unique name that remains +# unchanged. This results in far less churn. +# +# -f "fmt" +# Format ('%Y%m%d.%H%M%S') for suffix added to "log" to +# uniquely name it when using the '-S' option. +# If a "log" is saved more than once per second we add +# an extra suffix of our process-id. +# +# -d The "log" to be rotated/saved is a directory. +# We leave the mode of old directories alone. +# +# -e Normally logs are only cycled if non-empty, this +# option forces empty logs to be cycled as well. +# +# -g "group" +# Set the group of "log" to "group". +# +# -m "mode" +# Set the mode of "log". +# +# -M "mode" +# Set the mode of old logs (default 444). +# +# -n "num" +# Keep "num" generations of "log". +# +# -o "owner" +# Set the owner of "log". +# +# Regardless of whether '-R' or '-S' is provided, we attempt to +# choose the correct behavior based on observation of "log.0" if +# it exists; if it is a symbolic link, we save, otherwise +# we rotate. +# +# BUGS: +# 'Newlog.sh' tries to avoid being fooled by symbolic links, but +# multiply indirect symlinks are only handled on machines where +# test(1) supports a check for symlinks. +# +# AUTHOR: +# Simon J. Gerraty +# + +# RCSid: +# $Id: newlog.sh,v 1.27 2024/02/17 17:26:57 sjg Exp $ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# @(#) Copyright (c) 1993-2016 Simon J. Gerraty +# +# This file is provided in the hope that it will +# be of use. There is absolutely NO WARRANTY. +# Permission to copy, redistribute or otherwise +# use this file is hereby granted provided that +# the above copyright notice and this notice are +# left intact. +# +# Please send copies of changes and bug-fixes to: +# sjg@crufty.net +# + +Mydir=`dirname $0` +case $Mydir in +/*) ;; +*) Mydir=`cd $Mydir; pwd`;; +esac + +# places to find chown (and setopts.sh) +PATH=$PATH:/usr/etc:/sbin:/usr/sbin:/usr/local/share/bin:/share/bin:$Mydir + +# linux doesn't necessarily have compress, +# and gzip appears in various locations... +Which() { + case "$1" in + -*) t=$1; shift;; + *) t=-x;; + esac + case "$1" in + /*) test $t $1 && echo $1;; + *) + for d in `IFS=:; echo ${2:-$PATH}` + do + test $t $d/$1 && { echo $d/$1; break; } + done + ;; + esac +} + +# shell's typically have test(1) as built-in +# and not all support all options. +test_opt() { + _o=$1 + _a=$2 + _t=${3:-/} + + case `test -$_o $_t 2>&1` in + *:*) eval test_$_o=$_a;; + *) eval test_$_o=-$_o;; + esac +} + +# convert find/ls mode to octal +fmode() { + eval `echo $1 | + sed 's,\(.\)\(...\)\(...\)\(...\),ft=\1 um=\2 gm=\3 om=\4,'` + sm= + case "$um" in + *s*) sm=r + um=`echo $um | sed 's,s,x,'` + ;; + *) sm=-;; + esac + case "$gm" in + *[Ss]*) + sm=${sm}w + gm=`echo $gm | sed 's,s,x,;s,S,-,'` + ;; + *) sm=${sm}-;; + esac + case "$om" in + *t) + sm=${sm}x + om=`echo $om | sed 's,t,x,'` + ;; + *) sm=${sm}-;; + esac + echo $sm $um $gm $om | + sed 's,rwx,7,g;s,rw-,6,g;s,r-x,5,g;s,r--,4,g;s,-wx,3,g;s,-w-,2,g;s,--x,1,g;s,---,0,g;s, ,,g' +} + +get_mode() { + case "$OS,$STAT" in + FreeBSD,*) + $STAT -f %Op $1 | sed 's,.*\(....\),\1,' + return + ;; + esac + # fallback to find + fmode `find $1 -ls -prune | awk '{ print $3 }'` +} + +get_mtime_suffix() { + case "$OS,$STAT" in + FreeBSD,*) + $STAT -t "${2:-$opt_f}" -f %Sm $1 + return + ;; + esac + # this will have to do + date "+${2:-$opt_f}" +} + +case /$0 in +*/newlog*) rotate_func=rotate_log;; +*/save*) rotate_func=save_log;; +*) rotate_func=rotate_log;; +esac + +opt_n=7 +opt_m= +opt_M=444 +opt_f=%Y%m%d.%H%M%S +opt_str=dNn:o:g:G:C:M:m:eE:f:RS + +. setopts.sh + +test $# -gt 0 || exit 0 # nothing to do. + +OS=${OS:-`uname`} +STAT=${STAT:-`Which stat`} + +# sorry, setops semantics for booleans changed. +case "${opt_d:-0}" in +0) rm_f=-f + opt_d=-f + for x in $opt_C gzip compress + do + opt_C=`Which $x "/bin:/usr/bin:$PATH"` + test -x $opt_C && break + done + empty() { test ! -s $1; } + ;; +*) rm_f=-rf + opt_d=-d + opt_M= + opt_C=: + empty() { + if [ -d $1 ]; then + n=`'ls' -a1 $1/. | wc -l` + [ $n -gt 2 ] && return 1 + fi + return 0 + } + ;; +esac +case "${opt_N:-0}" in +0) ECHO=;; +*) ECHO=echo;; +esac +case "${opt_e:-0}" in +0) force=;; +*) force=yes;; +esac +case "${opt_R:-0}" in +0) ;; +*) rotate_func=rotate_log;; +esac +case "${opt_S:-0}" in +0) ;; +*) rotate_func=save_log opt_S=;; +esac + +# see whether test handles -h or -L +test_opt L -h +test_opt h "" +case "$test_L,$test_h" in +-h,) test_L= ;; # we don't support either! +esac + +case "$test_L" in +"") # No, so this is about all we can do... + logs=`'ls' -ld $* | awk '{ print $NF }'` + ;; +*) # it does + logs="$*" + ;; +esac + +read_link() { + case "$test_L" in + "") 'ls' -ld $1 | awk '{ print $NF }'; return;; + esac + if test $test_L $1; then + 'ls' -ld $1 | sed 's,.*> ,,' + else + echo $1 + fi +} + +# create the new log +new_log() { + log=$1 + mode=$2 + if test "x$opt_M" != x; then + $ECHO chmod $opt_M $log.0 2> /dev/null + fi + # someone may have managed to write to it already + # so don't truncate it. + case "$opt_d" in + -d) $ECHO mkdir -p $log;; + *) $ECHO touch $log;; + esac + # the order here matters + test "x$opt_o" = x || $ECHO chown $opt_o $log + test "x$opt_g" = x || $ECHO chgrp $opt_g $log + test "x$mode" = x || $ECHO chmod $mode $log +} + +rotate_log() { + log=$1 + n=${2:-$opt_n} + + # make sure excess generations are trimmed + $ECHO rm $rm_f `echo $log.$n | sed 's/\([0-9]\)$/[\1-9]*/'` + + mode=${opt_m:-`get_mode $log`} + while test $n -gt 0 + do + p=`expr $n - 1` + if test -s $log.$p; then + $ECHO rm $rm_f $log.$p.* + $ECHO $opt_C $log.$p + if test "x$opt_M" != x; then + $ECHO chmod $opt_M $log.$p.* 2> /dev/null + fi + fi + for ext in $opt_E .gz .Z "" + do + test $opt_d $log.$p$ext || continue + $ECHO mv $log.$p$ext $log.$n$ext + done + n=$p + done + # leave $log.0 uncompressed incase some one still has it open. + $ECHO mv $log $log.0 + new_log $log $mode +} + +# unlike rotate_log we do not rotate files, +# but give each log a unique (but stable name). +# This avoids churn for folk who rsync things. +# We make log.0 a symlink to the most recent log +# so it can be found and compressed next time around. +save_log() { + log=$1 + n=${2:-$opt_n} + fmt=$3 + + last=`read_link $log.0` + case "$last" in + $log.0) # should never happen + test -s $last && $ECHO mv $last $log.$$;; + $log.*) + $ECHO $opt_C $last + ;; + *.*) $ECHO $opt_C `dirname $log`/$last + ;; + esac + $ECHO rm -f $log.0 + # remove excess logs - we rely on mtime! + $ECHO rm $rm_f `'ls' -1td $log.* 2> /dev/null | sed "1,${n}d"` + + mode=${opt_m:-`get_mode $log`} + # this is our default suffix + opt_S=${opt_S:-`get_mtime_suffix $log $fmt`} + case "$fmt" in + ""|$opt_f) suffix=$opt_S;; + *) suffix=`get_mtime_suffix $log $fmt`;; + esac + + # find a unique name to save current log as + for nlog in $log.$suffix $log.$suffix.$$ + do + for f in $nlog* + do + break + done + test $opt_d $f || break + done + # leave $log.0 uncompressed incase some one still has it open. + $ECHO mv $log $nlog + test "x$opt_M" = x || $ECHO chmod $opt_M $nlog 2> /dev/null + $ECHO ln -s `basename $nlog` $log.0 + new_log $log $mode +} + +for f in $logs +do + n=$opt_n + save= + case "$f" in + *:[1-9]*) + set -- `IFS=:; echo $f`; f=$1; n=$2;; + *:n=*|*:save=*) + eval `echo "f=$f" | tr ':' ' '`;; + esac + # try and pick the right function to use + rfunc=$rotate_func # default + if test $opt_d $f.0; then + case `read_link $f.0` in + $f.0) rfunc=rotate_log;; + *) rfunc=save_log;; + esac + fi + case "$test_L" in + -?) + while test $test_L $f # it is [still] a symlink + do + f=`read_link $f` + done + ;; + esac + case ",${opt_G}," in + *,${f}:n=*|,${f}:save=*) + eval `echo ",${opt_G}," | sed "s!.*,${f}:\([^,]*\),.*!\1!;s,:, ,g"` + ;; + *,${f}:*) + # opt_G is a , separated list of log:n pairs + n=`echo ,$opt_G, | sed -e "s,.*${f}:\([0-9][0-9]*\).*,\1,"` + ;; + esac + + if empty $f; then + test "$force" || continue + fi + + test "$save" && rfunc=save_log + + $rfunc $f $n $save +done diff --git a/share/mk/setopts.sh b/share/mk/setopts.sh new file mode 100755 --- /dev/null +++ b/share/mk/setopts.sh @@ -0,0 +1,175 @@ +: +# NAME: +# setopts.sh - set opt_* for shell scripts +# +# SYNOPSIS: +# opt_str=s:a.b^cl,z= +# opt_a=default +# +# . setopts.sh +# +# DESCRIPTION: +# This module sets shell variables for each option specified in +# "opt_str". +# +# If the option is followed by a ``:'' it requires an argument. +# It defaults to an empty string and specifying that option on +# the command line overrides the current value. +# +# If the option is followed by a ``.'' then it is treated as for +# ``:'' except that any argument provided on the command line is +# appended to the current value using the value of "opt_dot" as +# separator (default is a space). +# +# If the option is followed by a ``,'' then it is treated as for +# a ``.'' except that the separator is "opt_comma" (default ,). +# +# If the option is followed by ``='' it requires an argument +# of the form "var=val" which will be evaluated. +# +# If the option is followed by a ``^'' then it is treated as a +# boolean and defaults to 0. +# +# Options that have no qualifier are set to the flag if present +# otherwise they are unset. That is if '-c' is given then +# "opt_c" will be set to '-c'. +# +# If "opt_assign_eval" is set (and to something other than +# 'no'), args of the form "var=val" will be evaluated. +# +# NOTES: +# The implementation uses the getopts builtin if available. +# +# Also it does not work when loaded via a function call as "$@" +# will be the args to that function. In such cases set +# _SETOPTS_DELAY and call 'setopts "$@"; shift $__shift' +# afterwards. +# +# AUTHOR: +# Simon J. Gerraty +# + +# RCSid: +# $Id: setopts.sh,v 1.13 2023/02/20 19:30:06 sjg Exp $ +# +# @(#) Copyright (c) 1995-2023 Simon J. Gerraty +# +# This file is provided in the hope that it will +# be of use. There is absolutely NO WARRANTY. +# Permission to copy, redistribute or otherwise +# use this file is hereby granted provided that +# the above copyright notice and this notice are +# left intact. +# +# Please send copies of changes and bug-fixes to: +# sjg@crufty.net +# + +# the case checks just skip the sed(1) commands unless needed +case "$opt_str" in +*\^*) # the only ones we need to set are the booleans x, + eval `echo $opt_str | sed -e 's/[^^]*$//' -e 's/[^^]*\([^^]^\)/\1/g' -e 's/\(.\)^/opt_\1=${opt_\1-0}; /g'` + ;; +esac +case "$opt_str" in +*[=,.\^]*) + _opt_str=`echo $opt_str | sed -e 's/[=,.]/:/g' -e 's/\^//g'`;; +*) _opt_str=$opt_str;; +esac + +opt_append=${opt_append:-" "} +opt_dot=${opt_dot:-$opt_append} +opt_comma=${opt_comma:-,} + +set1opt() { + o=$1 + a="$2" + + case "$opt_str" in + *${o}:*) eval "opt_$o=\"$a\"";; + *${o}.*) eval "opt_$o=\"\${opt_$o}\${opt_$o:+$opt_dot}$a\"";; + *${o},*) eval "opt_$o=\"\${opt_$o}\${opt_$o:+$opt_comma}$a\"";; + *${o}=*) + case "$a" in + *=*) eval "$a";; + *) Myname=${Myname:-`basename $0 .sh`} + echo "$Myname: -$o requires argument of form var=val" >&2 + exit 1 + ;; + esac + ;; + *${o}\^*) eval opt_$o=1;; + *) eval opt_$o=-$o;; + esac +} + +setopts() { + __shift=$# + # use getopts builtin if we can + case `type getopts 2>&1` in + *builtin*) + : OPTIND=$OPTIND @="$@" + while getopts $_opt_str o + do + case "$o" in + \?) exit 1;; + esac + set1opt $o "$OPTARG" + done + shift $(($OPTIND - 1)) + while : + do + case "$1" in + *=*) + case "$opt_assign_eval" in + ""|no) break;; + *) eval "$1"; shift;; + esac + ;; + *) break;; + esac + done + ;; + *) # likely not a POSIX shell either + # getopt(1) isn't as good + set -- `getopt $_opt_str "$@" 2>&1` + case "$1" in + getopt:) + Myname=${Myname:-`basename $0 .sh`} + echo "$*" | tr ':' '\012' | sed -e '/^getopt/d' -e 's/ getopt$//' -e "s/^/$Myname:/" -e 's/ --/:/' -e 's/-.*//' 2>&2 + exit 1 + ;; + esac + + while : + do + : 1="$1" + case "$1" in + --) shift; break;; + -*) + # Most shells give you ' ' in IFS whether you + # want it or not, but at least one, doesn't. + # So the following gives us consistency. + o=`IFS=" -"; set -- $1; echo $*` # lose the '-' + set1opt $o "$2" + case "$_opt_str" in + *${o}:*) shift;; + esac + ;; + *=*) case "$opt_assign_eval" in + ""|no) break;; + *) eval "$1";; + esac + ;; + *) break;; + esac + shift + done + ;; + esac + # let caller know how many args we consumed + __shift=`expr $__shift - $#` +} + +${_SETOPTS_DELAY:+:} setopts "$@" +${_SETOPTS_DELAY:+:} shift $__shift