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