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