xref: /freebsd/contrib/bmake/mk/newlog.sh (revision c60f6422ffae3ea85e7b10bad950ad27c463af18)
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