view sbin/fzfs @ 723:a97ec3f07bdb

farray.sh: REFACTOR: More flexible metadata retrieval. Using an array or alist variable name or token value (with prefix) is now supported in every function. This is possible because the value prefixes contain questin marks (?) which are not allowed in shell variable names. This again is a major precondition for recursive data structures (arrays/alists in arrays/alists).
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 05 Oct 2024 21:55:55 +0200
parents 6e7c118e7d47
children 8f1583faf9ea
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_destroy _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_destroy _cloned_datasets
    farray_destroy _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