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
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.
31 # $catch Whether to call atexit and reraise the signal.
32 # $caught Set to caught signal.
49 # Disable signal handlers, terminate all jobs and the process group,
50 # run and clear $atexit, and return $?.
56 # $atexit Code to be eval-ed.
57 # $_atexit_retval Return value.
60 # shellcheck disable=2319
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
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.
89 if [ -t "$_clearln_fd" ]
90 then printf '\033[0K' >&"$_clearln_fd"
96 # Print a message to standard error and exit.
99 # err [-s status] format [argument ...]
102 # -s status Exit with status (default: 1).
106 OPTIND
=1 OPTARG
='' _err_opt
=''
107 while getopts s
: _err_opt
110 (s
) _err_status
="${OPTARG:?}" ;;
115 shift $
((OPTIND
- 1))
122 # Enforce POSIX-compliance, register signal handlers, and set globals.
128 # $BIN_SH Set to xpg4.
129 # $CLICOLOR_FORCE Set to the empty string.
132 # $POSIXLY_CORRECT Set to y.
134 # $caught Set to the empty string.
135 # $progname Set to the basename of $0.
141 if [ "${BASH_VERSION-}" ] ||
[ "${KSH_VERSION-}" ]
143 # shellcheck disable=3040
144 if ( set -o posix
) 2>/dev
/null
147 elif [ "${ZSH_VERSION-}" ]
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
161 export BIN_SH
=xpg4 NULLCMD
=: POSIXLY_CORRECT
=y CLICOLOR_FORCE
=
163 # Make sure IFS is safe
166 # Trap signals that would terminate the script
168 # shellcheck disable=2064
169 for _init_sig
in ALRM HUP INT TERM USR1 USR2
170 do trap "catch $_init_sig" "$_init_sig"
176 [ "$caught" ] && kill -s "$caught" "$$"
178 # Safe permission mask
184 progname
="$(basename -- "$0")" || progname
="${0##*/}"
190 # Check if $2 matches any member of $@ using operator $1.
193 # inlist operator needle [straw ...]
196 # shellcheck disable=2034
198 _inlist_needle
="${2?}"
201 # shellcheck disable=2034
203 do test "$_inlist_straw" "$_inlist_op" "$_inlist_needle" && return
211 # Create a lock file.
217 # -t n Wait n seconds to acquire lock file (default: 3).
220 OPTARG
='' OPTIND
=1 _lock_opt
=''
221 while getopts 't:' _lock_opt
224 (t
) _lock_timeout
="$OPTARG" ;;
228 shift $
((OPTIND
- 1))
232 : "${_lock_timeout:=3}"
235 while [ "$_lock_timeout" -gt 0 ]
237 if ( printf '%d\n' "$$" >"$_lock_file"; ) >/dev
/null
2>&1
241 if ! read -r _lock_pid
<"$_lock_file" ||
242 ! [ "$_lock_pid" ] ||
243 ! kill -0 -- "$_lock_pid" >/dev
/null
2>&1
245 warn
-q 'Removing %s... ' "$_lock_file"
247 elif [ "$_lock_timeout" -gt 0 ]
252 _lock_timeout
=$
((_lock_timeout
- 1))
255 warn
'Could not acquire %s' "$_lock_file"
261 # Run $@ and redirect its output to a file unless $verbose is set.
264 # logged command [argument ...]
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).
274 OPTIND
=1 OPTARG
='' _logged_opt
=''
275 while getopts 'd:i:l:' _logged_opt
278 (d
) _logged_dir
="${OPTARG:?}" ;;
279 (i
) _logged_mask
="$_logged_mask${OPTARG:?} " ;;
280 (l
) _logged_fname
="${OPTARG:?}" ;;
284 shift $
((OPTIND
- 1))
287 : "${_logged_dir:=.}"
288 : "${_logged_fname:=}"
289 : "${_logged_mask:=0}"
298 # shellcheck disable=2030
300 : "${_logged_fname:="$(basename "$1").log"}"
301 : "${_logged_fname:?}"
302 _logged_log
="$TMPDIR/$_logged_fname"
305 "$@" >>"$_logged_log" 2>&1
309 # shellcheck disable=2086
310 if inlist
-eq "$_logged_xstatus" $_logged_mask
312 rm -f "$TMPDIR/$_logged_fname" >/dev
/null
2>&1
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.
329 # $TMPDIR Set to the created directory.
330 # $atexit Updated to reigster deletion of temporary directory.
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" ;;
344 shift $
((OPTIND
- 1))
347 : "${_mktmpdir_dir:="${TMPDIR:-/tmp}"}"
348 : "${_mktmpdir_prefix:=tmp}"
351 readonly _mktmpdir_tmpdir
="$_mktmpdir_dir/$_mktmpdir_prefix-$$"
352 mkdir
-m 0755 "$_mktmpdir_tmpdir" ||
exit
353 atexit
="rm -rf \"\$_mktmpdir_tmpdir\"; ${atexit-:}"
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.
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.
391 if ! [ "$quiet" ] && [ -t "$_spin_fd" ]
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" ;;
402 _spin_counter
=$
((_spin_counter
+ 1))
409 # Remove a lock file.
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.
427 # -n Suppress the terminating newline.
428 # -q Suppress output if $quiet is set.
429 # -v Suppress output unless $verbose is set.
434 OPTIND
=1 OPTARG
='' _warn_opt
=''
435 while getopts 'nqv' _warn_opt
439 (q
) [ "${quiet-}" ] && return 0 ;;
440 (v
) [ "${verbose-}" ] ||
return 0 ;;
444 shift $
((OPTIND
- 1))
446 : "${_warn_fmt="$1"}"
451 # shellcheck disable=2059
452 printf -- "${progname:-"${0##*/}"}: $_warn_fmt$_warn_nl" "$@" >&2