Mercurial > hgrepos > FreeBSD > ports > sysutils > local-bsdtools
view sbin/fzfs @ 818:31d5d152126c
===== signature for changeset 7b04d52fbd78
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Sat, 18 Jan 2025 16:38:39 +0100 |
| parents | e2f262ec2bf4 |
| children |
line wrap: on
line source
#!/bin/sh # -*- indent-tabs-mode: nil; -*- #: #: A ZFS management helper tool. #: #: :Author: Franz Glasner #: :Copyright: (c) 2022-2025 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
