xref: /freebsd/contrib/bmake/mk/newlog.sh (revision d0b2dbfa)
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.26 2021/04/30 16:29:02 sjg Exp $
80#
81#	@(#) Copyright (c) 1993-2016 Simon J. Gerraty
82#
83#	This file is provided in the hope that it will
84#	be of use.  There is absolutely NO WARRANTY.
85#	Permission to copy, redistribute or otherwise
86#	use this file is hereby granted provided that
87#	the above copyright notice and this notice are
88#	left intact.
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	esac
170	# fallback to find
171	fmode `find $1 -ls -prune | awk '{ print $3 }'`
172}
173
174get_mtime_suffix() {
175	case "$OS,$STAT" in
176	FreeBSD,*)
177		$STAT -t "${2:-$opt_f}" -f %Sm $1
178		return
179		;;
180	esac
181	# this will have to do
182	date "+${2:-$opt_f}"
183}
184
185case /$0 in
186*/newlog*) rotate_func=rotate_log;;
187*/save*) rotate_func=save_log;;
188*) rotate_func=rotate_log;;
189esac
190
191opt_n=7
192opt_m=
193opt_M=444
194opt_f=%Y%m%d.%H%M%S
195opt_str=dNn:o:g:G:C:M:m:eE:f:RS
196
197. setopts.sh
198
199test $# -gt 0 || exit 0	# nothing to do.
200
201OS=${OS:-`uname`}
202STAT=${STAT:-`Which stat`}
203
204# sorry, setops semantics for booleans changed.
205case "${opt_d:-0}" in
2060)	rm_f=-f
207	opt_d=-f
208	for x in $opt_C gzip compress
209	do
210		opt_C=`Which $x "/bin:/usr/bin:$PATH"`
211		test -x $opt_C && break
212	done
213	empty() { test ! -s $1; }
214	;;
215*)	rm_f=-rf
216	opt_d=-d
217	opt_M=
218	opt_C=:
219	empty() {
220	    if [ -d $1 ]; then
221		n=`'ls' -a1 $1/. | wc -l`
222		[ $n -gt 2 ] && return 1
223	    fi
224	    return 0
225	}
226	;;
227esac
228case "${opt_N:-0}" in
2290)	ECHO=;;
230*)	ECHO=echo;;
231esac
232case "${opt_e:-0}" in
2330)	force=;;
234*)	force=yes;;
235esac
236case "${opt_R:-0}" in
2370) ;;
238*) rotate_func=rotate_log;;
239esac
240case "${opt_S:-0}" in
2410) ;;
242*) rotate_func=save_log opt_S=;;
243esac
244
245# see whether test handles -h or -L
246test_opt L -h
247test_opt h ""
248case "$test_L,$test_h" in
249-h,) test_L= ;;			# we don't support either!
250esac
251
252case "$test_L" in
253"")	# No, so this is about all we can do...
254	logs=`'ls' -ld $* | awk '{ print $NF }'`
255	;;
256*)	# it does
257	logs="$*"
258	;;
259esac
260
261read_link() {
262	case "$test_L" in
263	"")	'ls' -ld $1 | awk '{ print $NF }'; return;;
264	esac
265	if test $test_L $1; then
266		'ls' -ld $1 | sed 's,.*> ,,'
267	else
268		echo $1
269	fi
270}
271
272# create the new log
273new_log() {
274	log=$1
275	mode=$2
276	if test "x$opt_M" != x; then
277		$ECHO chmod $opt_M $log.0 2> /dev/null
278	fi
279	# someone may have managed to write to it already
280	# so don't truncate it.
281	case "$opt_d" in
282	-d) $ECHO mkdir -p $log;;
283	*) $ECHO touch $log;;
284	esac
285	# the order here matters
286	test "x$opt_o" = x || $ECHO chown $opt_o $log
287	test "x$opt_g" = x || $ECHO chgrp $opt_g $log
288	test "x$mode" = x || $ECHO chmod $mode $log
289}
290
291rotate_log() {
292	log=$1
293	n=${2:-$opt_n}
294
295	# make sure excess generations are trimmed
296	$ECHO rm $rm_f `echo $log.$n | sed 's/\([0-9]\)$/[\1-9]*/'`
297
298	mode=${opt_m:-`get_mode $log`}
299	while test $n -gt 0
300	do
301		p=`expr $n - 1`
302		if test -s $log.$p; then
303			$ECHO rm $rm_f $log.$p.*
304			$ECHO $opt_C $log.$p
305			if test "x$opt_M" != x; then
306				$ECHO chmod $opt_M $log.$p.* 2> /dev/null
307			fi
308		fi
309		for ext in $opt_E .gz .Z ""
310		do
311			test $opt_d $log.$p$ext || continue
312			$ECHO mv $log.$p$ext $log.$n$ext
313		done
314		n=$p
315	done
316	# leave $log.0 uncompressed incase some one still has it open.
317	$ECHO mv $log $log.0
318	new_log $log $mode
319}
320
321# unlike rotate_log we do not rotate files,
322# but give each log a unique (but stable name).
323# This avoids churn for folk who rsync things.
324# We make log.0 a symlink to the most recent log
325# so it can be found and compressed next time around.
326save_log() {
327	log=$1
328	n=${2:-$opt_n}
329	fmt=$3
330
331	last=`read_link $log.0`
332	case "$last" in
333	$log.0) # should never happen
334		test -s $last && $ECHO mv $last $log.$$;;
335	$log.*)
336		$ECHO $opt_C $last
337		;;
338	*.*)	$ECHO $opt_C `dirname $log`/$last
339		;;
340	esac
341	$ECHO rm -f $log.0
342	# remove excess logs - we rely on mtime!
343	$ECHO rm $rm_f `'ls' -1td $log.* 2> /dev/null | sed "1,${n}d"`
344
345	mode=${opt_m:-`get_mode $log`}
346	# this is our default suffix
347	opt_S=${opt_S:-`get_mtime_suffix $log $fmt`}
348	case "$fmt" in
349	""|$opt_f) suffix=$opt_S;;
350	*) suffix=`get_mtime_suffix $log $fmt`;;
351	esac
352
353	# find a unique name to save current log as
354	for nlog in $log.$suffix $log.$suffix.$$
355	do
356		for f in $nlog*
357		do
358			break
359		done
360		test $opt_d $f || break
361	done
362	# leave $log.0 uncompressed incase some one still has it open.
363	$ECHO mv $log $nlog
364	test "x$opt_M" = x || $ECHO chmod $opt_M $nlog 2> /dev/null
365	$ECHO ln -s `basename $nlog` $log.0
366	new_log $log $mode
367}
368
369for f in $logs
370do
371	n=$opt_n
372	save=
373	case "$f" in
374	*:[1-9]*)
375		set -- `IFS=:; echo $f`; f=$1; n=$2;;
376	*:n=*|*:save=*)
377		eval `echo "f=$f" | tr ':' ' '`;;
378	esac
379	# try and pick the right function to use
380	rfunc=$rotate_func	# default
381	if test $opt_d $f.0; then
382		case `read_link $f.0` in
383		$f.0) rfunc=rotate_log;;
384		*) rfunc=save_log;;
385		esac
386	fi
387	case "$test_L" in
388	-?)
389		while test $test_L $f	# it is [still] a symlink
390		do
391			f=`read_link $f`
392		done
393		;;
394	esac
395	case ",${opt_G}," in
396	*,${f}:n=*|,${f}:save=*)
397		eval `echo ",${opt_G}," | sed "s!.*,${f}:\([^,]*\),.*!\1!;s,:, ,g"`
398		;;
399	*,${f}:*)
400		# opt_G is a , separated list of log:n pairs
401		n=`echo ,$opt_G, | sed -e "s,.*${f}:\([0-9][0-9]*\).*,\1,"`
402		;;
403	esac
404
405	if empty $f; then
406		test "$force" || continue
407	fi
408
409	test "$save" && rfunc=save_log
410
411	$rfunc $f $n $save
412done
413