view share/local-bsdtools/common.subr @ 805:e18cc5fe828c

.shellcheckrc: disable SC2214 because is fires for all long options. Long options are not understood by shellcheck.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 04 Nov 2024 22:45:00 +0100
parents 2cd233b137ec
children 6cf3e1021862
line wrap: on
line source

#!/bin/sh
# -*- mode: shell-script; indent-tabs-mode: nil; -*-
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2017-2024 Franz Glasner.
#:             All rights reserved.
#: :License:   BSD 3-Clause "New" or "Revised" License.
#:             See LICENSE for details.
#:             If you cannot find LICENSE see
#:             <https://opensource.org/licenses/BSD-3-Clause>
#: :ID:        @(#)@@SIMPLEVERSIONTAG@@
#:


: # Dummy separator for shellcheck: no module-wide settings below this line


# shellcheck disable=SC2223    # quote
#: The path to the external jq executable (JSON parser)
: ${JQ="/usr/local/bin/jq"}


#
# Some important definitions from :manpage:`sysexits(3)`
#

# shellcheck disable=SC2034       # seems unused
EX_OK=0
# shellcheck disable=SC2034
EX_BASE=64
# shellcheck disable=SC2034
EX_USAGE=$((EX_BASE + 0))
# shellcheck disable=SC2034
EX_DATAERR=$((EX_BASE + 1))
# shellcheck disable=SC2034
EX_NOINPUT=$((EX_BASE + 2))
# shellcheck disable=SC2034
EX_UNAVAILABLE=$((EX_BASE + 5))
# shellcheck disable=SC2034
EX_SOFTWARE=$((EX_BASE + 6))
# shellcheck disable=SC2034
EX_TEMPFAIL=$((EX_BASE + 12))
# shellcheck disable=SC2034
EX_CONFIG=$((EX_BASE + 14))


#:
#: Display an error message to stderr and exit with given exit code
#:
#: Args:
#:   $1 (int): The exit code to exit with
#:   $2 ...: The message to print to
#:
#: Exit:
#:   Always exits with $1
#:
#: The name of the script is prepended to the error message always.
#:
fatal() {
    local _ec

    if [ $# -ge 1 ]; then
        _ec="$1"
        [ -z "$_ec" ] && _ec="${EX_USAGE}"
        shift
    else
        _ec=1
    fi
    printf "%s: ERROR: %s\\n" "$0" "$*" 1>&2
    exit "${_ec}"
}


#:
#: Display an error message to stderr
#:
#: Args:
#:   $*: The message to print to
#:
#: Returns:
#:   int: 0
#:
#: The name of the script is prepended to the error message always.
#:
err() {
    printf "%s: ERROR: %s\\n" "$0" "$*" 1>&2
    return 0
}


#:
#: Display a warning to stderr
#:
#: Args:
#:   $*: the fields to display
#:
#: Returns:
#:   int: 0
#:
#: The name of the script is prepended to the warning message always.
#:
warn() {
    printf "%s: WARNING: %s\\n" "$0" "$*" 1>&2
    return 0
}


#:
#: Test $1 variable, and warn if not set to YES or NO.
#:
#: Args:
#:   $1 (str): The name of the variable to test to.
#:
#: Returns:
#:   int: 0 (truthy) iff ``yes`` (et al),
#:        1 (falsy) otherwise.
#:
#: This function warns if other than proper YES/NO or their alias
#: values are found.
#:
checkyesno() {
    # $1

    local _value

    eval _value=\"\$\{"${1}"\}\"
    # do not delegate to checkyesnovalue() because of the error message proper
    case "${_value}" in
        # "yes", "true", "on", or "1"
        [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
            return 0
            ;;
        # "no", "false", "off", or "0"
        [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
            return 1
            ;;
        *)
            warn "\${${1}} is not set properly"
            return 1
            ;;
    esac
}


#:
#: Test $1 variable, and warn if not set to YES or NO.
#:
#: Args:
#:   $1 (str): The value to test to.
#:
#: Returns:
#:   int: 0 (truthy) iff ``yes`` (et al),
#:        1 (falsy) otherwise.
#:
#: This function warns if other than proper YES/NO values are found.
#:
checkyesnovalue() {
    # $1

    case "${1}" in
        # "yes", "true", "on", or "1"
        [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
            return 0
            ;;
        # "no", "false", "off", or "0"
        [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
            return 1
            ;;
        *)
            warn "expected YES/NO but got \`${1}'"
            return 1
            ;;
    esac
}


#:
#: Test $1 variable whether it is set to YES.
#:
#: Args:
#:   $1 (str): The name of the variable to test to.
#:
#: Returns:
#:   int: 0 (truthy) iff ``yes`` (et al),
#:        1 (falsy) otherwise.
#:
#: Contrary to `checkyesno` this function does not warn if the variable
#: contains other values as YES.
#:
checkyes() {
    # $1

    local _value

    eval _value=\"\$\{"${1}"\}\"
    checkyesvalue "${_value}"
}


#:
#: Test a given value whether it is set to YES.
#:
#: Args:
#:   $1 (str): The value to test to.
#:
#: Returns:
#:   int: 0 (truthy) iff ``yes`` (et al),
#:        1 (falsy) otherwise.
#:
#: Contrary to `checkyesno` this function does not warn if the variable
#: contains other values as YES.
#:
checkyesvalue() {
    # $1

    case "${1}" in
        # "yes", "true", "on", or "1"
        [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}


#:
#: Ensure that no command line options are given
#:
#: Args:
#:   $@:
#:
#: Exit:
#:   2: If formally `getopts` finds options in "$@"
#:
#: Return:
#:   0
#:
_ensure_no_options() {
    local _opt

    while getopts ':' _opt ; do
        [ "${_opt}" = '?' ] &&  fatal 2 "no option allowed (given: -${OPTARG-<unknown>})"
    done
    return 0
}


#:
#: Helper for `getopts` to process long options also.
#:
#: Invocation:
#:    postprocess_getopts_for_long shortopts varname longopt-spec*  '' "$@"
#:
#: The `shortopts` *must* contain ``-:`` to allow a "short" option with
#: name ``-`` and further arguments. This is used by this function to
#: process them further and augment `OPTARG` and `varname` accordingly.
#:
#: Input (Globals):
#:   - The variable with name `varname`
#:   - OPTARG
#:
#: Output (Globals):
#:   - The variable with name `varname`
#:   - OPTARG
#:
#: Returns:
#:   int: 1 if an internal processing error occurs or if the long options
#:        are not terminated by a null string,
#:        0 otherwise
#:
#: Heavily inspired by
#: https://stackoverflow.com/questions/402377/using-getopts-to-process-long-and-short-command-line-options
#:
#: Error handling is in the line of the POSIX spec for a shell's `getopts`:
#: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html.
#: In addition to "normal" error handling, if a long options that does not
#: accept an option argument has one the same type of error is reported
#: as if a required option argument were missing.
#:
#: A leading ``:`` in `shortopts` is handled also if an alternate error
#: handling is to be employed.
#:
#: Arguments for long options *must* use the form ``--long-option=value``.
#:
#: There is no default relation between short and long options. The evaluation
#: by the caller is responsible for this.
#:
#: A long option may not contain the ``=`` character. But if a `longopt-spec`
#: has a trailing ``=`` the long option requires an argument in the same
#: way as the ``:`` for a short option.
#:
#: If a long option is specified more than once in `longopt-spec` the first
#: one is taken into account.
#:
#: The `shortopts`, the `varname` must be the same as given to `getopts`.
#: This is also true for the arguments to be analyzed.
#:
#: Example:
#:
#:   ::
#:     while getopts "a:bc-:" opt "$@"; do
#:       postprocess_getopts_for_long "a:bc-:" opt "long-a=" "long-b" "long-d" "$@"
#:       case "${opt}" in
#:         a|long-a)
#:           a_value="${OPTARG}";;
#:         b|long-b)
#:           opt_b=1;;
#:         c)
#:           opt_c=1;;
#:         long-d)
#:           opt_d=1;;
#:         \?)
#:           # handle the error (message already printed to stderr)
#:           exit 2;;
#:         *)
#:           # other inconsistency
#:           exit 2;;
#:       esac
#:     done
#:
postprocess_getopts_for_long() {
    # $1 $2 $3... "" $n...
    local -

    local __ppgofl_shortopts __ppgofl_varname \
          __ppgofl_opt __ppgofl_longspec __ppgofl_longopt __ppgofl_longoptarg \
          __ppgofl_caller_reports_error \
          __ppgofl_base

    __ppgofl_shortopts="${1}"
    __ppgofl_varname="${2}"
    eval __ppgofl_opt=\"\$\{"${__ppgofl_varname}"\}\"

    # Check whether it we currently handle a long option
    [ "${__ppgofl_opt}" = '-' ] || return 0

    #
    # long option: reformulate OPT and OPTARG
    #

    case "${__ppgofl_shortopts}" in
        :*)
            __ppgofl_caller_reports_error=yes;;
        *)
            __ppgofl_caller_reports_error='';;
    esac

    __ppgofl_longopt=''
    case "${OPTARG}" in
        *=*)
            __ppgofl_longopt="${OPTARG%%=*}"
            __ppgofl_longoptarg="${OPTARG#*=}"
            ;;
        *)
            __ppgofl_longopt="${OPTARG}"
            unset __ppgofl_longoptarg
            ;;
    esac

    shift 2

    #
    # Early check for null termination of long options.
    # Also check that the `=' character is not part of the name of a long
    # option.
    #
    __ppgofl_base=0
    for __ppgofl_longspec in "$@"; do
        __ppgofl_base=$((__ppgofl_base + 1))
        # [ -z "${__ppgofl_longspec}" ] && break
        case "${__ppgofl_longspec}" in
            '')
                break
                ;;
            *=*[!=]|*=*=)
                # Error: option contains a = character
                if [ -n "${__ppgofl_caller_reports_error}" ]; then
                    setvar "${__ppgofl_varname}" '?'  # XXX FIXME this char???
                    OPTARG="${__ppgofl_longspec}"
                else
                    setvar "${__ppgofl_varname}" '?'
                    unset OPTARG
                    err "Long option specification contains a forbidden \`=' character: ${__ppgofl_longspec}"
                fi
                return 1
                ;;
            *)
                :
                ;;
        esac
    done
    if [ -n "${__ppgofl_longspec}" ]; then
        if [ -n "${__ppgofl_caller_reports_error}" ]; then
            setvar "${__ppgofl_varname}" ':'
            OPTARG=''
        else
            setvar "${__ppgofl_varname}" '?'
            unset OPTARG
            err "Missing null terminator for long option specifications"
        fi
        return 1
    fi

#    # Print the "real" arguments that will be analyzed
#    i=1
#    while [ $((i + __ppgofl_base)) -le $# ]; do
#        eval v=\"\$\{$((i + __ppgofl_base))\}\"
#        echo "HUHU: ${v}"
#        i=$((i + 1))
#    done
#    return 0

    for __ppgofl_longspec in "$@"; do
        if [ -z "${__ppgofl_longspec}" ]; then
            # We did not hit a spec because we return ealy in the hit case
            if [ -n "${__ppgofl_caller_reports_error}" ]; then
                setvar "${__ppgofl_varname}" '?'
                OPTARG="${__ppgofl_longopt}"
            else
                setvar "${__ppgofl_varname}" '?'
                unset OPTARG
                err "Illegal option --${__ppgofl_longopt}"
            fi
            return 0
        elif [ "${__ppgofl_longspec}" = "${__ppgofl_longopt}=" ]; then
            # Need an argument value
            if [ -z "${__ppgofl_longoptarg+SET}" ]; then
                #
                # Error
                #
                # Looking for an option like --long-option option-value
                # would need to change OPTIND. This is unspecified in
                # POSIX. So we handle only --long-option=option-value.
                #
                if [ -n "${__ppgofl_caller_reports_error}" ]; then
                    setvar "${__ppgofl_varname}" ':'
                    OPTARG="${__ppgofl_longopt}"
                else
                    setvar "${__ppgofl_varname}" '?'
                    unset OPTARG
                    err "option argument required for option --${__ppgofl_longopt}"
                fi
                return 0
            fi
            setvar "${__ppgofl_varname}" "${__ppgofl_longopt}"
            OPTARG="${__ppgofl_longoptarg}"
            return 0
        elif [ "${__ppgofl_longspec}" = "${__ppgofl_longopt}" ]; then
            # No argument allowed
            if [ -n "${__ppgofl_longoptarg+SET}" ]; then
                # Error
                if [ -n "${__ppgofl_caller_reports_error}" ]; then
                    setvar "${__ppgofl_varname}" ':'
                    OPTARG="${__ppgofl_longopt}"
                else
                    setvar "${__ppgofl_varname}" '?'
                    unset OPTARG
                    err "no option argument allowed for option --${__ppgofl_longopt}"
                fi
                return 0
            fi
            setvar "${__ppgofl_varname}" "${__ppgofl_longopt}"
            unset OPTARG
            return 0
        fi
    done
    # Processing error
    return 1
}


#:
#: Determine some important dataset properties that are set locally
#:
#: Args:
#:   $1: the dataset
#:   $2 ...: the properties to check for
#:
#: Output (stdout):
#:   An option string suited for use in "zfs create"
#:
_get_local_zfs_properties_for_create() {
    local ds

    local _res _prop _value _source

    ds="${1}"
    shift

    _res=""

    for _prop in "$@" ; do
        IFS=$'\t' read -r _value _source <<EOF73GHASGJKKJ354
$(zfs get -H -p -o value,source "${_prop}" "${ds}")
EOF73GHASGJKKJ354
        case "${_source}" in
            local)
                if [ -z "${_res}" ]; then
                    _res="-o ${_prop}=${_value}"
                else
                    _res="${_res} -o ${_prop}=${_value}"
                fi
                ;;
            *)
                # VOID
                ;;
        esac
    done
    printf '%s' "${_res}"
}


#:
#: Use `mount -t zfs -p` to determine the ZFS dataset for a given mountpoint
#:
#: Args:
#:  $1: the mountpoint
#:
#: Output (stdout):
#:   The name of the ZFS dataset that is mounted on `$1`
#:
#: Return:
#:   0: if a mounted dataset is found
#:   1: if no ZFS dataset is found that is mounted on `$1`
#:
#: The dataset has to be mounted.
#:
_get_zfs_dataset_for_mountpoint() {
    local _mountpoint

    local _ds _mount _rest

    _mountpoint="$1"

    if [ -x "${JQ}" ]; then
        /sbin/mount -t zfs -p --libxo=json,no-locale \
        | LC_ALL=C.UTF-8 "${JQ}" -r $'.mount.fstab[] | [.device, .mntpoint, .fstype, .opts, .dump, .pass] | @tsv ' \
        | {
            while IFS=$'\t' read -r _ds _mount _rest ; do
                if [ "$_mount" = "$_mountpoint" ]; then
                    printf '%s' "${_ds}"
                    return 0
                fi
            done
            return 1
          }
    else
        # Check for unexpected spaces
        if ! check_for_proper_fstab; then
            fatal 1 "Unexpected spaces in fstab. Please install \`${JQ}'."
        fi
        /sbin/mount -t zfs -p \
        | {
            while IFS=$' \t' read -r _ds _mount _rest ; do
                if [ "$_mount" = "$_mountpoint" ]; then
                    printf '%s' "${_ds}"
                    return 0
                fi
            done
            return 1
          }
    fi
}


#:
#: Determine the ZFS dataset that is mounted at `$1`/var/empty.
#:
#: Allow special handling for <mountpoint>/var/empty which may be
#: mounted read-only.
#:
#: Args:
#:  $1: the root mountpoint
#:
#: Output (stdout):
#:   The name of the ZFS dataset that is mounted on `$1`/var/empty
#:
#: Return:
#:   0: if a mounted dataset is found
#:   1: if no ZFS dataset is found that is mounted on `$1`
#:
#: The dataset has to be mounted.
#:
_get_zfs_dataset_for_varempty() {
    local _mountpoint

    local _ve_mount

    _mountpoint="$1"

    if [ "$_mountpoint" = '/' ]; then
        _ve_mount='/var/empty'
    else
        _ve_mount="${_mountpoint}/var/empty"
    fi

    _get_zfs_dataset_for_mountpoint "${_ve_mount}"
}


#:
#: Search for a running jail where it's "path" points to a given location
#:
#: Args:
#:   $1 (str): The location to search for.
#:
#: Output (stdout):
#:   The name if the jail with a "path" that is equal to the input param.
#:   Nothing if a jail is not found.
#:   The name is written when the jail is running and/or a dying.
#:
#: Return:
#:   0: if a running jail is found
#:   1: error
#:   2: jail found but currently dying
#:   3: no running jail found
#:
_get_jail_from_path() {
    local _location

    local _name _path _dying

    _location="${1-}"
    case "${_location}" in
        '')
            err "no mountpoint given"
            return 1
            ;;
        /)
            true
            ;;
        *//*)
            err "given directory must not contain consequtive slashes"
            return 1
            ;;
        /*/)
            # Remove trailing slash
            _location="${_location%/}"
            ;;
        /*)
            true
            ;;
        *)
            err "given directory path must be an absolute path"
            return 1
            ;;
    esac

    if [ -x "${JQ}" ]; then
        /usr/sbin/jls --libxo=json,no-locale -d dying name path \
        | LC_ALL=C.UTF-8 "${JQ}" -r $'.["jail-information"].jail[] | [.dying, .name, .path] | @tsv ' \
        | { # '
            while IFS=$'\t' read -r _dying _name _path ; do
                if [ "${_path}" = "${_location}" ]; then
                    if [ "${_dying}" != "false" ]; then
                        # Dying
                        printf '%s' "${_name}"
                        return 2
                    fi
                    printf '%s' "${_name}"
                    return 0
                fi
            done
            return 3
          }
    else
        /usr/sbin/jls --libxo=text,no-locale -d name dying path \
        | {
            while IFS=' '$'\t' read -r _name _dying _path ; do
                if [ "${_path}" = "${_location}" ]; then
                    if [ "${_dying}" != "false" ]; then
                        # Dying
                        printf '%s' "${_name}"
                        return 2
                    fi
                    printf '%s' "${_name}"
                    return 0
                fi
            done
            return 3
        }
    fi
}


#:
#: Search for current mounts of a given ZFS dataset and its children.
#:
#: Only ZFS mounts are considered. Any returned datasets are just
#: the given mounted dataset or its mounted children.
#:
#: The output is sorted by the mountpoint.
#:
#: Args:
#:   $1 (str): The dataset the to check for.
#:             The dataset and its children are searched for.
#:
#: Other Parameters:
#:   -r (optional): If given the the output is sorted in reverse order.
#:
#: Output (stdout):
#:   The sorted list (lines) of mounts in :manpage:`fstab(5)` format.
#:   This list may be empty.
#:   The fields are separated by a single TAB character always.
#:
#: Exit:
#:   1: on fatal errors (usage et al.)
#:
_get_zfs_mounts_for_dataset_tree() {
    local _dsname
    local _opt_reversed

    local _opt _fstab

    _opt_reversed=""
    while getopts "r" _opt ; do
        case ${_opt} in
            r)
                _opt_reversed="--reverse"
                ;;
            \?)
                fatal 2 "invalid option given"
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _dsname="${1-}"
    [ -z "${_dsname}" ] && fatal 2 "no dataset given"

    if [ -x "${JQ}" ]; then
        /sbin/mount -t zfs -p --libxo=json,no-locale \
        | LC_ALL=C.UTF-8 "${JQ}" -r $'.mount.fstab[] | [.device, .mntpoint, .fstype, .opts, .dump, .pass] | @tsv ' \
        | LC_ALL=C.UTF-8 /usr/bin/awk -F '\t' -v OFS=$'\t' -v ds1="${_dsname}" -v ds2="${_dsname}/" $'{ if (($1 == ds1) || (index($1, ds2) == 1)) { print $1, $2, $3, $4, $5, $6; } }' \
        | LC_ALL=C.UTF-8 /usr/bin/sort --field-separator=$'\t' --key=2 ${_opt_reversed}
    else
        # Check for unexpected spaces
        if ! check_for_proper_fstab; then
            fatal 1 "Unexpected spaces in fstab. Please install \`${JQ}'."
        fi
        _fstab="$(/sbin/mount -t zfs -p | LC_ALL=C awk -v OFS=$'\t' -v ds1="${_dsname}" -v ds2="${_dsname}/" $'{ if (($1 == ds1) || (index($1, ds2) == 1)) { print $1, $2, $3, $4, $5, $6; } }' | LC_ALL=C /usr/bin/sort --field-separator=$'\t' --key=2 ${_opt_reversed})"
        printf '%s' "${_fstab}"
    fi
}


#:
#: Search for mounts and sub-mounts at a given directory.
#:
#: The output is sorted by the mountpoint.
#:
#: Args:
#:   $1 (str, optional): The directory where to start for mounts and sub-mounts.
#:       It must be an absolute path.
#:       The root directory :file:`/` is allowed.
#:       A `null` value is treated as if the root directory :file:`/`
#:       is given as argument.
#:
#: Other Parameters:
#:   -r (optional): If given the the output is sorted in reverse order.
#:
#: Output (stdout):
#:   The sorted list (lines) of mounts in :manpage:`fstab(5)` format.
#:   This list may be empty.
#:   The fields are separated by a single TAB character always.
#:
#: Exit:
#:   1: on fatal errors (usage et al.)
#:
#: Important:
#:   The input directory **must** be an absolute path.
#:
_get_mounts_at_directory() {
    local _directory
    local _opt_reversed

    local _opt _fstab _mp1 _mp2

    _opt_reversed=""
    while getopts "r" _opt ; do
        case ${_opt} in
            r)
                _opt_reversed="--reverse"
                ;;
            \?)
                fatal 2 "invalid option given"
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _directory="${1-}"
    case "${_directory}" in
        '')
            # OK this matches at the root directory
            _mp1='/'
            _mp2='/'
            ;;
        /)
            # OK this matches at the root directory
            _mp1='/'
            _mp2='/'
            ;;
        *//*)
            fatal 1 "given directory must not contain consequtive slashes"
            ;;
        /*/)
            # Remove trailing slash for equality check
            _mp1="${_directory%/}"
            # Hold the trailing slash as-is for the directory check
            _mp2="${_directory}"
            ;;
        /*)
            _mp1="${_directory}"
            _mp2="${_directory}/"
            ;;
        *)
            fatal 1 "given directory must be an absolute path"
            ;;
    esac
    if [ -x "${JQ}" ]; then
        /sbin/mount -p --libxo=json,no-locale \
        | LC_ALL=C.UTF-8 "${JQ}" -r $'.mount.fstab[] | [.device, .mntpoint, .fstype, .opts, .dump, .pass] | @tsv ' \
        | LC_ALL=C.UTF-8 /usr/bin/awk -F '\t' -v OFS=$'\t' -v mp1="${_mp1}" -v mp2="${_mp2}" $'{ if (($2 == mp1) || (index($2, mp2) == 1)) { print; } }' \
        | LC_ALL=C.UTF-8 /usr/bin/sort --field-separator=$'\t' --key=2 ${_opt_reversed}
    else
        # Check for unexpected spaces
        if ! check_for_proper_fstab; then
            fatal 1 "Unexpected spaces in fstab. Please install \`${JQ}'."
        fi
        _fstab="$(/sbin/mount -p | LC_ALL=C awk -v OFS=$'\t' -v mp1="${_mp1}" -v mp2="${_mp2}" $'{ if (($2 == mp1) || (index($2, mp2) == 1)) { print $1, $2, $3, $4, $5, $6; } }' | LC_ALL=C /usr/bin/sort --field-separator=$'\t' --key=2 ${_opt_reversed})"
        printf '%s' "${_fstab}"
    fi
}


#:
#: Use :command:`/usr/bin/procstat` to check if there are opened files or
#: VM mappings with a given path prefix.
#:
#: Args:
#:   $1 (str, optional): The path prefix to check open files for.
#:       This must be an absolute path.
#:       The root directory :file:`/` is allowed.
#:       A `null` value is treated as if the root directory :file:`/` is given
#:       as argument.
#:
#: Returns:
#:   int: 0 if there are no opened files found,
#:        10 if there are opened files found.
#:        Other error codes may also returned on errors.
#:
_check_no_open_files_from_all_proc() {
    local _path_prefix

    local _command _fd _fd_type _vnode_type _path _pid _kve_type _kve_path _rc

    _path_prefix="${1-}"
    case "${_path_prefix}" in
        '')
            # OK this matches at the root directory
            _p1='/'
            _p2='/'
            ;;
        /)
            # OK this matches at the root directory
            _p1='/'
            _p2='/'
            ;;
        *//*)
            fatal 1 "given path prefix must not contain consequtive slashes"
            ;;
        /*/)
            # Remove trailing slash for equality check
            _p1="${_path_prefix%/}"
            # Hold the trailing slash as-is for prefix check
            _p2="${_path_prefix}"
            ;;
        /*)
            _p1="${_path_prefix}"
            _p2="${_path_prefix}/"
            ;;
        *)
            fatal 1 "given path prefix must be an absolute path"
            ;;
    esac
    if [ -x "${JQ}" ]; then
        # shellcheck disable=SC2016    # $cmd is really not to expand here
        LC_ALL=C.UTF-8 /usr/bin/procstat --libxo=json -a file | LC_ALL=C.UTF-8 "${JQ}" -r '.procstat.files | map(.)| .[] | .command as $cmd | .files[] | [ $cmd, .fd, .fd_type, .vode_type, .path ] | @tsv' \
        | {
            _rc=0
            while IFS=$'\t' read -r _command _fd _fd_type _vnode_type _path; do
                case "${_path}" in
                    "${_p1}"|"${_p2}"*)
                        _rc=10
                        ;;
                    *)
                        ;;
                esac
            done
            [ "${_rc}" -ne 0 ] && return ${_rc}
          }
        LC_ALL=C.UTF-8 /usr/bin/procstat --libxo=json -a vm | LC_ALL=C.UTF-8 "${JQ}" -r $'.procstat.vm | map(.) | .[] | .process_id as $pid | .vm[] | [ $pid, .kve_type, .kve_path ] | @tsv' \
        | {
            _rc=0
            while IFD=$'\t' read -r _pid _kve_type _kve_path; do
                case "${_kve_path}" in
                    "${_p1}"|"${_p2}"*)
                        _rc=10
                        ;;
                    *)
                        ;;
                esac
            done
            return ${_rc}
          }
    else
        /usr/bin/procstat --libxo=text,no-locale -a file \
        | LC_ALL=C /usr/bin/awk -v OFS=$'\t' '{ print $2, $3, $4, $10; }' \
        | {
            _rc=0
            while IFS=$'\t' read -r _command _fd _fd_type _path; do
                case "${_path}" in
                    "${_p1}"|"${_p2}"*)
                        _rc=10
                        ;;
                    *)
                        ;;
                esac
            done
            [ "${_rc}" -ne 0 ] && return ${_rc}
          }
        /usr/bin/procstat --libxo=text,no-locale -a vm \
        | LC_ALL=C /usr/bin/awk -v OFS=$'\t' '{ print $1, $10, $11; }' \
        | {
            _rc=0
            while IFD=$'\t' read -r _pid _kve_type _kve_path; do
                case "${_kve_path}" in
                    "${_p1}"|"${_p2}"*)
                        _rc=10
                        ;;
                    *)
                        ;;
                esac
            done
            return ${_rc}
          }
    fi
}


#:
#: Use :command:`/usr/bin/fstat` to check if there are opened files or
#: VM mappings onr a given file system.
#:
#: Args:
#:   $1 (str, optional): The filesystem (aka mountpoint).
#:       This should be an absolute path.
#:       The root directory :file:`/` is allowed.
#:       A `null` value is treated as if the root directory :file:`/` is given
#:       as argument.
#:
#: Returns:
#:   int: 0 if there are no opened files found,
#:        10 if there are opened files found.
#:        Other error codes may also returned on errors.
#:
_check_no_open_files_on_filesystem() {
    local _fspath

    local _count

    _fspath="${1:-/}"

    _count="$(LC_ALL=C /usr/bin/fstat -m -f "${_fspath}" | LC_ALL=C /usr/bin/wc -l)"
    if [ -z "${_count}" ]; then
        # this is an error
        return 1
    fi
    # Note that fstat always prints a header: account for the header line
    if [ "${_count}" -gt 1 ]; then
        return 10
    else
        return 0
    fi
}


#:
#: Check the validity of ZFS dataset names.
#:
#: See: ZFS Component Naming Requirements
#: - https://docs.oracle.com/cd/E26505_01/html/E37384/gbcpt.html
#: - https://illumos.org/books/zfs-admin/zfsover-1.html#gbcpt
#:
#: But it seems that in OpenZFS a space character is also allowed in
#: name components; see https://github.com/openzfs/zfs/issues/439:
#: It is in zfs_namecheck.c:valid_char() line #55; its the last
#: character comparison.
#:
#: In OpenZFS pool names allow also the colon.
#:
#: Also there is no difference between "starting with" and other
#: "containing" (with the exception of pools starting with a alpha
#: character.
#:
#: Source code: https://iris.cs.tu-dortmund.de/freebsd-lockdoc/lockdoc-v13.0-0.1/source/sys/contrib/openzfs/module/zcommon/zfs_namecheck.c
#:
#: Args:
#:   $1 (str): The name of the dataset.
#:   $2 (bool, optional): If this evals to yes/on/1 then strict checking is
#:                        done, otherwise the OpenZFS implementation is
#:                        followed.
#:
#: Returns:
#:   int: 0 (truish) if it is a valid name,
#:        1 (falsy) if not.
#:
#: We never check for special pool names (such as ``mirror``, ``raidz``,
#: ``c0`` to ``c9`` et al.) because we do not create any pools.
#:
#: That also means that proper quoting with double quotes is enough to
#: handle all sorts of ZFS names.
#:
check_zfs_naming() {
    local _strict

    _strict="${2-}"
    if checkyes _strict; then
        # Oracle docs
        printf "%s" "$1" | LC_ALL=C /usr/bin/grep -q -E '^[A-Za-z][-A-Za-z0-9_.]*(/[A-Za-z0-9][-A-Za-z0-9:_.]*)*(@[A-Za-z0-9][-A-Za-z0-9:_.]*)?$'
    else
        # OpenZFS
        printf "%s" "$1" | LC_ALL=C /usr/bin/grep -q -E '^[A-Za-z][-A-Za-z0-9:_. ]*(/[-A-Za-z0-9:_. ]+)*(@[-A-Za-z0-9:_. ]+)?$'
    fi
}


#:
#: Check that the current fstab as returned by :command:`mount -p` does not
#: contain any spaces at improper -- and therefore unhandled -- locations.
#:
#: Returns:
#:   int: 0 (truish) if :command:`mount -p` contains no spaces,
#:        1 (falsy) otherwise
#:
check_for_proper_fstab() {
    local _dev _mp _fstype _opts _dump _pass _rest

    /sbin/mount -p \
    | {
        while IFS=$' \t' read -r _dev _mp _fstype _opts _dump _pass _rest; do
            if [ -n "${_rest}" ]; then
                return 1
            fi
            # XXX TBD: Check that _dump and _pass are numbers proper
        done
        return 0
      }
}


#:
#: Clean the current process environment somewhat
#:
reset_environment() {
    unset PATH_FSTAB
    unset GREP_OPTIONS

    # XXX: should we do this
    # export LC_ALL=C
}


# Automatically reset the environment
reset_environment