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.30 2025/06/01 05:07:48 sjg Exp $ 85# 86# SPDX-License-Identifier: BSD-2-Clause 87# 88# @(#) Copyright (c) 1993-2025 Simon J. Gerraty 89# 90# This file is provided in the hope that it will 91# be of use. There is absolutely NO WARRANTY. 92# Permission to copy, redistribute or otherwise 93# use this file is hereby granted provided that 94# the above copyright notice and this notice are 95# left intact. 96# 97# Please send copies of changes and bug-fixes to: 98# sjg@crufty.net 99# 100 101Mydir=`dirname $0` 102case $Mydir in 103/*) ;; 104*) Mydir=`cd $Mydir; pwd`;; 105esac 106 107# places to find chown (and setopts.sh) 108PATH=$PATH:/usr/etc:/sbin:/usr/sbin:/usr/local/share/bin:/share/bin:$Mydir 109 110# linux doesn't necessarily have compress, 111# and gzip appears in various locations... 112Which() { 113 case "$1" in 114 -*) t=$1; shift;; 115 *) t=-x;; 116 esac 117 case "$1" in 118 /*) test $t $1 && echo $1;; 119 *) 120 for d in `IFS=:; echo ${2:-$PATH}` 121 do 122 test $t $d/$1 && { echo $d/$1; break; } 123 done 124 ;; 125 esac 126} 127 128# shell's typically have test(1) as built-in 129# and not all support all options. 130test_opt() { 131 _o=$1 132 _a=$2 133 _t=${3:-/} 134 135 case `test -$_o $_t 2>&1` in 136 *:*) eval test_$_o=$_a;; 137 *) eval test_$_o=-$_o;; 138 esac 139} 140 141# convert find/ls mode to octal 142fmode() { 143 eval `echo $1 | 144 sed 's,\(.\)\(...\)\(...\)\(...\),ft=\1 um=\2 gm=\3 om=\4,'` 145 sm= 146 case "$um" in 147 *s*) sm=r 148 um=`echo $um | sed 's,s,x,'` 149 ;; 150 *) sm=-;; 151 esac 152 case "$gm" in 153 *[Ss]*) 154 sm=${sm}w 155 gm=`echo $gm | sed 's,s,x,;s,S,-,'` 156 ;; 157 *) sm=${sm}-;; 158 esac 159 case "$om" in 160 *t) 161 sm=${sm}x 162 om=`echo $om | sed 's,t,x,'` 163 ;; 164 *) sm=${sm}-;; 165 esac 166 echo $sm $um $gm $om | 167 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' 168} 169 170get_mode() { 171 case "$OS,$STAT" in 172 FreeBSD,*) 173 $STAT -f %Op $1 | sed 's,.*\(....\),\1,' 174 return 175 ;; 176 Linux,$STAT) # works on Ubuntu 177 $STAT -c %a $1 2> /dev/null && 178 return 179 ;; 180 esac 181 # fallback to find 182 fmode `find $1 -ls -prune | awk '{ print $3 }'` 183} 184 185get_mtime_suffix() { 186 case "$OS,$STAT" in 187 FreeBSD,*) 188 $STAT -t "${2:-$opt_f}" -f %Sm $1 189 return 190 ;; 191 Linux,*) # works on Ubuntu 192 mtime=`$STAT --format=%Y $1 2> /dev/null` 193 if [ ${mtime:-0} -gt 1 ]; then 194 date --date=@$mtime "+${2:-$opt_f}" 2> /dev/null && 195 return 196 fi 197 ;; 198 esac 199 # this will have to do 200 date "+${2:-$opt_f}" 201} 202 203case /$0 in 204*/newlog*) rotate_func=${NEWLOG_METHOD:-rotate_log};; 205*/save*) rotate_func=save_log;; 206*) rotate_func=${NEWLOG_METHOD:-rotate_log};; 207esac 208case "$rotate_func" in 209save|rotate) rotate_func=${rotate_func}_log;; 210esac 211 212opt_C=${NEWLOG_COMPRESS} 213opt_E=${NEWLOG_EXT} 214opt_n=${NEWLOG_NUM:-7} 215opt_m=${NEWLOG_MODE} 216opt_M=${NEWLOG_OLD_MODE:-444} 217opt_f=${NEWLOG_FMT:-%Y-%m-%dT%T} # rfc3339 218opt_str=dNn:o:g:G:C:M:m:eE:f:RS 219 220. setopts.sh 221 222test $# -gt 0 || exit 0 # nothing to do. 223 224OS=${OS:-`uname`} 225STAT=${STAT:-`Which stat`} 226 227# sorry, setops semantics for booleans changed. 228case "${opt_d:-0}" in 2290) rm_f=-f 230 opt_d=-f 231 for x in $opt_C gzip compress 232 do 233 opt_C=`Which $x "/bin:/usr/bin:$PATH"` 234 test -x $opt_C && break 235 done 236 empty() { test ! -s $1; } 237 ;; 238*) rm_f=-rf 239 opt_d=-d 240 opt_M= 241 opt_C=: 242 empty() { 243 if [ -d $1 ]; then 244 n=`'ls' -a1 $1/. | wc -l` 245 [ $n -gt 2 ] && return 1 246 fi 247 return 0 248 } 249 ;; 250esac 251case "${opt_N:-0}" in 2520) ECHO=;; 253*) ECHO=echo;; 254esac 255case "${opt_e:-0}" in 2560) force=;; 257*) force=yes;; 258esac 259case "${opt_R:-0}" in 2600) ;; 261*) rotate_func=rotate_log;; 262esac 263case "${opt_S:-0}" in 2640) ;; 265*) rotate_func=save_log;; 266esac 267 268# see whether test handles -h or -L 269test_opt L -h 270test_opt h "" 271case "$test_L,$test_h" in 272-h,) test_L= ;; # we don't support either! 273esac 274 275case "$test_L" in 276"") # No, so this is about all we can do... 277 logs=`'ls' -ld $* | awk '{ print $NF }'` 278 ;; 279*) # it does 280 logs="$*" 281 ;; 282esac 283 284read_link() { 285 case "$test_L" in 286 "") 'ls' -ld $1 | awk '{ print $NF }'; return;; 287 esac 288 if test $test_L $1; then 289 'ls' -ld $1 | sed 's,.*> ,,' 290 else 291 echo $1 292 fi 293} 294 295# create the new log 296new_log() { 297 log=$1 298 mode=$2 299 if test "x$opt_M" != x; then 300 $ECHO chmod $opt_M $log.0 2> /dev/null 301 fi 302 # someone may have managed to write to it already 303 # so don't truncate it. 304 case "$opt_d" in 305 -d) $ECHO mkdir -p $log;; 306 *) $ECHO touch $log;; 307 esac 308 # the order here matters 309 test "x$opt_o" = x || $ECHO chown $opt_o $log 310 test "x$opt_g" = x || $ECHO chgrp $opt_g $log 311 test "x$mode" = x || $ECHO chmod $mode $log 312} 313 314rotate_log() { 315 log=$1 316 n=${2:-$opt_n} 317 318 # make sure excess generations are trimmed 319 $ECHO rm $rm_f `echo $log.$n | sed 's/\([0-9]\)$/[\1-9]*/'` 320 321 mode=${opt_m:-`get_mode $log`} 322 while test $n -gt 0 323 do 324 p=`expr $n - 1` 325 if test -s $log.$p; then 326 $ECHO rm $rm_f $log.$p.* 327 $ECHO $opt_C $log.$p 328 if test "x$opt_M" != x; then 329 $ECHO chmod $opt_M $log.$p.* 2> /dev/null 330 fi 331 fi 332 for ext in $opt_E .gz .Z "" 333 do 334 test $opt_d $log.$p$ext || continue 335 $ECHO mv $log.$p$ext $log.$n$ext 336 done 337 n=$p 338 done 339 # leave $log.0 uncompressed incase some one still has it open. 340 $ECHO mv $log $log.0 341 new_log $log $mode 342} 343 344# unlike rotate_log we do not rotate files, 345# but give each log a unique (but stable name). 346# This avoids churn for folk who rsync things. 347# We make log.0 a symlink to the most recent log 348# so it can be found and compressed next time around. 349save_log() { 350 log=$1 351 n=${2:-$opt_n} 352 fmt=$3 353 354 last=`read_link $log.0` 355 case "$last" in 356 $log.0) # should never happen 357 test -s $last && $ECHO mv $last $log.$$;; 358 $log.*) 359 $ECHO $opt_C $last 360 ;; 361 *.*) $ECHO $opt_C `dirname $log`/$last 362 ;; 363 esac 364 $ECHO rm -f $log.0 365 # remove excess logs - we rely on mtime! 366 $ECHO rm $rm_f `'ls' -1td $log.* 2> /dev/null | sed "1,${n}d"` 367 368 mode=${opt_m:-`get_mode $log`} 369 suffix=`get_mtime_suffix $log $fmt` 370 371 # find a unique name to save current log as 372 for nlog in $log.$suffix $log.$suffix.$$ 373 do 374 for f in $nlog* 375 do 376 break 377 done 378 test $opt_d $f || break 379 done 380 # leave $log.0 uncompressed incase some one still has it open. 381 $ECHO mv $log $nlog 382 test "x$opt_M" = x || $ECHO chmod $opt_M $nlog 2> /dev/null 383 $ECHO ln -s `basename $nlog` $log.0 384 new_log $log $mode 385} 386 387for f in $logs 388do 389 n=$opt_n 390 save= 391 case "$f" in 392 *:[1-9]*) 393 set -- `IFS=:; echo $f`; f=$1; n=$2;; 394 *:n=*|*:save=*) 395 eval `echo "f=$f" | tr ':' ' '`;; 396 esac 397 # try and pick the right function to use 398 rfunc=$rotate_func # default 399 if test $opt_d $f.0; then 400 case `read_link $f.0` in 401 $f.0) rfunc=rotate_log;; 402 *) rfunc=save_log;; 403 esac 404 fi 405 case "$test_L" in 406 -?) 407 while test $test_L $f # it is [still] a symlink 408 do 409 f=`read_link $f` 410 done 411 ;; 412 esac 413 case ",${opt_G}," in 414 *,${f}:n=*|,${f}:save=*) 415 eval `echo ",${opt_G}," | sed "s!.*,${f}:\([^,]*\),.*!\1!;s,:, ,g"` 416 ;; 417 *,${f}:*) 418 # opt_G is a , separated list of log:n pairs 419 n=`echo ,$opt_G, | sed -e "s,.*${f}:\([0-9][0-9]*\).*,\1,"` 420 ;; 421 esac 422 423 if empty $f; then 424 test "$force" || continue 425 fi 426 427 test "$save" && rfunc=save_log 428 429 $rfunc $f $n $save 430done 431