view sbin/ftjail @ 689:425b1ae264a9

ftjail: FIX: call jexec command params after "--"
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 01 Oct 2024 15:04:47 +0200
parents 5156eaa27ac9
children 8f1583faf9ea
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: A very minimal BSD Thin Jail management tool.
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2022-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@@
#:

set -eu

# shellcheck disable=SC2034    # VERSION appears unused
VERSION='@@VERSION@@'

USAGE='
USAGE: ftjail [ OPTIONS ] COMMAND [ COMMAND OPTIONS ] [ ARG ... ]

OPTIONS:

  -V    Print the program name and version number to stdout and exit

  -h    Print this help message to stdout and exit

COMMANDS:

  datasets-tmpl -L|-P PARENT-BASE PARENT-SKELETON NAME

  mount-tmpl -L|-P [-n] [-u] BASE-RO SKELETON-RW MOUNTPOINT

  umount-tmpl BASE-RO SKELETON-RW

  interlink-tmpl MOUNTPOINT

  populate-tmpl -L|-P [-b] DIRECTORY BASETXZ [KERNELTXZ]

  snapshot-tmpl BASE-RO SKELETON-RW SNAPSHOT-NAME

  copy-skel [-A] [-D] [-L] [-M MOUNTPOINT] [-P] [-u]  SOURCE-DS SNAPSHOT-NAME TARGET-DS

  build-etcupdate-current-tmpl DIRECTORY TARBALL

  check-freebsd-update [-k] [-o OLD-ORIGIN] [-R SNAPSHOT] DIRECTORY NEW-ORIGIN [ETCUPDATE-TARBALL]

  freebsd-update [-k] [-o OLD-ORIGIN] [-R SNAPSHOT] DIRECTORY NEW-ORIGIN [ETCUPDATE-TARBALL]

ENVIRONMENT:

  All environment variables that affect "zfs" are effective also.

DESCRIPTION:

  All commands with the exception of "populate-tmpl" require ZFS as
  filesystem.
'


_p_datadir='@@DATADIR@@'
[ "${_p_datadir#@@DATADIR}" = '@@' ] && _p_datadir="$(dirname "$0")"/../share/local-bsdtools
. "${_p_datadir}/common.subr"
. "${_p_datadir}/farray.sh"


# Reset to standard umask
umask 0022


#
# PARENT-BASE NAME DRY-RUN
#
command_datasets_tmpl_base() {
    local _p_base _name

    local _opt_dry_run

    local _ds_base _opt

    _opt_dry_run=""
    while getopts "nu" _opt ; do
        case "${_opt}" in
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _p_base="${1-}"
    _name="${2-}"

    if [ -z "${_p_base}" ]; then
        echo "ERROR: no parent dataset for base given" >&2
        return 2
    fi
    if [ -z "${_name}" ]; then
        echo "ERROR: no name given" >&2
        return 2
    fi

    if ! zfs list -H -o mountpoint -t filesystem "${_p_base}" >/dev/null 2>/dev/null; then
        echo "ERROR: parent dataset \`${_p_base}' does not exist" >&2
        return 1
    fi
    _ds_base="${_p_base}/${_name}"
    if zfs list -H -o mountpoint -t filesystem "${_ds_base}" >/dev/null 2>/dev/null; then
        echo "ERROR: dataset \`${_ds_base}' does already exist" >&2
        return 1
    fi


    [ "${_opt_dry_run}" = "yes" ] && return 0

    echo "Creating RO base datasets in:"
    printf "\\t%s\\n" "${_ds_base}"

    zfs create -u -o canmount=noauto "${_ds_base}"

}


#
# SKELETON NAME DRY-RUN
#
command_datasets_tmpl_skel() {
    local _p_base _name

    local _opt_dry_run _opt_symlink

    local _ds_skel _child _child_zfsopts _opt

    _opt_dry_run=""
    _opt_symlink=""

    while getopts "LPnu" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _p_skel="${1-}"
    _name="${2-}"

    if [ -z "${_p_skel}" ]; then
        echo "ERROR: no parent dataset for skeleton given" >&2
        return 2
    fi
    if [ -z "${_name}" ]; then
        echo "ERROR: no name given" >&2
        return 2
    fi

    if ! zfs list -H -o mountpoint -t filesystem "${_p_skel}" >/dev/null 2>/dev/null; then
        echo "ERROR: parent dataset \`${_p_skel}' does not exist" >&2
        return 1
    fi
    _ds_skel="${_p_skel}/${_name}"
    if zfs list -H -o mountpoint -t filesystem "${_ds_skel}" >/dev/null 2>/dev/null; then
        echo "ERROR: dataset \`${_ds_skel}' does already exist" >&2
        return 1
    fi

    [ "${_opt_dry_run}" = "yes" ] && return 0

    echo "Creating RW skeleton datasets in:"
    printf "\\t%s\\n" "${_ds_skel}"

    if [ "${_opt_symlink}" = "yes" ]; then
        # In this case the skeleton root needs to be mounted into a "skeleton" subdir
        zfs create -u -o canmount=noauto "${_ds_skel}"
    else
        # Only children are to be mounted
        zfs create -u -o canmount=off "${_ds_skel}"
    fi
    # "usr" is only a container holding "usr/local"
    zfs create -u -o canmount=off "${_ds_skel}/usr"
    #
    # XXX FIXME: What about usr/ports/distfiles
    #            We typically want to use binary packages.
    #            And if we use ports they are not in usr/ports typically.
    #
    #zfs create -u -o canmount=off "${_ds_skel}/usr/ports"
    #
    # XXX FIXME: What about home
    #
    # /var/mail is here because it relies on atime
    #
    for _child in etc home root tmp usr/local var var/mail ; do
        case "${_child}" in
            "tmp"|"var/tmp")
                _child_zfsopts="-o sync=disabled -o setuid=off"
                ;;
            "home")
                _child_zfsopts="-o setuid=off"
                ;;
            "usr/ports/distfiles")
                _child_zfsopts="-o exec=off -o setuid=off -o compression=off -o primarycache=metadata"
                ;;
            "var/mail")
                _child_zfsopts="-o atime=on -o exec=off -o setuid=off"
                ;;
            *)
                _child_zfsopts=""
                ;;
        esac
        zfs create -u -o canmount=noauto ${_child_zfsopts} "${_ds_skel}/${_child}"
    done
}


#
# "datasets-tmpl" -- create the ZFS dataset tree
#
# PARENT-BASE PARENT-SKELETON NAME
#
command_datasets_tmpl() {
    # parent ZFS dataset -- child ZFS dataset name
    local _p_base _p_skel _name
    local _opt_symlink

    local _ds_base _ds_skel _opt

    _opt_symlink=""

    while getopts "LP" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="-L"
                ;;
            P)
                _opt_symlink="-P"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _p_base="${1-}"
    _p_skel="${2-}"
    _name="${3-}"

    # Check preconditions
    command_datasets_tmpl_base -n "${_p_base}" "${_name}" || return
    command_datasets_tmpl_skel -n ${_opt_symlink} "${_p_skel}" "${_name}" || return

    # Really do it
    command_datasets_tmpl_base "${_p_base}" "${_name}" || return
    command_datasets_tmpl_skel ${_opt_symlink} "${_p_skel}" "${_name}" || return
    return 0
}


#
# "populate-tmpl" -- populate the datasets with content from a FreeBSD base.txz
#
# command_populate_tmpl mountpoint basetxz
#
command_populate_tmpl() {
    # MOUNTPOINT -- base.txz
    local _mp _basetxz
    local _opt_symlink _opt_preserve_boot

    local _opt _dir

    _opt_symlink=""
    _opt_preserve_boot=""

    while getopts "LPb" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            b)
                _opt_preserve_boot="yes"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _mp="${1-}"
    _basetxz="${2-}"
    _kerneltxz="${3-}"

    if [ -z "${_mp}" ]; then
        echo "ERROR: no mountpoint given" >&2
        return 2
    fi
    if [ -z "${_basetxz}" ]; then
        echo "ERROR: no base.txz given" >&2
        return 2
    fi
    if [ ! -d "${_mp}" ]; then
        echo "ERROR: mountpoint \`${_mp}' does not exist" >&2
        return 1
    fi
    if [ ! -r "${_basetxz}" ]; then
        echo "ERROR: file \`${_basetxz}' is not readable" >&2
        return 1
    fi

    if [ "${_opt_symlink}" = "yes" ]; then
        echo "Extracting RO base ..."
        tar -C "${_mp}" --exclude=./etc --exclude=./root --exclude=./tmp --exclude=./usr/local --exclude=./var --no-safe-writes -xJp -f "${_basetxz}" || return
        # "home" is not part of base
        for _dir in etc root tmp usr/local var ; do
            echo "Extracting RW skeleton: ${_dir} ..."
            tar -C "${_mp}/skeleton" --include="./${_dir}" --exclude=./root/.cshrc --exclude=./root/.profile -xJp -f "${_basetxz}" || return
        done
        # In the original archive they are archived as hardlinks: make proper symlinks here
        (cd "${_mp}/skeleton/root" && ln -s ../../.profile .profile) || return
        (cd "${_mp}/skeleton/root" && ln -s ../../.cshrc .cshrc) || return
    else
        echo "Extracting base ..."
        tar -C "${_mp}" --exclude=./root/.cshrc --exclude=./root/.profile --no-safe-writes -xJp -f "${_basetxz}" || return
        # In the original archive they are archived as hardlinks: make proper symlinks here
        (cd "${_mp}/root" && ln -s ../.profile .profile) || return
        (cd "${_mp}/root" && ln -s ../.cshrc .cshrc) || return
    fi

    if [ \( "${_opt_preserve_boot}" = "yes" \) -o \( -n "${_kerneltxz}" \) ]; then
        if [ -n "${_kerneltxz}" ]; then
            echo "Extracting kernel ..."
            tar -C "${_mp}" -xJp -f "${_kerneltxz}" || return
        else
            echo "Preserved \"boot\""
        fi
    else
        find "${_mp}/boot" -type f -delete || true
    fi
}


#
# _do_mount dataset mountpoint dry-run mount-natural childs-only
#
_do_mount() {
    local _dsname _mountpoint _dry_run _mount_natural _childs_only

    local _name _mp _canmount _mounted
    local _rootds_mountpoint _relative_mp _real_mp

    _dsname="${1}"
    _mountpoint="${2}"
    _dry_run="${3}"
    _mount_natural="${4}"
    _childs_only="${5}"

    if [ -z "${_dsname}" ]; then
        echo "ERROR: no dataset given" >&2
        return 2
    fi

    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")"  || \
        { echo "ERROR: root dataset \`${_dsname}' does not exist" >&2; return 1; }

    if [ -z "${_mountpoint}" ]; then
        if [ "${_mount_natural}" = "yes" ]; then
            _mountpoint="${_rootds_mountpoint}"
        else
            echo "ERROR: no mountpoint given" >&2
            return 2
        fi
    else
        if [ "${_mount_natural}" = "yes" ]; then
            echo "ERROR: Cannot have a custom mountpoint when mount-natural is activated" >&2
            return 2
        fi
    fi

    # Eventually remove a trailing slash
    _mountpoint="${_mountpoint%/}"
    if [ -z "${_mountpoint}" ]; then
        echo "ERROR: would mount over the root filesystem" >&2
        return 1
    fi

    zfs list -H -o name,mountpoint,canmount,mounted -s mountpoint -t filesystem -r "${_dsname}" \
    | {
        while IFS=$'\t' read -r _name _mp _canmount _mounted ; do
            # Skip filesystems that are already mounted
            [ "${_mounted}" = "yes" ] && continue
            # Skip filesystems that must not be mounted
            [ "${_canmount}" = "off" ] && continue
            case "${_mp}" in
                "none"|"legacy")
                    # Do nothing for filesystem with unset or legacy mountpoints
                    ;;
                "${_rootds_mountpoint}"|"${_rootds_mountpoint}/"*)
                    #
                    # Handle only mountpoints that have a mountpoint below
                    # the parent datasets mountpoint
                    #

                    # Determine the mountpoint relative to the parent mountpoint
                    _relative_mp="${_mp#"${_rootds_mountpoint}"}"
                    # Eventually remove a trailing slash
                    _relative_mp="${_relative_mp%/}"
                    # The real effective full mountpoint
                    _real_mp="${_mountpoint}${_relative_mp}"

                    #
                    # Consistency and sanity check: computed real mountpoint must
                    # be equal to the configured mountpoint when no custom mountpoint
                    # is given.
                    #
                    if [ "${_mount_natural}" = "yes" ]; then
                        if [ "${_real_mp}" != "${_mp}" ]; then
                            echo "ERROR: mountpoint mismatch" 1>&2
                            return 1
                        fi
                    fi

                    if [ \( "${_childs_only}" = "yes" \) -a \( "${_name}" = "${_dsname}" \) ]; then
                        echo "Skipping ${_name} because mounting childs only" 1>&2
                    else
                        if [ "${_dry_run}" = "yes" ]; then
                            echo "Would mount ${_name} on ${_real_mp}"
                        else
                            mkdir -p "${_real_mp}" 1> /dev/null 2> /dev/null || \
                                { echo "ERROR: cannot create mountpoint ${_real_mp}" 1>&2; return 1; }
                            echo "Mounting ${_name} on ${_real_mp}"
                            /sbin/mount -t zfs "${_name}" "${_real_mp}" || return 1
                        fi
                    fi
                    ;;
                *)
                    echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 1>&2
                    ;;
            esac
        done

        return 0
    }
}


#
# "mount-tmpl" -- recursively mount a base and skeleton datasets including subordinate datasets
#
# command_mount_tmpl base-ro skeleton-rw  mountpoint
#
command_mount_tmpl() {
    local _ds_base _ds_skel _mountpoint
    local _opt_dry_run _opt_symlink

    local _opt

    _opt_dry_run=""
    _opt_symlink=""

    while getopts "LPnu" _opt ; do
        case "${_opt}" in
            L)
                _opt_symlink="yes"
                ;;
            P)
                _opt_symlink="no"
                ;;
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }

    _ds_base="${1-}"
    _ds_skel="${2-}"
    _mountpoint="${3-}"

    _do_mount "${_ds_base}" "${_mountpoint}" "${_opt_dry_run}" "" "" || return
    if [ "${_opt_symlink}" = "yes" ]; then
        if [ "${_opt_dry_run}" != "yes" ]; then
            if [ ! -d "${_mountpoint}/skeleton" ]; then
                mkdir "${_mountpoint}/skeleton" || return
            fi
        fi
        _do_mount "${_ds_skel}" "${_mountpoint}/skeleton" "${_opt_dry_run}" "" "" || return
    else
        _do_mount "${_ds_skel}" "${_mountpoint}" "${_opt_dry_run}" "" "yes" || return
    fi

    return 0
}


#
# _do_umount dataset
#
_do_umount() {
    local _dsname

    local _name _mp _rest
    local _rootds_mountpoint

    _dsname="${1}"
    [ -z "${_dsname}" ] && { echo "ERROR: no dataset given" >&2; return 2; }

    # Just determine whether the given dataset name exists
    _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}" 2>/dev/null)" || { err "dataset not found"; return 1; }

    _get_zfs_mounts_for_dataset_tree -r "${_dsname}" \
    | {
        while IFS=$'\t' read -r _name _mp _rest ; do
            echo "Umounting ${_name} on ${_mp}"
            /sbin/umount "${_mp}" || return 1
        done
        return 0
      }
}


#
# "umount-tmpl" -- umount skeleton and base datasets
#
# command_umount_tmpl ds-base ds-skeleton
#
command_umount_tmpl() {
    local _ds_base _ds_skel

    _ensure_no_options "$@"

    _ds_base="${1-}"
    _ds_skel="${2-}"

    [ -z "${_ds_base}" ] && { echo "ERROR: no RO base dataset given" >&2; return 2; }
    [ -z "${_ds_skel}" ] && { echo "ERROR: no RW skeleton dataset given" >&2; return 2; }

    _do_umount "${_ds_skel}" || return
    _do_umount "${_ds_base}" || return

    return 0
}


#
# "interlink-tmpl" -- create links from base to skeleton
#
# command_interlink_tmpl mountpint
#
command_interlink_tmpl() {
    local _mountpoint

    local _dir _dirpart _basepart

    _ensure_no_options "$@"

    _mountpoint="${1-}"

    [ -z "${_mountpoint}" ] && { echo "ERROR: no mountpoint given" 1>&2; return 2; }
    [ -d "${_mountpoint}" ] || { echo "ERROR: mountpoint \`${_mountpoint}' does not exist" 1>&2; return 1; }
    [ -d "${_mountpoint}/skeleton" ] || { echo "WARNING: skeleton is not mounted at \`${_mountpoint}/skeleton'" 1>&2; }

    for _dir in etc home root tmp usr/local var ; do
        case "${_dir}" in
            "usr/local")
                _dirpart="$(dirname "${_dir}")"
                _basepart="$(basename "${_dir}")"
                [ -d "${_mountpoint}/${_dirpart}" ] || mkdir "${_mountpoint}/${_dirpart}" || return
                ( cd "${_mountpoint}/${_dirpart}" && ln -s "../skeleton/${_dir}" "${_basepart}" ) || return
                ;;
            *)
                ( cd "${_mountpoint}" && ln -s "skeleton/${_dir}" "${_dir}" ) || return
                ;;
        esac
    done
    return 0
}

#:
#: Create a snapshot for a dataset and all of its children.
#:
#: Args:
#:     $1: the datasets
#:     $2: the name of the snapshot
#:
_do_snapshot() {
    local _ds _snap_name

    _ds="${1}"
    _snap_name="${2}"

    [ -z "${_ds}" ] && { echo "ERROR: no dataset given" 1>&2; return 2; }
    [ -z "${_snap_name}" ] && { echo "ERROR: no snapshot name given" 1>&2; return 2; }

    if ! zfs list -H -o name -t filesystem "${_ds}" 1> /dev/null 2> /dev/null ; then
        echo "ERROR: parent dataset \`${_ds}' does not exist or is not available" 1>&2
        return 1
    fi

    zfs snapshot -r "${_ds}@${_snap_name}" || return
}


#:
#: Implement the "snapshot-tmpl" command
#:
command_snapshot_tmpl() {
    local _ds_base _ds_skel _snap_name

    _ensure_no_options "$@"

    _ds_base="${1-}"
    _ds_skel="${2-}"
    _snap_name="${3-}"

    # Here extra checks because of better error messages possible
    [ -z "${_ds_base}" ] && { echo "ERROR: no RO base dataset name given" 1>&2; return 2; }
    [ -z "${_ds_skel}" ] && { echo "ERROR: no RW skeleton dataset name given" 1>&2; return 2; }

    _do_snapshot "${_ds_base}" "${_snap_name}" || return
    _do_snapshot "${_ds_skel}" "${_snap_name}" || return
    return 0
}


#:
#: Implementation of "copy-skel"
#:
command_copy_skel() {
    local _ds_source _snapshot_name _ds_target
    local _opt_symlink _opt_nomount _opt_canmount _opt_mountpoint _opt_nodata

    local _opt _name _relative_name  _root_canmount

    _opt_symlink=""
    _opt_nomount=""
    _opt_canmount="-o canmount=on"
    _opt_mountpoint=""
    _opt_nodata=""

    while getopts "ADLM:Pu" _opt ; do
        case "${_opt}" in
            A)
                _opt_canmount="-o canmount=noauto"
                ;;
            D)
                _opt_nodata="yes"
                ;;
            L)
                _opt_symlink="yes"
                ;;
            M)
                _opt_mountpoint="${OPTARG}"
                ;;
            P)
                _opt_symlink="no"
                ;;
            u)
                _opt_nomount="-u"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    [ -z "${_opt_symlink}" ] && { echo "ERROR: -L or -P must be given" 1>&2; return 2; }
    [ \( "${_opt_nodata}" = "yes" \) -a \( "${_opt_symlink}" = "yes" \) ] && { echo "ERROR: -L and -D are incompatible" 1>&2; return 2; }

    _ds_source="${1-}"
    _snapshot_name="${2-}"
    _ds_target="${3-}"

    [ -z "${_ds_source}" ] && { echo "ERROR: no source given" 1>&2; return 2; }
    [ -z "${_snapshot_name}" ] && { echo "ERROR: no snapshot name given" 1>&2; return 2; }
    [ -z "${_ds_target}" ] && { echo "ERROR: no target given" 1>&2; return 2; }

    zfs list -r -t all -o name "${_ds_source}" \
    | {
        while IFS=$'\t' read -r _name ; do
            if [ "${_name}" = "${_name%@*}@${_snapshot_name}" ]; then
                echo "FOUND: $_name"
                # Determine the relative name of the dataset
                _relative_name="${_name#"${_ds_source}"}"
                _relative_name="${_relative_name%@*}"
                echo "  -> $_relative_name"
                if [ -z "${_relative_name}" ]; then
                    #
                    # Root
                    #
                    if [ "${_opt_symlink}" = "yes" ]; then
                        _root_canmount="${_opt_canmount}"
                    else
                        _root_canmount="-o canmount=off"
                    fi
                    if [ -n "${_opt_mountpoint}" ]; then
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v ${_root_canmount} -o "mountpoint=${_opt_mountpoint}" -o readonly=off "${_ds_target}${_relative_name}"
                    else
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -o readonly=off -v ${_root_canmount} -x mountpoint "${_ds_target}${_relative_name}"
                    fi
                else
                    #
                    # Children
                    #
                    if [ "${_relative_name}" = "/usr" ]; then
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v -o canmount=off -x mountpoint "${_ds_target}${_relative_name}"
                    else
                        zfs send -Lec -p -v "${_name}" | zfs receive ${_opt_nomount} -v ${_opt_canmount} -x mountpoint "${_ds_target}${_relative_name}"
                    fi
                fi
            fi
        done
    }
    # Need only the filesystem data (no associated snapshots)
    echo "Destroying unneeded snapshots ..."
    zfs destroy -rv "${_ds_target}@${_snapshot_name}"
}


#:
#: Implement the "build-etcupdate-current-tmpl" command
#:
command_build_etcupdate_current_tmpl() {
    local _directory _tarball

    _directory="${1-}"
    _tarball="${2-}"

    [ -z "${_directory}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
    [ -z "${_tarball}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
    [ -e "${_tarball}" ] && { echo "ERROR: \`${_tarball}' exists already" 1>&2; return 1; }

    if ! tar -cjf "${_tarball}" -C "${_directory}/var/db/etcupdate/current" . ; then
        rm -f "${_tarball}" || true
        return 1
    fi
}


#:
#: Determine extra clone options with respect to the "mountpoint" property
#:
#: Args:
#:   $1: the dataset
#:
#: Output (stdout)
#:   The extra clone arguments
#:
#: Exit:
#:   On unexpected source values
#:
_get_clone_extra_prop_for_mountpoint() {
    local ds

    local _mp_name _mp_property _mp_value _mp_source

    ds="${1}"

    zfs get -H mountpoint "${ds}" \
    | {
        IFS=$'\t' read -r _mp_name _mp_property _mp_value _mp_source
        case "${_mp_source}" in
            local)
                printf '%s' "-o mountpoint=${_mp_value}"
                ;;
            default|inherited*)
                ;;
            temporary*|received*|'-'|none)
                # XXX FIXME: Is this relevant on FreeBSD?
                echo "ERROR: Unexpected SOURCE \"${_mp_source}\" for mountpoint at \`${_mp_value}'" 1>&2
                exit 1
                ;;
            *)
                echo "ERROR: Unexpected SOURCE for mountpoint property at \`${_mp_value}'" 1>&2
                exit 1;
                ;;
        esac
        if [ "${_mp_value}" != "${_directory}" ]; then
            echo "WARNING: dataset is not mounted at its configured mountpoint but elsewhere (probably via \"mount -t zfs\")" 1>&2
        fi
    }
}


#:
#: Determine the "canmount" property for a dataset
#:
#: Args:
#:   $1: the dataset
#:
#: Output (stdout):
#:   The local value or "DEFAULT" for the (unset) default
#:
#: Exit:
#:   On unexpected source values
#:
_get_canmount_setting_for_dataset() {
    local ds

    local _cm_name _cm_property _cm_value _cm_source

    ds="${1}"

    zfs get -H canmount "${ds}" \
    | {
        IFS=$'\t' read -r _cm_name _cm_property _cm_value _cm_source
        case "${_cm_source}" in
            local)
                printf '%s' "canmount=${_cm_value}"
                ;;
            default)
                printf '%s' "DEFAULT"
                ;;
            inherited|temporary*|received*|'-'|none)
                # XXX FIXME: Is this relevant on FreeBSD?
                echo "ERROR: Unexpected SOURCE \"${_cm_source}\" for canmount at \`${_cm_name}'" 1>&2
                exit 1
                ;;
            *)
                echo "ERROR: Unexpected SOURCE for canmount property at \`${_cm_name}'" 1>&2
                exit 1;
                ;;
        esac
    }
}


#:
#: Callback for _print_check_errors
#:
_print_check_error() {
    printf '%s CHECK: %s\n' '-' "$3" 1>&2
    return 0
}


#:
#: Print all the errors to stderr
#:
_print_check_errors() {
    farray_istrue "$1" || return 0
    echo "There are ERRORs to be resolved before \`freebsd-update' can be run:" 1>&2
    farray_for_each "$1" _print_check_error
}


#:
#: Callback for _print_check_warnings
#:
_print_check_warning() {
    printf '%s WARNING: %s\n' '-' "$3" 1>&2
    return 0
}


#:
#: Print all the warnings to stderr
#:
_print_check_warnings() {
    farray_istrue "$1" || return 0
    echo "There are WARNINGs to be considered before \`freebsd-update' should be run:" 1>&2
    farray_for_each "$1" _print_check_warning
}


#:
#: Implement the "check-freebsd-update" command for a thin jail
#:
command_check_freebsd_update() {
    local _directory _new_origin _etcupdate_tarball
    local _opt_keep _opt_old_origin _opt_snapshots

    local _errors _warnings _rc
    local _directory _new_origin _etcupdate_tarball
    local _dir_basename _dir_mounts _jailname _running_jailname
    local _tmp _line _log_sock
    local _root_dataset _root_mountpoint _root_type _root_options
    local _mnt_device _mnt_mountpoint _mnt_type _mnt_options
    local _idx _sn_ds _sn_name _sn_ds_related
    local _etcupdate_status

    _rc=0

    _warnings=''
    farray_create _warnings
    _errors=''
    farray_create _errors

    _opt_snapshots=''
    falist_create _opt_snapshots
    _opt_keep="no"
    _opt_old_origin=""
    while getopts "R:ko:" _opt ; do
        case "${_opt}" in
            R)
                case "${OPTARG}" in
                    *?\@?*)
                        #
                        # Split in two parts: dataset hierarchy and the name of the
                        # snapshot
                        #
                        falist_set _opt_snapshots "${OPTARG%%@*}" "${OPTARG#*@}"
                        ;;
                    *)
                        farray_append _errors "argument \`${OPTARG}' is not a snapshot name"
                        ;;
                    esac
                ;;
            k)
                _opt_keep="yes"
                ;;
            o)
                _opt_old_origin="${OPTARG}"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _directory="${1-}"
    _new_origin="${2-}"
    _etcupdate_tarball="${3-}"

    if [ -z "${_directory}" ]; then
        farray_append _errors "no directory given"
    else
        [ -d "${_directory}" ] || farray_append _errors "directory \`${_directory}' does not exist"
    fi
    if [ -z "${_new_origin}" ]; then
        farray_append _errors "no new origin given"
    else
        zfs list -H -o name -t snapshot "${_new_origin}" >/dev/null 2>/dev/null || farray_append  _errors "ZFS dataset snapshot for the new origin \`${_new_origin}' does not exist"
    fi
    if [ -n "${_etcupdate_tarball}" ]; then
        [ -r "${_etcupdate_tarball}" ] || farray_append _errors "given etcupdate tarball does not exist and/or is not readable"
    fi

    # Check snapshotting
    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        if zfs get -H -o value name "${_sn_ds}" >/dev/null 2>/dev/null; then
            # yes dataset exists: check that snapshots do not exist
            while IFS=$'\t' read -r _line; do
                if zfs get -H -o value name "${_line}@${_sn_name}" >/dev/null 2>/dev/null; then
                    farray_append _errors "snapshot \`${_line}@${_sn_name}' already exists"
                fi
            done <<EOF2988ee715b2d93fd93bdce23
$(zfs list -H -r -o name "${_sn_ds}")
EOF2988ee715b2d93fd93bdce23
        else
            farray_append _errors "dataset for snapshots \`${_sn_ds}' does not exist"
        fi
        _idx=$((_idx + 1))
    done

    _jailname=''
    _running_jailname=''
    if [ -n "${_directory}" ]; then

        _dir_basename="$(basename "${_directory}")"

        set +e
        _jailname="$(_get_jail_from_path "${_directory}")"
        _tmp=$?
        set -e
        case ${_tmp} in
            0)
                farray_append _errors "Jail \`${_jailname}' is running. Please stop it."
                _running_jailname="${_jailname}"
                ;;
            1)
                farray_append _errors "Cannot determine jail name"
                ;;
            3)
                true
                ;;
            2)
                farray_append _errors "Jail \`${_jailname}' is currently yet dying. Please wait."
                ;;
            *)
                farray_append _errors "UNHANDLED RETURN VALUE from _get_jail_from_path()"
                ;;
        esac

        #
        # Check whether additional log sockets are opened at their default
        # locations. Because they hinder proper unmounting of filesystems.
        #
        for _log_sock in /var/run/log /var/run/logpriv ; do
            if [ -S "${_directory}${_log_sock}" ]; then
                farray_append _errors "log socket is open at \`${_directory}${_log_sock}'"
            fi
        done

        # Check whether there are any open files or VM mappings  within the jail.
        if ! _check_no_open_files_from_all_proc "${_directory}" ; then
            farray_append _errors "There are open files or memory mappings within the jail"
        fi

        _dir_mounts="$(_get_mounts_at_directory "${_directory}")"

        #
        # Check preconditions thoroughly!
        #
        # Check that the first item/line is a read-only ZFS mount directly
        # at the given directory. This must also be its configured
        # mountpoint in ZFS.
        # Also check that it is a clone proper.
        #
        IFS=$'\t' read -r _root_dataset _root_mountpoint _root_type _root_options _line <<EOF4tHGCSSf5d7d9cf
${_dir_mounts}
EOF4tHGCSSf5d7d9cf
        [ "${_root_mountpoint}" != "${_directory}" ] && farray_append _errors "found root mountpoint does not match given directory"
        [ "${_root_type}" != "zfs" ] && farray_append _errors "root mountpoint is not from a ZFS dataset"
        _root_readonly="$(zfs get -H -o value readonly "${_root_dataset}")"
        [ "${_root_readonly}" != "on" ] &&  farray_append _errors "the root dataset is not mounted read-only"
        _root_origin="$(zfs get -H -o value origin "${_root_dataset}")"
        if [ -n "${_opt_old_origin}" ]; then
            [ "${_opt_old_origin}" != "${_root_origin}" ] && farray_append _errors "origin mismatch"
        else
            [ "${_root_origin}" = '-' ] &&  farray_append _errors "the root dataset is not a ZFS clone"
        fi
        #
        # 1. Check for open files on all the mounted filesystems.
        # 2. Check for mounted filesystems that cannot re-mounted successfuly
        #    because the fstab does not contain all the needed informations
        #    (e.g. unionfs).
        # 3. If snapshots are requested check that they are related somehow to
        #    mounted filesystems.
        #
        _sn_ds_related=''
        while IFS=$'\t' read -r _mnt_device _mnt_mountpoint _mnt_type _mnt_options _line; do
            if ! _check_no_open_files_on_filesystem "${_mnt_mountpoint}" ; then
                farray_append _errors "There are open files or memory mapping on file system \`${_mnt_mountpoint}'"
            fi
            case "${_mnt_type}" in
                unionfs)
                    farray_append _errors "A \`${_mnt_type}' filesystem is mounted at \`${_mnt_mountpoint}' which cannot re-mounted properly"
                    ;;
                *)
                    true
                    ;;
            esac
            _idx=1
            while falist_tryget_key_at_index _sn_ds _opt_snapshots ${_idx}; do
                case "${_mnt_device}" in
                    "${_sn_ds}")
                        _sn_ds_related="yes"
                        ;;
                    "${_sn_ds}"/*)
                        _sn_ds_related="yes"
                        ;;
                    *)
                        ;;
                esac
                _idx=$((_idx + 1))
            done
        done <<EOF4tHGCAASL775f9f320205
${_dir_mounts}
EOF4tHGCAASL775f9f320205
    fi
    if falist_istrue _opt_snapshots; then
        if ! checkyes _sn_ds_related; then
            farray_append _warnings "snapshot datasets and mounted datasets are not related"
        fi
    fi

    #
    # Check whether conflicts remain from previous update, aborting.
    # This would result in errors when running etcupdate.
    #
    if [ -n "${_directory}" ]; then
        _etcupdate_status=''
        if [ -n "${_running_jailname}" ]; then
            _etcupdate_status="$(/usr/sbin/jexec -l -U root -- "${_running_jailname}" /usr/sbin/etcupdate status  2>&1 || true)"
        elif [ -d "${_directory}" ]; then
            _etcupdate_status="$(LC_ALL=C.UTF-8 /usr/sbin/etcupdate status -D "${_directory}" 2>&1 || true)"
        fi
        [ -n "${_etcupdate_status}" ] && farray_append _errors "Unresolved conflicts from last update. Please run \"etcupdate resolve\" first."
    fi

    if farray_istrue _errors; then
        _print_check_errors _errors
        _rc=1
    fi
    # Warnings do not influence the return code
    _print_check_warnings _warnings

    farray_destroy _errors
    farray_destroy _warnings
    falist_destroy _opt_snapshots
    return ${_rc}
}


#:
#: Implement the "freebsd-update" command for a thin jail
#:
#: .. note:: FreeBSD's :command:`etcupdate` also executes
#:           :command:`certctl rehash` if certs are to be added or removed!
#:
command_freebsd_update() {
    local _directory _new_origin _etcupdate_tarball
    local _opt_keep _opt_old_origin _opt_snapshots

    local _res _jailname _dir_mounts _dir_fn_fstab _dir_fn_fstab2
    local _dir_basename _dir_fn_tldir
    local _root_dataset _root_mountpoint _root_type _root_options
    local _mnt_device _mnt_mountpoint _mnt_type _mnt_options
    local _idx _sn_ds _sn_name
    local _clone_extra_props _canmount_prop
    local _line _opt
    local _root_readonly _root_origin
    local _u_tmpdir
    local _add_log_sock

    _opt_snapshots=''
    falist_create _opt_snapshots
    _opt_keep="no"
    _opt_old_origin=""
    while getopts "R:ko:" _opt ; do
        case "${_opt}" in
            R)
                case "${OPTARG}" in
                    *?\@?*)
                        #
                        # Split in two parts: dataset hierarchy and the name of the
                        # snapshot
                        #
                        falist_set _opt_snapshots "${OPTARG%%@*}" "${OPTARG#*@}"
                        ;;
                    *)
                        err "argument \`${OPTARG}' is not a snapshot name"
                        return 1
                        ;;
                    esac
                ;;
            k)
                _opt_keep="yes"
                ;;
            o)
                _opt_old_origin="${OPTARG}"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _directory="${1-}"
    _new_origin="${2-}"
    _etcupdate_tarball="${3-}"

    [ -z "${_directory}" ] && { echo "ERROR: no directory given" 1>&2; return 2; }
    [ -d "${_directory}" ] || { echo "ERROR: directory \`${_directory}' does not exist" 1>&2; return 1; }

    [ -z "${_new_origin}" ] && { echo "ERROR: no new origin given" 1>&2; return 2; }
    zfs list -H -o name -t snapshot "${_new_origin}" >/dev/null 2>/dev/null || { echo "ERROR: ZFS dataset snapshot for the new origin \`${_new_origin}' does not exist" 1>&2; return 1; }
    if [ -n "${_etcupdate_tarball}" ]; then
        [ -r "${_etcupdate_tarball}" ] || { echo "ERROR: given etcupdate tarball does not exist and/or is not readable" 1>&2; return 1; }
    fi

    # Check snapshotting
    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        if zfs get -H -o value name "${_sn_ds}" >/dev/null 2>/dev/null; then
            # yes dataset exists: check that snapshots do not exist
            while IFS=$'\t' read -r _line; do
                if zfs get -H -o value name "${_line}@${_sn_name}" >/dev/null 2>/dev/null; then
                    err "snapshot \`${_line}@${_sn_name}' already exists"
                    return 1
                fi
            done <<EOF2988ee715b2d93fd93bdce23
$(zfs list -H -r -o name "${_sn_ds}")
EOF2988ee715b2d93fd93bdce23
        else
            err "dataset for snapshots \`${_sn_ds}' does not exist"
            return 1
        fi
        _idx=$((_idx + 1))
    done

    _dir_basename="$(basename "${_directory}")"

    set +e
    _jailname="$(_get_jail_from_path "${_directory}")"
    _res=$?
    set -e
    case ${_res} in
        0)
            err "Please stop the \`${_jailname}' jail"
            return 1
            ;;
        1)
            return 1
            ;;
        2)
            err "Jail \`${_jailname}' is currently yet dying"
            return 1
            ;;
        3)
            true
            ;;
        *)
            return ${_res}
            ;;
    esac

    #
    # Check whether additional log sockets are opened at their default
    # locations. Because they hinder proper unmounting of filesystems.
    #
    for _add_log_sock in /var/run/log /var/run/logpriv ; do
        if [ -S "${_directory}${_add_log_sock}" ]; then
            echo "ERROR: additional log socket is open at \`${_directory}${_add_log_sock}'" >&2
            return 1
        fi
    done
    # Check whether there are any open files or VM mappings  within the jail.
    if ! _check_no_open_files_from_all_proc "${_directory}" ; then
        err "There are open files or memory mappings within the jail"
        return 1
    fi

    _dir_mounts="$(_get_mounts_at_directory "${_directory}")"

    #
    # Check preconditions thoroughly!
    #
    # Check that the first item/line is a read-only ZFS mount directly
    # at the given directory. This must also be its configured
    # mountpoint in ZFS.
    # Also check that it is a clone proper.
    #
    IFS=$'\t' read -r _root_dataset _root_mountpoint _root_type _root_options _line <<EOF4tHGCSSf5d7d9cf
${_dir_mounts}
EOF4tHGCSSf5d7d9cf
    [ "${_root_mountpoint}" != "${_directory}" ] && { echo "ERROR: found root mountpoint does not match given directory" 1>&2; return 1; }
    [ "${_root_type}" != "zfs" ] && { echo "ERROR: root mountpoint is not from a ZFS dataset" 1>&2; return 1; }
    _root_readonly="$(zfs get -H -o value readonly "${_root_dataset}")"
    [ "${_root_readonly}" != "on" ] &&  { echo "ERROR: the root dataset is not mounted read-only" 1>&2; return 1; }
    _root_origin="$(zfs get -H -o value origin "${_root_dataset}")"
    if [ -n "${_opt_old_origin}" ]; then
        [ "${_opt_old_origin}" != "${_root_origin}" ] && { echo "ERROR: origin mismatch" 1>&2; return 1; }
    else
        [ "${_root_origin}" = '-' ] &&  { echo "ERROR: the root dataset is not a ZFS clone" 1>&2; return 1; }
    fi
    #
    # 1. Check for open files on all the mounted filesystems.
    # 2. Check for mounted filesystems that cannot re-mounted successfuly
    #    because the fstab does not contain all the needed informations
    #    (e.g. unionfs).
    #
    while IFS=$'\t' read -r _mnt_device _mnt_mountpoint _mnt_type _mnt_options _line; do
        if ! _check_no_open_files_on_filesystem "${_mnt_mountpoint}" ; then
            err "There are open files or memory mapping on file system \`${_mnt_mountpoint}'"
            return 1
        fi
        case "${_mnt_type}" in
            unionfs)
                err "A \`${_mnt_type}' filesystem is mounted at \`${_mnt_mountpoint}' which cannot re-mounted properly"
                return 1
                ;;
            *)
                true
                ;;
        esac
    done <<EOF4tHGCAASLfafbf1b5
${_dir_mounts}
EOF4tHGCAASLfafbf1b5

    # Determine we need to clone with a custom (non inherited) "mountpoint"
    _clone_extra_props="$(_get_clone_extra_prop_for_mountpoint "${_root_dataset}") "
    # Determine we need to clone with a custom (non inherited) "canmount"
    _canmount_prop="$(_get_canmount_setting_for_dataset "${_root_dataset}")"

    #
    # XXX FIXME: should we check that _root_options equals "ro" or
    #            start with "ro,"
    #    _root_origin="$(zfs list -H -o origin "${_root_dataset}")"

    _u_tmpdir="$(env TMPDIR=/var/tmp mktemp -d -t ftjail_"${_dir_basename}")"
    [ -z "${_u_tmpdir}" ] && { echo "ERROR: cannot create unique temp dir" 1>&2; return 1; }
    # The fstab that is corrently mounted at relevant locations (normalized)
    _dir_fn_fstab="${_u_tmpdir}/fstab"
    # The very same fstab -- but with spaces replaced by \040
    _dir_fn_fstab2="${_u_tmpdir}/fstab2"
    printf '%s' "${_dir_mounts}" >"${_dir_fn_fstab}"
    # Replace all spaces with a sequence that is understood by mount
    LC_ALL=C /usr/bin/sed -e 's/ /\\040/g' <"${_dir_fn_fstab}" >"${_dir_fn_fstab2}"
    _dir_fn_tldir="${_u_tmpdir}/tldirs"
    LC_ALL=C /usr/bin/find "${_directory}" -depth 1 -type d 2>/dev/null | LC_ALL=C /usr/bin/sort >>"${_dir_fn_tldir}"

    _idx=1
    while falist_tryget_item_at_index _sn_ds _sn_name _opt_snapshots ${_idx}; do
        echo "Creating snapshot \`${_sn_ds}@${_sn_name}'"
        zfs snapshot -r "${_sn_ds}@${_sn_name}" || { err "cannot snapshot \`${_sn_ds}@${_sn_name}'"; return 1; }
        _idx=$((_idx + 1))
    done

    # Unmount in reverse order: unmount can do it for us
    echo "Unmounting all datasets mounted at \`${_directory}'"
    /sbin/umount -a -F "${_dir_fn_fstab2}" -v

    #
    # XXX TBD: Hooks to create some new top-level dirs (/srv /proc et
    #          al.)  if needed: clone RW, mount, make the dirs,
    #          umount, make the clone RO and continue "normally" by
    #          completely mounting the stored fstab.
    #

    #
    # Destroy the current read-only root clone and make a new clone based
    # on the given new origin.
    # The new clone temporarily is RW and is not to be mounted automatically.
    # These both properties are set again below after the new base is
    # adjusted properly.
    #
    echo "Destroying the cloned root dataset \`${_root_dataset}'"
    zfs destroy -v "${_root_dataset}"
    echo "Cloning a new root dataset \`${_root_dataset}' from new origin \`${_new_origin}'"
    zfs clone -o readonly=off -o canmount=noauto ${_clone_extra_props} "${_new_origin}" "${_root_dataset}"
    #
    # NOTE: Always mount with "mount -t zfs" because a custom
    #       mountpoint is not reflected in the "mountpoint"
    #       property. So in scripts to be sure to unmount and re-mount
    #       at the same location always use "mount -t zfs".
    #
    echo "Remounting only the root dataset at \`${_directory}'"
    [ ! -d "${_directory}" ] && mkdir "${_directory}"
    /sbin/mount -t zfs "${_root_dataset}" "${_directory}"
    #
    # Re-create all currently missing top-level dirs (aka mountpoint)
    # in the new clone. Most probably they serve as mountpoints for other
    # datasets.
    #
    # XXX FIXME: Re-create the current mode bits and/or ACLs also.
    #            But most probably they are set properly in the mounted
    #            datasets.
    #
    echo "Recreating missing top-level directories"
    while IFS='' read -r _line ; do
        if [ ! -d "${_line}" ]; then
            echo "Recreating top-level directory: ${_line}"
            mkdir "${_line}"
        fi
    done < "${_dir_fn_tldir}"
    echo "Unmounting the new root dataset"
    /sbin/umount "${_directory}"
    echo "Re-setting some ZFS properties on the new cloned dataset"
    zfs set readonly=on "${_root_dataset}"
    #
    # Copy "canmount" properly last because it has been set to "noauto"
    # temporarily.
    #
    if [ -n "${_canmount_prop}" ]; then
        if [ "${_canmount_prop}" = "DEFAULT" ]; then
            #
            # "zfs inherit" is not possible for "canmount".
            # Use "inherit -S" to simulate a reset to "default" somewhat
            #
            # See also: https://github.com/openzfs/zfs/issues/5733
            #
            zfs inherit -S canmount "${_root_dataset}"
        else
            zfs set "${_canmount_prop}" "${_root_dataset}"
        fi
    fi

    # Mount again
    echo "Mounting all datasets rooted at \`${_directory}'"
    [ ! -d "${_directory}" ] && mkdir "${_directory}"
    /sbin/mount -a -F "${_dir_fn_fstab2}" -v

    # Update and/or merge configs
    if [ -n "${_etcupdate_tarball}" ]; then
        # Note: Check for readability has been done above
        echo "Calling etcupdate for DESTDIR=${_directory}"
        LC_ALL=C.UTF-8 /usr/sbin/etcupdate -D "${_directory}" -t "${_etcupdate_tarball}"
    fi

    echo "Checking status of etcupdate at DESTDIR=${_directory}"
    LC_ALL=C.UTF-8 /usr/sbin/etcupdate status -D "${_directory}" || true

    if [ "${_opt_keep}" != "yes" ]; then
        echo "Cleaning up..."""
        [ -n "${_u_tmpdir}" ] && [ -d "${_u_tmpdir}" ] && rm -rvf "${_u_tmpdir}"
    fi
    echo "Done."

    falist_destroy _opt_snapshots
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case "${_opt}" in
        V)
            printf 'ftjail %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
            ;;
        h)
            echo "${USAGE}"
            exit 0
            ;;
        \?)
            exit 2;
            ;;
        *)
            echo "ERROR: option handling failed" 1>&2
            exit 2
            ;;
    esac
done

#
# Reset the Shell's option handling system to prepare for handling
# command-local options.
#
shift $((OPTIND-1))
OPTIND=1

test $# -gt 0 || { echo "ERROR: no command given" 1>&2; exit 2; }

command="$1"
shift

case "${command}" in
    datasets-tmpl)
        command_datasets_tmpl "$@"
        ;;
    mount-tmpl)
        command_mount_tmpl "$@"
        ;;
    umount-tmpl|unmount-tmpl)
        command_umount_tmpl "$@"
        ;;
    interlink-tmpl)
        command_interlink_tmpl "$@"
        ;;
    populate-tmpl)
        command_populate_tmpl "$@"
        ;;
    snapshot-tmpl)
        command_snapshot_tmpl "$@"
        ;;
    copy-skel)
        command_copy_skel "$@"
        ;;
    build-etcupdate-current-tmpl)
        command_build_etcupdate_current_tmpl "$@"
        ;;
    configure)
        echo "ERROR: use \`fjail configure' instead" 1>&2;
        exit 2
        ;;
    check-freebsd-update)
        command_check_freebsd_update "$@"
        ;;
    freebsd-update)
        command_freebsd_update "$@"
        ;;
    *)
        fatal 2 "unknown command \`${command}'"
        ;;
esac