style: Silenced Cppcheck warnings
[para.git] / libutil.sh
blobc48a51bf7cd2e0ebf47eb510d04950d0d64a2c5f
1 #!/bin/sh
4 # Shell utility functions
6 # Copyright 2022-2024 Odin Kroeger.
8 # This program is free software: you can redistribute it and/or modify it under
9 # the terms of the GNU General Public License as published by the Free Software
10 # Foundation, either version 3 of the License, or (at your option) any later
11 # version.
13 # This program is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along with
18 # this program. If not, see <https://www.gnu.org/licenses/>.
21 # shellcheck disable=2015,2031
24 # Save the caught signal to $caught.
25 # If $catch is non-empty, also call atexit and reraise the signal.
27 # Usage:
28 # catch signal
30 # Globals:
31 # $catch Whether to call atexit and reraise the signal.
32 # $caught Set to caught signal.
34 catch() {
35 : "${1:?}"
37 warn 'Caught %s' "$1"
38 caught="$1"
39 if [ "${catch-}" ]
40 then
41 atexit
42 trap - "$1"
43 kill -s "$1" "$$"
49 # Disable signal handlers, terminate all jobs and the process group,
50 # run and clear $atexit, and return $?.
52 # Usage:
53 # atexit
55 # Globals:
56 # $atexit Code to be eval-ed.
57 # $_atexit_retval Return value.
59 atexit() {
60 # shellcheck disable=2319
61 _atexit_retval=$?
62 set +e
63 trap '' EXIT ALRM HUP INT TERM USR1 USR2
64 _atexit_jobs="$(jobs -p 2>/dev/null)"
65 # shellcheck disable=2086
66 kill -s TERM $_atexit_jobs -$$ >/dev/null 2>&1
67 # shellcheck disable=2086
68 if [ "$_atexit_jobs" ]
69 then wait $_atexit_jobs
71 unset _atexit_jobs
72 eval "${atexit-}"
73 atexit=
74 wait
75 return $_atexit_retval
80 # Clear the current line of the given file descriptor (default: 2),
81 # but only if the file is a teletype device.
83 # Usage:
84 # clearln [fd]
87 clearln() (
88 _clearln_fd="${1:-2}"
89 if [ -t "$_clearln_fd" ]
90 then printf '\033[0K' >&"$_clearln_fd"
96 # Print a message to standard error and exit.
98 # Usage:
99 # err [-s status] format [argument ...]
101 # Options:
102 # -s status Exit with status (default: 1).
104 err() {
105 _err_status=1
106 OPTIND=1 OPTARG='' _err_opt=''
107 while getopts s: _err_opt
109 case $_err_opt in
110 (s) _err_status="${OPTARG:?}" ;;
111 (*) exit 2
112 esac
113 done
114 unset _err_opt
115 shift $((OPTIND - 1))
116 warn -- "$@"
117 exit "$_err_status"
122 # Enforce POSIX-compliance, register signal handlers, and set globals.
124 # Usage:
125 # init
127 # Globals:
128 # $BIN_SH Set to xpg4.
129 # $CLICOLOR_FORCE Set to the empty string.
130 # $IFS Unset.
131 # $NULLCMD Set to :.
132 # $POSIXLY_CORRECT Set to y.
133 # $catch Set to y.
134 # $caught Set to the empty string.
135 # $progname Set to the basename of $0.
136 # $quiet -- " --.
137 # $verbose -- " --.
139 init() {
140 set +e
141 if [ "${BASH_VERSION-}" ] || [ "${KSH_VERSION-}" ]
142 then
143 # shellcheck disable=3040
144 if ( set -o posix ) 2>/dev/null
145 then set -o posix
147 elif [ "${ZSH_VERSION-}" ]
148 then
149 emulate sh 2>/dev/null
150 setopt POSIX_ALIASES 2>/dev/null
151 setopt POSIX_ARGZERO 2>/dev/null
152 setopt POSIX_BUILTINS 2>/dev/null
153 setopt POSIX_CD 2>/dev/null
154 setopt POSIX_IDENTIFIERS 2>/dev/null
155 setopt POSIX_JOBS 2>/dev/null
156 setopt POSIX_STRINGS 2>/dev/null
157 setopt POSIX_TRAPS 2>/dev/null
159 set -e
161 export BIN_SH=xpg4 NULLCMD=: POSIXLY_CORRECT=y CLICOLOR_FORCE=
163 # Make sure IFS is safe
164 unset IFS
166 # Trap signals that would terminate the script
167 catch='' caught=''
168 # shellcheck disable=2064
169 for _init_sig in ALRM HUP INT TERM USR1 USR2
170 do trap "catch $_init_sig" "$_init_sig"
171 done
172 unset _init_sig
174 trap atexit EXIT
175 catch=y
176 [ "$caught" ] && kill -s "$caught" "$$"
178 # Safe permission mask
179 umask 022
181 # Output control
182 quiet='' verbose=''
184 progname="$(basename -- "$0")" || progname="${0##*/}"
185 readonly progname
190 # Check if $2 matches any member of $@ using operator $1.
192 # Usage:
193 # inlist operator needle [straw ...]
195 inlist() (
196 # shellcheck disable=2034
197 _inlist_op="${1:?}"
198 _inlist_needle="${2?}"
199 shift 2
201 # shellcheck disable=2034
202 for _inlist_straw
203 do test "$_inlist_straw" "$_inlist_op" "$_inlist_needle" && return
204 done
206 return 1
211 # Create a lock file.
213 # Usage:
214 # lock [-t n] file
216 # Options:
217 # -t n Wait n seconds to acquire lock file (default: 3).
219 lock() (
220 OPTARG='' OPTIND=1 _lock_opt=''
221 while getopts 't:' _lock_opt
223 case $_lock_opt in
224 (t) _lock_timeout="$OPTARG" ;;
225 (*) exit 2
226 esac
227 done
228 shift $((OPTIND - 1))
229 unset _lock_opt
231 : "${1:?}"
232 : "${_lock_timeout:=3}"
233 _lock_file="$1"
235 while [ "$_lock_timeout" -gt 0 ]
237 if ( printf '%d\n' "$$" >"$_lock_file"; ) >/dev/null 2>&1
238 then
239 return
240 else
241 if ! read -r _lock_pid <"$_lock_file" ||
242 ! [ "$_lock_pid" ] ||
243 ! kill -0 -- "$_lock_pid" >/dev/null 2>&1
244 then
245 warn -q 'Removing %s... ' "$_lock_file"
246 rm -f "$_lock_file"
247 elif [ "$_lock_timeout" -gt 0 ]
248 then
249 sleep 1
252 _lock_timeout=$((_lock_timeout - 1))
253 done
255 warn 'Could not acquire %s' "$_lock_file"
256 return 1
261 # Run $@ and redirect its output to a file unless $verbose is set.
263 # Usage:
264 # logged command [argument ...]
266 # Options:
267 # -d dir Store log file in dir (default: .).
268 # -i status Do not save output if $@ exits with status (default: 0).
269 # -l file Store output in file (default: basename of $1).
270 # -u user Make user the owner of file (default: current user).
271 # -g group Make group the group of file (default: current group).
273 logged() (
274 OPTIND=1 OPTARG='' _logged_opt=''
275 while getopts 'd:i:l:' _logged_opt
277 case $_logged_opt in
278 (d) _logged_dir="${OPTARG:?}" ;;
279 (i) _logged_mask="$_logged_mask${OPTARG:?} " ;;
280 (l) _logged_fname="${OPTARG:?}" ;;
281 (*) return 1
282 esac
283 done
284 shift $((OPTIND - 1))
286 : "${1:?}"
287 : "${_logged_dir:=.}"
288 : "${_logged_fname:=}"
289 : "${_logged_mask:=0}"
291 if [ "$verbose" ]
292 then
293 set +e
294 "$@"
295 return
298 # shellcheck disable=2030
299 : "${TMPDIR:=/tmp}"
300 : "${_logged_fname:="$(basename "$1").log"}"
301 : "${_logged_fname:?}"
302 _logged_log="$TMPDIR/$_logged_fname"
304 set +e
305 "$@" >>"$_logged_log" 2>&1
306 _logged_xstatus=$?
307 set -e
309 # shellcheck disable=2086
310 if inlist -eq "$_logged_xstatus" $_logged_mask
311 then
312 rm -f "$TMPDIR/$_logged_fname" >/dev/null 2>&1
313 else
314 warn '%s: Exited with status %d' "$1" "$_logged_xstatus"
315 if mv "$_logged_log" "$_logged_dir" 2>/dev/null
316 then warn -q 'See %s for details' "$_logged_fname"
320 return $_logged_xstatus
325 # Create a temporary directory with the filename $1-$$ in $2,
326 # register it for deletion via $atexit, and set it as $TMPDIR.
328 # Globals:
329 # $TMPDIR Set to the created directory.
330 # $atexit Updated to reigster deletion of temporary directory.
332 mktmpdir() {
333 [ "${_mktmpdir_tmpdir-}" ] && return
335 OPTIND=1 OPTARG='' _mktmpdir_opt=''
336 while getopts 'p:d:' _mktmpdir_opt
338 case $_mktmpdir_opt in
339 (d) _mktmpdir_dir="$OPTARG" ;;
340 (p) _mktmpdir_prefix="$OPTARG" ;;
341 (*) exit 2
342 esac
343 done
344 shift $((OPTIND - 1))
345 unset _mktmpdir_opt
347 : "${_mktmpdir_dir:="${TMPDIR:-/tmp}"}"
348 : "${_mktmpdir_prefix:=tmp}"
350 catch=
351 readonly _mktmpdir_tmpdir="$_mktmpdir_dir/$_mktmpdir_prefix-$$"
352 mkdir -m 0755 "$_mktmpdir_tmpdir" || exit
353 atexit="rm -rf \"\$_mktmpdir_tmpdir\"; ${atexit-:}"
354 catch=y
355 [ "${caught-}" ] && kill -s "$caught" "$$"
356 unset _mktmpdir_dir _mktmpdir_prefix
358 export TMPDIR="$_mktmpdir_tmpdir"
363 # Return to the beginning of the current line of the given file
364 # descriptor (default: 2), but only if the file is a teletype device.
366 # Usage:
367 # rewindln [fd]
370 rewindln() (
371 _rewindln_fd="${1:-2}"
372 if [ -t "$_rewindln_fd" ]
373 then printf '\r' >&"$_rewindln_fd"
379 # Show and update a spinner on the given file descriptor (default: 2),
380 # but only if that file is a teletype device and $quiet is unset.
382 # Usage:
383 # spin [fd]
385 # Globals:
386 # $quiet Be quiet?
388 spin() {
389 _spin_fd="${1:-2}"
391 if ! [ "$quiet" ] && [ -t "$_spin_fd" ]
392 then
393 : "${_spin_counter:=0}"
395 case $((_spin_counter % 4)) in
396 (0) printf '\r-\r' >&"$_spin_fd" ;;
397 (1) printf '\r\\\r' >&"$_spin_fd" ;;
398 (2) printf '\r|\r' >&"$_spin_fd" ;;
399 (3) printf '\r/\r' >&"$_spin_fd" ;;
400 esac
402 _spin_counter=$((_spin_counter + 1))
404 unset _spin_fd
409 # Remove a lock file.
411 # Usage:
412 # unlock file
414 unlock() (
415 _unlock_file="${1:?}"
416 if read -r _unlock_pid <"$_unlock_file" &&
417 [ "$_unlock_pid" -eq $$ ]
418 then rm -f "$_unlock_file"
424 # Print a message to standard error.
426 # Options:
427 # -n Suppress the terminating newline.
428 # -q Suppress output if $quiet is set.
429 # -v Suppress output unless $verbose is set.
431 warn() (
432 : "${1:?}"
434 OPTIND=1 OPTARG='' _warn_opt=''
435 while getopts 'nqv' _warn_opt
437 case $_warn_opt in
438 (n) _warn_nl= ;;
439 (q) [ "${quiet-}" ] && return 0 ;;
440 (v) [ "${verbose-}" ] || return 0 ;;
441 (*) return 1
442 esac
443 done
444 shift $((OPTIND - 1))
446 : "${_warn_fmt="$1"}"
447 : "${_warn_nl="
449 shift 1
451 # shellcheck disable=2059
452 printf -- "${progname:-"${0##*/}"}: $_warn_fmt$_warn_nl" "$@" >&2