1#!/bin/sh 2 3# NAME: 4# newlog - rotate log files 5# 6# SYNOPSIS: 7# newlog.sh [options] "log"[:"num"] ... 8# 9# DESCRIPTION: 10# This script saves multiple generations of each "log". 11# The "logs" are kept compressed except for the current and 12# previous ones. 13# 14# Options: 15# 16# -C "compress" 17# Compact old logs (other than .0) with "compress" 18# (default is "$NEWLOG_COMPRESS" 'gzip' or 'compress' if 19# no 'gzip'). 20# 21# -E "ext" 22# If "compress" produces a file extention other than 23# '.Z' or '.gz' we need to know ("$NEWLOG_EXT"). 24# 25# -G "gens" 26# "gens" is a comma separated list of "log":"num" pairs 27# that allows certain logs to handled differently. 28# 29# -N Don't actually do anything, just show us. 30# 31# -R Rotate rather than save logs by default. 32# This is the default anyway. 33# 34# -S Save rather than rotate logs by default. 35# Each log is saved to a unique name that remains 36# unchanged. This results in far less churn. 37# 38# -f "fmt" 39# Format ('%Y%m%d.%H%M%S') for suffix added to "log" to 40# uniquely name it when using the '-S' option. 41# If a "log" is saved more than once per second we add 42# an extra suffix of our process-id. 43# The default can be set in the env via "$NEWLOG_FMT". 44# 45# -d The "log" to be rotated/saved is a directory. 46# We leave the mode of old directories alone. 47# 48# -e Normally logs are only cycled if non-empty, this 49# option forces empty logs to be cycled as well. 50# 51# -g "group" 52# Set the group of "log" to "group". 53# 54# -m "mode" 55# Set the mode of "log" ("$NEWLOG_MODE"). 56# 57# -M "mode" 58# Set the mode of old logs (default "$NEWLOG_OLD_MODE" 59# or 444). 60# 61# -n "num" 62# Keep "num" generations of "log" ("$NEWLOG_NUM"). 63# 64# -o "owner" 65# Set the owner of "log". 66# 67# The default method for dealing with logs can be set via 68# "$NEWLOG_METHOD" ('save' or 'rotate'). 69# Regardless of "$NEWLOG_METHOD" or whether '-R' or '-S' is 70# provided, we attempt to choose the correct behavior based on 71# observation of "log.0" if it exists; if it is a symbolic link, 72# we 'save', otherwise we 'rotate'. 73# 74# BUGS: 75# 'Newlog.sh' tries to avoid being fooled by symbolic links, but 76# multiply indirect symlinks are only handled on machines where 77# test(1) supports a check for symlinks. 78# 79# AUTHOR: 80# Simon J. Gerraty <sjg@crufty.net> 81# 82 83# RCSid: 84# $Id: newlog.sh,v 1.31 2025/08/07 22:07:13 sjg Exp $ 85# 86# @(#) Copyright (c) 1993-2025 Simon J. Gerraty 87# 88# SPDX-License-Identifier: BSD-2-Clause 89# 90# Please send copies of changes and bug-fixes to: 91# sjg@crufty.net 92# 93 94Mydir=`dirname $0` 95case $Mydir in 96/*) ;; 97*) Mydir=`cd $Mydir; pwd`;; 98esac 99 100# places to find chown (and setopts.sh) 101PATH=$PATH:/usr/etc:/sbin:/usr/sbin:/usr/local/share/bin:/share/bin:$Mydir 102 103# linux doesn't necessarily have compress, 104# and gzip appears in various locations... 105Which() { 106 case "$1" in 107 -*) t=$1; shift;; 108 *) t=-x;; 109 esac 110 case "$1" in 111 /*) test $t $1 && echo $1;; 112 *) 113 for d in `IFS=:; echo ${2:-$PATH}` 114 do 115 test $t $d/$1 && { echo $d/$1; break; } 116 done 117 ;; 118 esac 119} 120 121# shell's typically have test(1) as built-in 122# and not all support all options. 123test_opt() { 124 _o=$1 125 _a=$2 126 _t=${3:-/} 127 128 case `test -$_o $_t 2>&1` in 129 *:*) eval test_$_o=$_a;; 130 *) eval test_$_o=-$_o;; 131 esac 132} 133 134# convert find/ls mode to octal 135fmode() { 136 eval `echo $1 | 137 sed 's,\(.\)\(...\)\(...\)\(...\),ft=\1 um=\2 gm=\3 om=\4,'` 138 sm= 139 case "$um" in 140 *s*) sm=r 141 um=`echo $um | sed 's,s,x,'` 142 ;; 143 *) sm=-;; 144 esac 145 case "$gm" in 146 *[Ss]*) 147 sm=${sm}w 148 gm=`echo $gm | sed 's,s,x,;s,S,-,'` 149 ;; 150 *) sm=${sm}-;; 151 esac 152 case "$om" in 153 *t) 154 sm=${sm}x 155 om=`echo $om | sed 's,t,x,'` 156 ;; 157 *) sm=${sm}-;; 158 esac 159 echo $sm $um $gm $om | 160 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' 161} 162 163get_mode() { 164 case "$OS,$STAT" in 165 FreeBSD,*) 166 $STAT -f %Op $1 | sed 's,.*\(....\),\1,' 167 return 168 ;; 169 Linux,$STAT) # works on Ubuntu 170 $STAT -c %a $1 2> /dev/null && 171 return 172 ;; 173 esac 174 # fallback to find 175 fmode `find $1 -ls -prune | awk '{ print $3 }'` 176} 177 178get_mtime_suffix() { 179 case "$OS,$STAT" in 180 FreeBSD,*) 181 $STAT -t "${2:-$opt_f}" -f %Sm $1 182 return 183 ;; 184 Linux,*) # works on Ubuntu 185 mtime=`$STAT --format=%Y $1 2> /dev/null` 186 if [ ${mtime:-0} -gt 1 ]; then 187 date --date=@$mtime "+${2:-$opt_f}" 2> /dev/null && 188 return 189 fi 190 ;; 191 esac 192 # this will have to do 193 date "+${2:-$opt_f}" 194} 195 196case /$0 in 197*/newlog*) rotate_func=${NEWLOG_METHOD:-rotate_log};; 198*/save*) rotate_func=save_log;; 199*) rotate_func=${NEWLOG_METHOD:-rotate_log};; 200esac 201case "$rotate_func" in 202save|rotate) rotate_func=${rotate_func}_log;; 203esac 204 205opt_C=${NEWLOG_COMPRESS} 206opt_E=${NEWLOG_EXT} 207opt_n=${NEWLOG_NUM:-7} 208opt_m=${NEWLOG_MODE} 209opt_M=${NEWLOG_OLD_MODE:-444} 210opt_f=${NEWLOG_FMT:-%Y-%m-%dT%T} # rfc3339 211opt_str=dNn:o:g:G:C:M:m:eE:f:RS 212 213. setopts.sh 214 215test $# -gt 0 || exit 0 # nothing to do. 216 217OS=${OS:-`uname`} 218STAT=${STAT:-`Which stat`} 219 220# sorry, setops semantics for booleans changed. 221case "${opt_d:-0}" in 2220) rm_f=-f 223 opt_d=-f 224 for x in $opt_C gzip compress 225 do 226 opt_C=`Which $x "/bin:/usr/bin:$PATH"` 227 test -x $opt_C && break 228 done 229 empty() { test ! -s $1; } 230 ;; 231*) rm_f=-rf 232 opt_d=-d 233 opt_M= 234 opt_C=: 235 empty() { 236 if [ -d $1 ]; then 237 n=`'ls' -a1 $1/. | wc -l` 238 [ $n -gt 2 ] && return 1 239 fi 240 return 0 241 } 242 ;; 243esac 244case "${opt_N:-0}" in 2450) ECHO=;; 246*) ECHO=echo;; 247esac 248case "${opt_e:-0}" in 2490) force=;; 250*) force=yes;; 251esac 252case "${opt_R:-0}" in 2530) ;; 254*) rotate_func=rotate_log;; 255esac 256case "${opt_S:-0}" in 2570) ;; 258*) rotate_func=save_log;; 259esac 260 261# see whether test handles -h or -L 262test_opt L -h 263test_opt h "" 264case "$test_L,$test_h" in 265-h,) test_L= ;; # we don't support either! 266esac 267 268case "$test_L" in 269"") # No, so this is about all we can do... 270 logs=`'ls' -ld $* | awk '{ print $NF }'` 271 ;; 272*) # it does 273 logs="$*" 274 ;; 275esac 276 277read_link() { 278 case "$test_L" in 279 "") 'ls' -ld $1 | awk '{ print $NF }'; return;; 280 esac 281 if test $test_L $1; then 282 'ls' -ld $1 | sed 's,.*> ,,' 283 else 284 echo $1 285 fi 286} 287 288# create the new log 289new_log() { 290 log=$1 291 mode=$2 292 if test "x$opt_M" != x; then 293 $ECHO chmod $opt_M $log.0 2> /dev/null 294 fi 295 # someone may have managed to write to it already 296 # so don't truncate it. 297 case "$opt_d" in 298 -d) $ECHO mkdir -p $log;; 299 *) $ECHO touch $log;; 300 esac 301 # the order here matters 302 test "x$opt_o" = x || $ECHO chown $opt_o $log 303 test "x$opt_g" = x || $ECHO chgrp $opt_g $log 304 test "x$mode" = x || $ECHO chmod $mode $log 305} 306 307rotate_log() { 308 log=$1 309 n=${2:-$opt_n} 310 311 # make sure excess generations are trimmed 312 $ECHO rm $rm_f `echo $log.$n | sed 's/\([0-9]\)$/[\1-9]*/'` 313 314 mode=${opt_m:-`get_mode $log`} 315 while test $n -gt 0 316 do 317 p=`expr $n - 1` 318 if test -s $log.$p; then 319 $ECHO rm $rm_f $log.$p.* 320 $ECHO $opt_C $log.$p 321 if test "x$opt_M" != x; then 322 $ECHO chmod $opt_M $log.$p.* 2> /dev/null 323 fi 324 fi 325 for ext in $opt_E .gz .Z "" 326 do 327 test $opt_d $log.$p$ext || continue 328 $ECHO mv $log.$p$ext $log.$n$ext 329 done 330 n=$p 331 done 332 # leave $log.0 uncompressed incase some one still has it open. 333 $ECHO mv $log $log.0 334 new_log $log $mode 335} 336 337# unlike rotate_log we do not rotate files, 338# but give each log a unique (but stable name). 339# This avoids churn for folk who rsync things. 340# We make log.0 a symlink to the most recent log 341# so it can be found and compressed next time around. 342save_log() { 343 log=$1 344 n=${2:-$opt_n} 345 fmt=$3 346 347 last=`read_link $log.0` 348 case "$last" in 349 $log.0) # should never happen 350 test -s $last && $ECHO mv $last $log.$$;; 351 $log.*) 352 $ECHO $opt_C $last 353 ;; 354 *.*) $ECHO $opt_C `dirname $log`/$last 355 ;; 356 esac 357 $ECHO rm -f $log.0 358 # remove excess logs - we rely on mtime! 359 $ECHO rm $rm_f `'ls' -1td $log.* 2> /dev/null | sed "1,${n}d"` 360 361 mode=${opt_m:-`get_mode $log`} 362 suffix=`get_mtime_suffix $log $fmt` 363 364 # find a unique name to save current log as 365 for nlog in $log.$suffix $log.$suffix.$$ 366 do 367 for f in $nlog* 368 do 369 break 370 done 371 test $opt_d $f || break 372 done 373 # leave $log.0 uncompressed incase some one still has it open. 374 $ECHO mv $log $nlog 375 test "x$opt_M" = x || $ECHO chmod $opt_M $nlog 2> /dev/null 376 $ECHO ln -s `basename $nlog` $log.0 377 new_log $log $mode 378} 379 380for f in $logs 381do 382 n=$opt_n 383 save= 384 case "$f" in 385 *:[1-9]*) 386 set -- `IFS=:; echo $f`; f=$1; n=$2;; 387 *:n=*|*:save=*) 388 eval `echo "f=$f" | tr ':' ' '`;; 389 esac 390 # try and pick the right function to use 391 rfunc=$rotate_func # default 392 if test $opt_d $f.0; then 393 case `read_link $f.0` in 394 $f.0) rfunc=rotate_log;; 395 *) rfunc=save_log;; 396 esac 397 fi 398 case "$test_L" in 399 -?) 400 while test $test_L $f # it is [still] a symlink 401 do 402 f=`read_link $f` 403 done 404 ;; 405 esac 406 case ",${opt_G}," in 407 *,${f}:n=*|,${f}:save=*) 408 eval `echo ",${opt_G}," | sed "s!.*,${f}:\([^,]*\),.*!\1!;s,:, ,g"` 409 ;; 410 *,${f}:*) 411 # opt_G is a , separated list of log:n pairs 412 n=`echo ,$opt_G, | sed -e "s,.*${f}:\([0-9][0-9]*\).*,\1,"` 413 ;; 414 esac 415 416 if empty $f; then 417 test "$force" || continue 418 fi 419 420 test "$save" && rfunc=save_log 421 422 $rfunc $f $n $save 423done 424