view sbin/fzfs @ 743:6fcf7da87981

farray.sh: implement "falist_merge()" to merge two "sorted" alists and add the result to a resulting alist
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 08 Oct 2024 15:52:30 +0200
parents 8f1583faf9ea
children e2f262ec2bf4
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: A ZFS management helper 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: fzfs [ 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:

  clone-tree [-k] [-n] SOURCE-DATASET DEST-DATASET

  copy-tree [-A] [-M MOUNTPOINT] [-n] [-u] SOURCE-DATASET DEST-DATASET

  create-tree [-A] [-M MOUNTPOINT] [-n] [-p] [-u] SOURCE-DATASET DEST-DATASET

  mount [-O] [-N] [-P] [-k] [-u] [-n] DATASET [MOUNTPOINT]

  umount [-f] [-k] DATASET

  unmount [-f] [-k] DATASET

'


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


#
#: Implementation of the "mount" command.
#:
#: Mount a dataset and recursively all its children datasets.
#:
command_mount() {
    local _dsname _mountpoint
    local _opt_dry_run _opt_mount_outside _opt_mount_natural
    local _opt_mount_children_only _opt_keep

    local _name _mp _canmount _mounted _rootds_mountpoint _rootds_mountpoint_prefix _relative_mp _real_mp
    local _mounted_datasets

    _opt_dry_run=""
    _opt_keep=""
    _opt_mount_outside=""
    _opt_mount_natural=""
    _opt_mount_children_only=""
    while getopts "ONPknu" _opt ; do
        case ${_opt} in
            O)
                _opt_mount_outside="yes"
                ;;
            N)
                _opt_mount_natural="yes"
                ;;
            P)
                _opt_mount_children_only="yes"
                ;;
            k)
                _opt_keep="yes"
                ;;
            n|u)
                _opt_dry_run="yes"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _dsname="${1-}"
    _mountpoint="${2-}"

    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 does not exist" >&2; return 1; }

    if [ -z "${_mountpoint}" ]; then
        if [ "${_opt_mount_natural}" = "yes" ]; then
            _mountpoint="${_rootds_mountpoint}"
        else
            echo "ERROR: no mountpoint given" >&2
            return 2
        fi
    else
        if [ "${_opt_mount_natural}" = "yes" ]; then
            echo "ERROR: Cannot have a custom mountpoint when \"-O\" is given" >&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

    if [ "${_rootds_mountpoint}" = "/" ]; then
        _rootds_mountpoint_prefix="/"
    else
        _rootds_mountpoint_prefix="${_rootds_mountpoint}/"
    fi

    zfs list -H -o name,mountpoint,canmount,mounted -s mountpoint -t filesystem -r "${_dsname}" \
    | {

        #
        # _mounted_datasets is an array of ZFS datasets that have been
        # mounted by this routine and should be unmounted on errors
        # -- if possible.
        #
        farray_create _mounted_datasets

        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
            #
            # Mount only the children and skip the given parent dataset
            # if required
            #
            [ \( "${_opt_mount_children_only}" = "yes" \) -a \( "${_name}" = "${_dsname}" \) ] && continue
            case "${_mp}" in
                "none"|"legacy")
                    # Do nothing for filesystem with unset or legacy mountpoints
                    ;;
                "${_rootds_mountpoint}"|"${_rootds_mountpoint_prefix}"*)
                    #
                    # Handle only mountpoints that have a mountpoint below
                    # or exactly at the parent datasets mountpoint
                    #

                    #
                    # Determine the mountpoint relative to the parent
                    # mountpoint. Extra effort is needed because the root
                    # filesystem mount is just a single slash.
                    #
                    if [ "${_mp}" = "${_rootds_mountpoint}" ]; then
                        if [ "${_name}" != "${_dsname}" ]; then
                            echo "ERROR: child dataset mounts over root dataset" >&2
                            if ! checkyes _opt_keep; then
                                _umount_datasets _mounted_datasets || true
                            fi
                            return 1
                        fi
                        _relative_mp=""
                        _real_mp="${_mountpoint}"
                    else
                        _relative_mp="${_mp#"${_rootds_mountpoint_prefix}"}"
                        # Eventually remove a trailing slash
                        _relative_mp="${_relative_mp%/}"
                        if [ -z "${_relative_mp}" ]; then
                            echo "ERROR: got an empty relative mountpoint in child" >&2
                            if ! checkyes _opt_keep; then
                                _umount_datasets _mounted_datasets || true
                            fi
                            return 1
                        fi
                        # The real effective full mountpoint
                        _real_mp="${_mountpoint}/${_relative_mp}"
                    fi

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

                    if [ "${_opt_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}" >&2; _umount_datasets _mounted_datasets || true; return 1; }
                        echo "Mounting ${_name} on ${_real_mp}"
                        if /sbin/mount -t zfs "${_name}" "${_real_mp}"; then
                            farray_append _mounted_datasets "${_name}"
                        else
                            if ! checkyes _opt_keep; then
                                _umount_datasets _mounted_datasets || true
                            fi
                            return 1
                        fi
                    fi
                    ;;
                *)
                    if [ "${_opt_mount_outside}" = "yes" ]; then
                        if [ "${_opt_dry_run}" = "yes" ]; then
                            echo "Would mount ${_name} on configured ZFS dataset mountpoint ${_mp}"
                        else
                            echo "Mounting ${_name} on configured ZFS dataset mountpoint ${_mp}"
                            if zfs mount "${_name}"; then
                                farray_append _mounted_datasets "${_name}"
                            else
                                if ! checkyes _opt_keep; then
                                    _umount_datasets _mounted_datasets || true
                                fi
                                return 1
                            fi
                        fi
                    else
                        echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 2>&1
                    fi
                    ;;
            esac
        done
        farray_release _mounted_datasets
        return 0
    }
}


#:
#: Helper to unmount mounted ZFS filesystems
#:
#: Args:
#:   $1 (str): The name of the array that contains the datasets to unmount to
#:
#: Returns:
#:   0 if successfully unmounted all given datasets,
#:   1 otherwise
#:
#: Unmounting is done in the reverse order.
#:
_umount_datasets() {
    farray_reversed_for_each "$1" _umount_datasets_umount
}


#:
#: Array callback to unmount a single ZFS filesystem
#:
_umount_datasets_umount() {
    if ! zfs umount "$3" ; then
        warn "Dataset \`${3}' cannot unmounted"
        return 1
    fi
    return 0
}


#:
#: Implement the "umount" command.
#:
#: Umount a datasets and recursively all its children datasets.
#:
command_umount() {
    local _dsname
    local _opt_force _opt_dry_run

    local _opt _name _mp _rest _rootds_mountpoint

    _opt_force=""
    _opt_dry_run=""

    while getopts "fk" _opt ; do
        case ${_opt} in
            f)
                _opt_force="-f"
                ;;
            k)
                _opt_dry_run="yes"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _dsname="${1-}"
    [ -z "${_dsname}" ] && { err "no dataset given"; 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
            if checkyes _opt_dry_run ; then
                if [ -z "${_opt_force}" ]; then
                    echo "Would umount ${_name} from ${_mp}"
                else

                    echo "Would forcefully umount ${_name} from ${_mp}"
                fi
            else
                echo "Umounting ${_name} on ${_mp}"
                /sbin/umount ${_opt_force} "${_mp}" || return 1
            fi
        done
        return 0
      }
}


#:
#: Implementation of "copy-tree"
#:
command_copy_tree() {
    local _ds_source _ds_target
    local _opt_mountpoint _opt_mount_noauto _opt_nomount _opt_keep _opt_dry_run

    local _ds_source_base _ds_source_snapshot _snapshot_suffix
    local _ds_tree _ds _ds_relname _ds_canmount
    local _arg_canmount _arg_mp1 _arg_mp2

    _opt_mountpoint=""
    _opt_mount_noauto=""
    _opt_nomount=""
    _opt_keep=""
    _opt_dry_run=""

    while getopts "AM:knu" _opt ; do
        case ${_opt} in
            A)
                _opt_mount_noauto="-o canmount=noauto"
                ;;
            M)
                _opt_mountpoint="${OPTARG}"
                ;;
            k)
                _opt_keep="yes"
                ;;
            n)
                _opt_dry_run="-n"
                ;;
            u)
                _opt_nomount="-u"
                ;;
            \?)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

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

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

    if ! zfs get -H name "${_ds_source}" >/dev/null 2>&1; then
        echo "ERROR: source dataset does not exist: ${_ds_source}" 1>&2;
        return 1;
    fi
    if zfs get -H name "${_ds_target}" >/dev/null 2>&1; then
        echo "ERROR: target dataset already exists: ${_ds_target}" 1>&2;
        return 1;
    fi

    _ds_source_base="${_ds_source%@*}"
    _ds_source_snapshot="${_ds_source##*@}"
    _snapshot_suffix="@${_ds_source_snapshot}"

    if [ "${_ds_source_snapshot}" = "${_ds_source_base}" ]; then
        # No snapshot given
        [ "${_ds_source_base}" = "${_ds_source}" ] || { echo "ERROR:" 1>&2; return 1; }
        _ds_source_snapshot=""
        _snapshot_suffix=""
    fi

    _ds_tree=""

    while IFS=$'\n' read -r _ds; do
        if [ -z "${_ds_tree}" ]; then
            _ds_tree="${_ds}"
        else
            _ds_tree="${_ds_tree}"$'\n'"${_ds}"
        fi
    done <<EOF20ee7ea0781414fab8c305d3875d15e
$(zfs list -H -r -t filesystem -o name -s name "${_ds_source_base}")
EOF20ee7ea0781414fab8c305d3875d15e
    # Check the existence of all intermediate datasets and their shapshots
    IFS=$'\n'
    for _ds in ${_ds_tree}; do
        if  ! zfs get -H name "${_ds}${_snapshot_suffix}" >/dev/null 2>&1; then
            echo "ERROR: child dataset does not exist: ${_ds}${_snapshot_suffix}" 1>&2
            return 1
        fi
    done
    IFS=$'\n'
    for _ds in ${_ds_tree}; do
        # Reset IFS
        IFS=$' \t\n'

        # Determine the relative name of the dataset
        _ds_relname="${_ds#"${_ds_source_base}"}"

        _ds_canmount=$(zfs get -H -o value canmount "${_ds}")

        if [ "${_ds_canmount}" = "off" ]; then
            _arg_canmount="-o canmount=off"
        else
            _arg_canmount="${_opt_mount_noauto}"
        fi

        if [ -z "${_ds_relname}" ]; then
            #
            # Source root to target root
            #
            if [ -z "${_opt_mountpoint}" ]; then
                _arg_mp1=-x
                _arg_mp2=mountpoint
            else
                _arg_mp1=-o
                _arg_mp2="mountpoint=${_opt_mountpoint}"
            fi
        else
            _arg_mp1=-x
            _arg_mp2=mountpoint
        fi
        if [ -z "${_opt_dry_run}" ]; then
            zfs send -Lec -p -v "${_ds}${_snapshot_suffix}" | zfs receive -v ${_opt_nomount} ${_arg_canmount} ${_arg_mp1} "${_arg_mp2}" "${_ds_target}${_ds_relname}"
        else
            echo "Would execute: zfs send -Lec -p -v '${_ds}${_snapshot_suffix}' | zfs receive -v ${_opt_nomount} ${_arg_canmount} ${_arg_mp1} '${_arg_mp2}' '${_ds_target}${_ds_relname}'"
        fi
        # for the loop
        IFS=$'\n'
    done
    # Reset to default
    IFS=$' \t\n'
    # Remove received snapshots by default
    if [ -n "${_ds_source_snapshot}" ]; then
        if [ -z "${_opt_keep}" ]; then
            if [ -z "${_opt_dry_run}" ]; then
                zfs destroy -rv "${_ds_target}${_snapshot_suffix}"
            else
                echo "Would execute: zfs destroy -rv '${_ds_target}${_snapshot_suffix}'"
            fi
        fi
    fi
}


#:
#: Implementation of "clone-tree"
#:
command_clone_tree() {
    local _ds_source _ds_dest
    local _opt_keep _opt_dry_run

    local _ds _snapshot_name _ds_source_base _ds_relname
    local _ds_canmount _ds_mountpoint
    local _clone_props _arg_canmount _args_other_clone_props
    local _opt _idx _idx_lp _prop _propvalue
    local _ds_tree _cloned_datasets _local_props

    _opt_dry_run=""
    _opt_keep=""

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

    _ds_source="${1-}"
    _ds_dest="${2-}"

    [ -z "${_ds_source}" ] && { err "no source dataset/snapshot given"; return 2; }
    [ -z "${_ds_dest}" ] && { err "no destination name given"; return 2; }

    if ! zfs get -H name "${_ds_source}" >/dev/null 2>&1; then
        err "source dataset does not exist: ${_ds_source}"
        return 1
    fi
    if zfs get -H name "${_ds_dest}" >/dev/null 2>&1; then
        err "destination dataset already exists: ${_ds_dest}"
        return 1
    fi

    _ds_source_base="${_ds_source%@*}"
    _snapshot_name="${_ds_source##*@}"

    if [ "${_ds_source_base}" = "${_snapshot_name}" ]; then
        err "no snapshot given"
        return 1
    fi

    _ds_tree=""
    farray_create _ds_tree

    while IFS=$'\n' read -r _ds; do
        farray_append _ds_tree "${_ds}"
    done <<EOF_9ef07253679011efa78174d435fd3892
$(zfs list -H -r -t filesystem -o name -s name "${_ds_source_base}")
EOF_9ef07253679011efa78174d435fd3892

    # Check the existence of all intermediate datasets and their shapshots
    _idx=1
    while farray_tryget _ds _ds_tree ${_idx}; do
        if  ! zfs get -H name "${_ds}@${_snapshot_name}" >/dev/null 2>&1; then
            err "child dataset (snapshot) does not exist: ${_ds}@${_snapshot_name}" 1>&2
            return 1
        fi
        _idx=$((${_idx} + 1))
    done

    _cloned_datasets=""
    farray_create _cloned_datasets
    _local_props=""
    falist_create _local_props
    _args_other_clone_props=""
    farray_create _args_other_clone_props

    #
    # 1. Clone with "safe" canmount settings
    #

    _idx=1
    while farray_tryget _ds _ds_tree ${_idx}; do
        # Determine the relative name of the dataset
        _ds_relname="${_ds#"${_ds_source_base}"}"

        # Need to determine in *every* case (local, default, received, ...)
        _ds_canmount="$(zfs get -H -o value canmount "${_ds}")"

        falist_clear _local_props
        while IFS=$'\t' read -r _prop _propvalue ; do
            falist_set _local_props "${_prop}" "${_propvalue}"
        done <<EOF_ce8c76187f33471f8e8c1607ed09c42e
$(zfs get -H -o property,value -s local,received all "${_ds}")
EOF_ce8c76187f33471f8e8c1607ed09c42e

        #
        # - "zfs clone" does NOT copy/clone properties.
        #
        #   Clones inherit their properties from the target filesystem
        #   context or are set back to their ZFS default.
        #
        if [ "${_ds_canmount}" = "off" ]; then
            _arg_canmount="-o canmount=off"
        else
            _arg_canmount="-o canmount=noauto"
        fi

        # Copy all local props with the exception of canmount and mountpoint
        _idx_lp=1
        while falist_tryget_item_at_index _prop _propvalue _local_props ${_idx_lp}; do
            if [ "${_prop}" = "mountpoint" ]; then
                _idx_lp=$((${_idx_lp} + 1))
                continue
            fi
            if [ "${_prop}" = "canmount" ]; then
                _idx_lp=$((${_idx_lp} + 1))
                continue
            fi
            farray_append _args_other_clone_props "-o" "${_prop}"="${_propvalue}"
            _idx_lp=$((${_idx_lp} + 1))
        done

        if ! checkyes _opt_dry_run; then
            echo "Cloning ${_ds}@${_snapshot_name} into ${_ds_dest}${_ds_relname} with ${_arg_canmount} $(farray_print_join_for_eval _args_other_clone_props)"
            if eval "zfs clone \${_arg_canmount} $(farray_print_join_for_eval _args_other_clone_props) \"\${_ds}@\${_snapshot_name}\" \"\${_ds_dest}\${_ds_relname}\""; then
                farray_append _cloned_datasets "${_ds_dest}${_ds_relname}"
            else
                if ! checkyes _opt_keep; then
                    _destroy_datasets _cloned_datasets || true
                fi
                return 1
            fi
        else
            echo "Would execute: zfs clone ${_arg_canmount} $(farray_print_join_for_eval _args_other_clone_props) '${_ds}@${_snapshot_name}' '${_ds_dest}${_ds_relname}'"
        fi
        _idx=$((${_idx} + 1))
    done

    #
    # 2. Copy property mountpoint for root and inherit for all children;
    #    also handle canmount.
    #
    _idx=1
    while farray_tryget _ds _ds_tree ${_idx}; do
        # Determine the relative name of the dataset
        _ds_relname="${_ds#"${_ds_source_base}"}"

        # Need to determine in *every* case (default, local, received, ...)
        _ds_canmount="$(zfs get -H -o value canmount "${_ds}")"
        # Local mountpoint
        _ds_mountpoint="$(zfs get -H -o value -s local,received mountpoint "${_ds}")"

        if [ \( "${_ds_canmount}" = "off" \) -o \( "${_ds_canmount}" = "noauto" \) ]; then
            #
            # Already handled above because "nomount" is the default if not
            # already set to "off".
            #
            _arg_canmount=""
        else
            _arg_canmount="-o canmount=${_ds_canmount}"
        fi

        if [ \( -n "${_arg_canmount}" \) -o \( -n "${_ds_mountpoint}" \) ]; then
            if ! checkyes _opt_dry_run; then
                # If a local or received mountpoint is given set it here
                if [ -n "${_ds_mountpoint}" ]; then
                    echo "Correcting properties for ${_ds_dest}${_ds_relname}: ${_arg_canmount} mountpoint=\"${_ds_mountpoint}\""
                    zfs set -u ${_arg_canmount} mountpoint="${_ds_mountpoint}" "${_ds_dest}${_ds_relname}" || true
                else
                    echo "Correcting properties for ${_ds_dest}${_ds_relname}: ${_arg_canmount}"
                    zfs set -u ${_arg_canmount} "${_ds_dest}${_ds_relname}" || true
                fi
            else
                # If a local or received mountpoint is given set it here
                if [ -n "${_ds_mountpoint}" ]; then
                    echo "Would execute: zfs set -u ${_arg_canmount} mountpoint='${_ds_mountpoint}' '${_ds_dest}${_ds_relname}'"
                else
                    echo "Would execute: zfs set -u ${_arg_canmount} '${_ds_dest}${_ds_relname}'"
                fi
            fi
        fi
        _idx=$((${_idx} + 1))
    done
    farray_release _cloned_datasets
    farray_release _args_other_clone_props
    return 0
}


#:
#: Implement the "create-tree" command
#:
#: Create a ZFS dataset tree from a source tree tree including important properties
#:
command_create_tree() {
    local _source_ds _target_ds
    local _opt_dry_run _opt_mountpoint _opt_noauto _opt_nomount _opt_canmount_parent_only

    local _opt _source_name _snapshot_name _current_name _current_type _relative_name _zfs_opts _zfs_canmount _zfs_mountpoint

    _opt_noauto=""
    _opt_dry_run=""
    _opt_mountpoint=""
    _opt_nomount=""
    _opt_canmount_parent_only=""

    while getopts "AM:npu" _opt ; do
        case ${_opt} in
            A)
                _opt_noauto="-o canmount=noauto"
                ;;
            M)
                _opt_mountpoint="${OPTARG}"
                ;;
            n)
                _opt_dry_run="yes"
                ;;
            p)
                _opt_canmount_parent_only="yes"
                ;;
            u)
                _opt_nomount="-u"
                ;;
            \?|:)
                return 2;
                ;;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    _source_ds="${1-}"
    _target_ds="${2-}"

    [ -z "${_source_ds}" ] && { echo "ERROR: no source dataset given" 1>&2; return 2; }
    [ -z "${_target_ds}" ] && { echo "ERROR: no destination dataset given" 1>&2; return 2; }

    _source_name="${_source_ds%@*}"
    if [ "${_source_name}" != "${_source_ds}" ]; then
        _snapshot_name="${_source_ds##*@}"
    else
        _snapshot_name=""
    fi

    [ -n "${_snapshot_name}" ] && { echo "ERROR: No snapshot sources are supported yet" 1>&2; return 2; }

    zfs list -H -r -t filesystem -o name,type "${_source_ds}" \
    | {
        while IFS=$'\t' read -r _current_name _current_type ; do
            # Determine the relative name of the dataset
            _relative_name="${_current_name#"${_source_name}"}"
            _relative_name="${_relative_name%@*}"
            if [ "${_current_type}" != "filesystem" ]; then
                echo "ERROR: Got a snapshot but expected a filesystem" 1>&2
                return 1
            fi
            _zfs_opts="$(_get_local_zfs_properties_for_create "${_current_name}" atime exec setuid compression primarycache sync readonly)"
            if [ -z "${_relative_name}" ]; then
                #
                # Root
                #
                if [ -z "${_opt_noauto}" ]; then
                    _zfs_canmount="$(_get_local_zfs_properties_for_create "${_current_name}" canmount)"
                else
                    _zfs_canmount="${_opt_noauto}"
                fi
                if [ -n "${_opt_mountpoint}" ]; then
                    _zfs_mountpoint="${_opt_mountpoint}"
                else
                    _zfs_mountpoint=""
                fi

                if [ "${_opt_dry_run}" = "yes" ]; then
                    if [ -z "${_zfs_mountpoint}" ]; then
                        echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} ${_target_ds}${_relative_name}"
                    else
                        echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} -o mountpoint=${_zfs_mountpoint} ${_target_ds}${_relative_name}"
                    fi
                else
                    if [ -z "${_zfs_mountpoint}" ]; then
                        zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} "${_target_ds}${_relative_name}"
                    else
                        zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} -o "mountpoint=${_zfs_mountpoint}" "${_target_ds}${_relative_name}"
                    fi
                fi
            else
                #
                # Children
                #
                if [ -z "${_opt_noauto}" ]; then
                    if [ "${_opt_canmount_parent_only}" = "yes" ]; then
                        _zfs_canmount=""
                    else
                        _zfs_canmount="$(_get_local_zfs_properties_for_create "${_current_name}" canmount)"
                    fi
                else
                    _zfs_canmount="${_opt_noauto}"
                fi
                if [ "${_opt_dry_run}" = "yes" ]; then
                    echo "Would call: zfs create ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} ${_target_ds}${_relative_name}"
                else
                    zfs create -v ${_opt_nomount} ${_zfs_opts} ${_zfs_canmount} "${_target_ds}${_relative_name}"
                fi
            fi
        done
    }
    return 0
}


#:
#: Helper to destroy some created ZFS filesystems
#:
#: Args:
#:   $1 (str): The name of the array that contains the datasets to destroy to
#:
#: Returns:
#:   0 if successfully destroyed all given datasets,
#:   1 otherwise
#:
#: Destruction is done in the reverse order.
#:
_destroy_datasets() {
    farray_reversed_for_each "$1" _destroy_datasets_destroy
}


#:
#: Array callback to destroy a single ZFS filesystem
#:
_destroy_datasets_destroy() {
    if ! zfs destroy -v "$3" ; then
        warn "Dataset \`${3}' cannot unmounted"
        return 1
    fi
    return 0
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case ${_opt} in
        V)
            printf 'fzfs %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
            ;;
        h)
            echo "${USAGE}"
            exit 0
            ;;
        \?)
            exit 2;
            ;;
        *)
            fatal 2 "option handling failed" 1>&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 || fatal 2 "no command given"

command="$1"
shift

case "${command}" in
    mount)
        command_mount "$@"
        ;;
    umount|unmount)
        command_umount "$@"
        ;;
    clone-tree)
        command_clone_tree "$@"
        ;;
    copy-tree)
        command_copy_tree "$@"
        ;;
    create-tree)
        command_create_tree "$@"
        ;;
    *)
        fatal 2 "unknown command \`${command}'" 1>&2
        ;;
esac