#!/bin/sh

# Copyright (c) 2023 The PaSh Authors.
#
# Usage of this source code is governed by the MIT license, you can find the
# LICENSE file in the root directory of this project.
#
# https://github.com/binpash/try

TRY_VERSION="0.2.0"
TRY_COMMAND="${0##*/}"
EXECID="$(date +%s%3N)"
export EXECID
export TRY_COMMAND

# exit status invariants
#
# 0 -- command ran
# 1 -- consistency error/failure
# 2 -- input error

################################################################################
# Tries to detect a setting for TRY_SHELL
################################################################################

set_TRY_SHELL() {
  # case: TRY_SHELL set to an executable
  [ -x "$TRY_SHELL" ] && return

  # use SHELL (if it's not fish)
  if [ -x "$SHELL" ] && [ "${SHELL##*/}" != "fish" ]; then
    TRY_SHELL="$SHELL"

  else
    login_shell=$(grep -e "^$LOGNAME" /etc/passwd | cut -d: -f7)

    # use login shell (if it's not fish)
    if [ -x "$login_shell" ] && [ "${login_shell##*/}" != "fish" ]; then
      TRY_SHELL="$login_shell"
    else
      TRY_SHELL="/bin/sh"
    fi
  fi

  export TRY_SHELL
}


################################################################################
# Run a command (in `$@`) in an overlay (in `$SANDBOX_DIR`)
################################################################################

try() {
    set_TRY_SHELL
    START_DIR="$PWD"

    if [ "$SANDBOX_DIR" ]
    then
        ## If the name of a sandbox is given then we need to exit prematurely if its directory doesn't exist
        [ -d "$SANDBOX_DIR" ] || error "could not find sandbox directory $SANDBOX_DIR" 2
        # Force absolute path
        SANDBOX_DIR="$(cd "$SANDBOX_DIR" && pwd)"

        # shellcheck disable=SC2181
        [ "$?" -eq 0 ] || error "could not find sandbox directory $SANDBOX_DIR (could not cd in)" 2
    else
        ## Create a new sandbox if one was not given
        SANDBOX_DIR="$(mktemp -d --suffix ".try-$EXECID")"
    fi

    ## If the sandbox is not valid we exit early
    if ! sandbox_valid_or_empty "$SANDBOX_DIR"
    then
        error "given sandbox '$SANDBOX_DIR' is invalid" 1
    fi

    ## Make any directories that don't already exist, this is OK to do here
    ## because we have already checked if it valid.
    export SANDBOX_DIR

    try_mount_log="$SANDBOX_DIR"/mount.log
    export try_mount_log

    # If we're in a docker container, we want to mount tmpfs on sandbox_dir, #136
    # tail -n +2 to ignore the first line with the column name
    tmpfstype=$(df --output=fstype "$SANDBOX_DIR" | tail -n +2)
    if [ "$tmpfstype" = "overlay" ] && [ "$(id -u)" -eq "0" ]
    then
        echo "mounting sandbox '$SANDBOX_DIR' as tmpfs (underlying fs is overlayfs)" >> "$try_mount_log"
        echo "consider docker volumes if you want persistence" >> "$try_mount_log"
        mount -t tmpfs tmpfs "$SANDBOX_DIR"
    fi

    # Move ignore patterns after any tmpfs mount; mounting SANDBOX_DIR hides prior contents.
    mv "$IGNORE_FILE" "$SANDBOX_DIR"/ignore
    IGNORE_FILE="$SANDBOX_DIR"/ignore

    mkdir -p "$SANDBOX_DIR/upperdir" "$SANDBOX_DIR/workdir" "$SANDBOX_DIR/temproot"

    ## Find all the directories and mounts that need to be mounted
    DIRS_AND_MOUNTS="$SANDBOX_DIR"/mounts
    export DIRS_AND_MOUNTS
    find / -maxdepth 1 >"$DIRS_AND_MOUNTS"
    sort -u -o "$DIRS_AND_MOUNTS" "$DIRS_AND_MOUNTS"

    # Calculate UPDATED_DIRS_AND_MOUNTS that contains the merge arguments in LOWER_DIRS
    UPDATED_DIRS_AND_MOUNTS="$SANDBOX_DIR"/mounts.updated
    export UPDATED_DIRS_AND_MOUNTS
    while IFS="" read -r mountpoint
    do
        new_mountpoint=""
        OLDIFS=$IFS
        IFS=":"

        for lower_dir in $LOWER_DIRS
        do
            temp_mountpoint="$lower_dir/upperdir$mountpoint"
            # Make sure we put : between, but not at the beginning
            new_mountpoint="${new_mountpoint:+$new_mountpoint:}$temp_mountpoint"
        done
        IFS=$OLDIFS
        # Add the original mountpoint at the end
        new_mountpoint="${new_mountpoint:+$new_mountpoint:}$mountpoint"
        echo "$new_mountpoint" >> "$UPDATED_DIRS_AND_MOUNTS"
    done <"$DIRS_AND_MOUNTS"


    # we will overlay-mount each root directory separately (instead of all at once) because some directories cannot be overlayed
    # so we set up the mount points now
    #
    # KK 2023-06-29 This approach (of mounting each root directory separately) was necessary because we could not mount `/` in an overlay.
    #               However, this might be solvable using mergerfs/unionfs, allowing us to mount an overlay on a unionfs of the `/` once.
    while IFS="" read -r mountpoint
    do
        ## Only make the directory if the original is a directory too
        if [ -d "$mountpoint" ] && ! [ -L "$mountpoint" ]
        then
            # shellcheck disable=SC2174 # warning acknowledged, "When used with -p, -m only applies to the deepest directory."
            mkdir -m "$(stat -c %a "$mountpoint")" -p "${SANDBOX_DIR}/upperdir/${mountpoint}" "${SANDBOX_DIR}/workdir/${mountpoint}" "${SANDBOX_DIR}/temproot/${mountpoint}"
        fi
    done <"$DIRS_AND_MOUNTS"

    chmod "$(stat -c %a /)" "$SANDBOX_DIR/temproot"

    mount_and_execute="$SANDBOX_DIR"/mount_and_execute.sh
    chroot_executable="$SANDBOX_DIR"/chroot_executable.sh
    script_to_execute="$SANDBOX_DIR"/script_to_execute.sh

    export chroot_executable
    export script_to_execute

    cat >"$mount_and_execute" <<"EOF"
#!/bin/sh

TRY_COMMAND="$TRY_COMMAND($0)"

## A wrapper of `mount -t overlay` to have cleaner looking code
make_overlay() {
    sandbox_dir="$1"
    lowerdirs="$2"
    overlay_mountpoint="$3"
    mount -t overlay overlay -o userxattr -o "lowerdir=$lowerdirs,upperdir=$sandbox_dir/upperdir/$overlay_mountpoint,workdir=$sandbox_dir/workdir/$overlay_mountpoint" "$sandbox_dir/temproot/$overlay_mountpoint"
}


devices_to_mount="tty null zero full random urandom"

## Mounts and unmounts a few select devices instead of the whole `/dev`
mount_devices() {
    sandbox_dir="$1"
    mkdir -p "$sandbox_dir/temproot/dev" "$sandbox_dir/temproot/dev/pts"
    for dev in $devices_to_mount
    do
        touch "$sandbox_dir/temproot/dev/$dev"
        mount -o bind /dev/$dev "$sandbox_dir/temproot/dev/$dev"
    done
}

unmount_devices() {
    sandbox_dir="$1"
    for dev in $devices_to_mount
    do
        umount "$sandbox_dir/temproot/dev/$dev" 2>>"$try_mount_log"
        rm -f "$sandbox_dir/temproot/dev/$dev"
    done
}

## Try to autodetect union helper: {mergerfs | unionfs}
## Returns an empty string if no union helper is found
autodetect_union_helper() {
    if command -v mergerfs >/dev/null; then
        UNION_HELPER=mergerfs
    elif command -v unionfs >/dev/null; then
        UNION_HELPER=unionfs
    fi
}

# Detect if union_helper is set, if not, we try to autodetect them
if [ -z "$UNION_HELPER" ]
then
    ## Try to detect the union_helper (the variable could still be empty afterwards).
    autodetect_union_helper
fi

# actually mount the overlays
for mountpoint in $(cat "$UPDATED_DIRS_AND_MOUNTS")
do
    pure_mountpoint=${mountpoint##*:}

    ## We are not interested in mounts that are not directories
    if ! [ -d "$pure_mountpoint" ]
    then
        continue
    fi

    ## Symlinks
    if [ -L "$pure_mountpoint" ]
    then
        ln -s $(readlink "$pure_mountpoint") "$SANDBOX_DIR/temproot/$pure_mountpoint"
        continue
    fi

    ## Don't do anything for the root and skip if it is /dev or /proc, we will mount it later
    case "$pure_mountpoint" in
        (/|/dev|/proc) continue;;
    esac

    # Try mounting everything normally
    make_overlay "$SANDBOX_DIR" "$mountpoint" "$pure_mountpoint" 2>>"$try_mount_log"
    # If mounting everything normally fails, we try using either using mergerfs or unionfs to mount them.
    if [ "$?" -ne 0 ]
    then
        ## If the overlay failed, it means that there is a nested mount inside the target mount, e.g., both `/home` and `/home/user/mnt` are mounts.
        ## To address this, we use unionfs/mergerfs (they support the same functionality) to show all mounts under the target mount as normal directories.
        ## Then we can normally make the overlay on the new union directory.
        ##
        ## MMG 2025-01-27
        ##   There used  to be more complicated logic here using `findmnt`, but we currently
        ##   just build unions for every mount in the root.

        if [ -z "$UNION_HELPER" ]
        then
            ## We can ignore this mountpoint, if the user program tries to use it, it will crash, but if not we can run normally
            printf "%s: Warning: Failed mounting $mountpoint as an overlay and mergerfs or unionfs not set and could not be found, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2
        else
            merger_dir="$SANDBOX_DIR"/mergerdir"$(echo "$pure_mountpoint" | tr '/' '.')"
            mkdir "$merger_dir"

            ## Create a union directory
            ## NB $mountpoint is the local directory to mount
            ##    $merger_dir is where we'll put its merger
            "$UNION_HELPER" "$mountpoint" "$merger_dir" 2>>"$try_mount_log" ||
                printf "%s: Warning: Failed mounting $mountpoint via $UNION_HELPER, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2
            make_overlay "$SANDBOX_DIR" "$merger_dir" "$pure_mountpoint" 2>>"$try_mount_log" ||
            printf "%s: Warning: Failed mounting $mountpoint as an overlay via $UNION_HELPER, see \"$try_mount_log\"\n" "$TRY_COMMAND" >&2
        fi
    fi
done

## Mount a few select devices in /dev
mount_devices "$SANDBOX_DIR"

## Check if chroot_executable exists, #29
if ! [ -f "$SANDBOX_DIR/temproot/$chroot_executable" ]
then
    cp $chroot_executable "$SANDBOX_DIR/temproot/$chroot_executable"
fi

unshare --root="$SANDBOX_DIR/temproot" "$TRY_SHELL" "$chroot_executable"
exitcode="$?"

# unmount the devices
rm "$sandbox_dir/temproot/dev/stdin"
rm "$sandbox_dir/temproot/dev/stdout"
rm "$sandbox_dir/temproot/dev/stderr"
rm "$sandbox_dir/temproot/dev/fd"
rm "$sandbox_dir/temproot/dev/ptmx"

unmount_devices "$SANDBOX_DIR"

exit $exitcode
EOF

    # NB we substitute in the heredoc, so the early unsets are okay!
    cat >"$chroot_executable" <<EOF
#!/bin/sh

unset START_DIR SANDBOX_DIR UNION_HELPER DIRS_AND_MOUNTS TRY_EXIT_STATUS TRY_COMMAND TRY_SHELL UPDATED_DIRS_AND_MOUNTS
unset script_to_execute chroot_executable try_mount_log

mount -t devpts devpts /dev/pts -o newinstance,ptmxmode=0666,mode=0620,gid=$(getent group tty | cut -d: -f3 || echo 5) 2>/dev/null &&
[ -e /dev/ptmx ] || ln -s /dev/pts/ptmx /dev/ptmx &&
mount -t proc proc /proc &&
ln -s /proc/self/fd /dev/fd &&
ln -s /proc/self/fd/0 /dev/stdin &&
ln -s /proc/self/fd/1 /dev/stdout &&
ln -s /proc/self/fd/2 /dev/stderr &&
cd "$START_DIR" &&
OLDPWD=$OLDPWD . "$script_to_execute"
EOF

    echo "$@" >"$script_to_execute"

    # `$script_to_execute` need not be +x to be sourced
    chmod +x "$mount_and_execute" "$chroot_executable"

    # enable job control so interactive commands will play nicely with try asking for user input later(for committing). #5
    [ -t 0 ] && set -m

    # --mount: mounting and unmounting filesystems will not affect the rest of the system outside the unshare
    # --map-root-user: map to the superuser UID and GID in the newly created user namespace.
    # --user: the process will have a distinct set of UIDs, GIDs and capabilities.
    # --pid: create a new process namespace (needed fr procfs to work right)
    # --fork: necessary if we do --pid
    #         "Creation of a persistent PID namespace will fail if the --fork option is not also specified."
    # shellcheck disable=SC2086 # we want field splitting!
    unshare --mount --map-root-user --user --pid --fork $EXTRA_NS "$mount_and_execute"
    TRY_EXIT_STATUS=$?

    # remove symlink
    # first set temproot to be writible, rhel derivatives defaults / to r-xr-xr-x
    chmod 755 "${SANDBOX_DIR}/temproot"
    while IFS="" read -r mountpoint
    do
        pure_mountpoint=${mountpoint##*:}
        if [  -L "$pure_mountpoint" ]
        then
            rm "${SANDBOX_DIR}/temproot/${mountpoint}" 2>"$try_mount_log"
        fi
    done <"$DIRS_AND_MOUNTS"

    ################################################################################
    # commit?

    case "$NO_COMMIT" in
        (quiet)       ;;
        (show)        echo "$SANDBOX_DIR";;
        (commit)      commit;;
        (interactive) summary >&2
                      # shellcheck disable=SC2181
                      if [ "$?" -eq 0 ]
                      then
                          printf "\nCommit these changes? [y/N] " >&2
                          read -r DO_COMMIT
                          case "$DO_COMMIT" in
                              (y|Y|yes|YES) commit;;
                              (*)           echo "Not committing." >&2
                                            echo "$SANDBOX_DIR";;
                          esac
                      fi;;
    esac
}

################################################################################
# Summarize the overlay in `$SANDBOX_DIR`
################################################################################

if type try-summary >/dev/null 2>&1
then
    summary() {
        try-summary -i "$IGNORE_FILE" "$SANDBOX_DIR" || return 1
        TRY_EXIT_STATUS=0
    }
else
    summary() {
        if ! [ -d "$SANDBOX_DIR" ]
        then
            error "could not find directory $SANDBOX_DIR" 2
        elif ! [ -d "$SANDBOX_DIR/upperdir" ]
        then
            error "could not find directory $SANDBOX_DIR/upperdir" 1
        fi

        ## Finds all potential changes
        changed_files=$(find_upperdir_changes "$SANDBOX_DIR" "$IGNORE_FILE")
        summary_output=$(process_changes "$SANDBOX_DIR" "$changed_files")
        if [ -z "$summary_output" ]
        then
            return 1
        fi

        echo
        echo "Changes detected in the following files:"
        echo

        echo "$summary_output" | while IFS= read -r summary_line
        do
            local_file="$(echo "$summary_line" | cut -c 4-)"
            case "$summary_line" in
                (ln*) echo "$local_file (symlink)";;
                (rd*) echo "$local_file (replaced with dir)";;
                (md*) echo "$local_file (created dir)";;
                (de*) echo "$local_file (deleted)";;
                (mo*) echo "$local_file (modified)";;
                (ad*) echo "$local_file (added)";;
            esac
        done

        TRY_EXIT_STATUS=0
    }
fi

################################################################################
# Commit the results of an overlay in `$SANDBOX_DIR`
################################################################################

if type try-commit >/dev/null 2>&1
then
    commit() {
        try-commit -i "$IGNORE_FILE" "$SANDBOX_DIR"
        TRY_EXIT_STATUS=$?
    }
else
    commit() {
        if ! [ -d "$SANDBOX_DIR" ]
        then
            error "could not find directory $SANDBOX_DIR" "$TRY_COMMAND" 2
        elif ! [ -d "$SANDBOX_DIR/upperdir" ]
        then
            error "could not find directory $SANDBOX_DIR/upperdir" 1
        fi

        changed_files=$(find_upperdir_changes "$SANDBOX_DIR" "$IGNORE_FILE")
        summary_output=$(process_changes "$SANDBOX_DIR" "$changed_files")

        TRY_EXIT_STATUS=0
        echo "$summary_output" | while IFS= read -r summary_line; do
            local_file="$(echo "$summary_line" | cut -c 4-)"
            changed_file="$SANDBOX_DIR/upperdir$local_file"
            case $summary_line in
                (ln*) rm -rf "$local_file"; ln -s "$(readlink "$changed_file")" "$local_file";;
                (rd*) rm -rf "$local_file"; mkdir "$local_file";;
                (md*) mkdir "$local_file";;
                (de*) rm -rf "$local_file";;
                (mo*) rm -rf "$local_file"; mv "$changed_file" "$local_file";;
                (ad*) mv "$changed_file" "$local_file";;
            esac

            # shellcheck disable=SC2181
            if [ "$?" -ne 0 ]
            then
                warn "couldn't commit $changed_file"
                TRY_EXIT_STATUS=1
            fi
        done
    }
fi

################################################################################
## Defines which changes we want to ignore in the summary and commit
################################################################################

ignore_changes() {
    ignore_file="$1"

    grep -v -f "$ignore_file"
}

################################################################################
## Lists all upperdir changes in raw format
################################################################################

find_upperdir_changes() {
    sandbox_dir="$1"
    ignore_file="$2"

    find "$sandbox_dir/upperdir/" -type f -o \( -type c -size 0 \) -o -type d -o -type l | ignore_changes "$ignore_file"
}

################################################################################
# Processes upperdir changes to an internal format that can be processed by summary and commit
#
# Output format:
#
#   XX PATH
#
#   where:
#     XX is a two character code for the modification
#     - rd: Replaced with a directory
#     - md: Created a directory
#     - de: Deleted a file
#     - mo: Modified a file
#     - ad: Added a file
#
#     PATH is the local/host path (i.e., without the upper
################################################################################

process_changes() {
    sandbox_dir="$1"
    changed_files="$2"

    while IFS= read -r changed_file
    do
        local_file="${changed_file#"$sandbox_dir/upperdir"}"
        if [ -L "$changed_file" ]
        then
            # // TRYCASE(symlink, *)
            echo "ln $local_file"
        elif [ -d "$changed_file" ]
        then
            if ! [ -e "$local_file" ]
            then
                # // TRYCASE(dir, nonexist)
                echo "md $local_file"
                continue
            fi

            if [ "$(getfattr --absolute-names --only-values --e text -n user.overlay.opaque "$changed_file" 2>/dev/null)" = "y" ]
            then
                # // TRYCASE(opaque, *)
                # // TRYCASE(dir, dir)
                echo "rd $local_file"
                continue
            fi

            if ! [ -d "$local_file" ]
            then
                # // TRYCASE(dir, file)
                # // TRYCASE(dir, symlink)
                echo "rd $local_file"
                continue
            fi

            # must be a directory, but not opaque---leave it!
        elif [ -c "$changed_file" ] && ! [ -s "$changed_file" ] && [ "$(stat -c %t,%T "$changed_file")" = "0,0" ]
        then
            # // TRYCASE(whiteout, *)
            echo "de $local_file"
        elif [ -f "$changed_file" ]
        then
            if [ -f "$changed_file" ] && getfattr --absolute-names -d "$changed_file" 2>/dev/null | grep -q -e "user.overlay.whiteout"
            then
                # // TRYCASE(whiteout, *)
                echo "de $local_file"
                continue
            fi

            if [ -e "$local_file" ]
            then
                # // TRYCASE(file, file)
                # // TRYCASE(file, dir)
                # // TRYCASE(file, symlink)
                echo "mo $local_file"
            else
                # // TRYCASE(file, nonexist)
                echo "ad $local_file"
            fi
        fi
    done <<EOF
$changed_files
EOF
}

################################################################################
# Returns 0 if a sandbox is empty (fresh for use) or pre-existing and well formed
################################################################################

sandbox_valid_or_empty() {
    sandbox_dir="$1"

    if ! [ -d "$sandbox_dir/upperdir" ] && ! [ -d "$sandbox_dir/workdir" ] && ! [ -d "$sandbox_dir/temproot" ]
    then
        # No sandbox directory exists so we can happily return
        return 0
    fi

    # The sandbox already exists so we now need to check if it is valid
    # Validity requirements:
    # - no file exists in the temproot tree, i.e., all directories are empty
    #
    # TODO: Make this validity assertion tighter
    # KK 2023-06-28 workdir seems to be non-empty after a single use, is that expected?
    if [ "$(find "$sandbox_dir/temproot" -depth -not -type d)" ]
    then
        return 1
    fi

    return 0
}

################################################################################
# Emit a warning
################################################################################

warn() {
    msg="$1"

    printf "%s: %s\n" "$TRY_COMMAND" "$msg" >&2
}

################################################################################
# Emit a warning and exit
################################################################################

error() {
    msg="$1"
    exit_status="$2"

    warn "$msg"
    exit "$exit_status"
}

################################################################################
# Argument parsing
################################################################################

usage() {
    cat >&2 <<EOF
Usage: $TRY_COMMAND [-nvhyx] [-i PATTERN] [-D DIR] [-U PATH] [-L dir1:dir2:...] CMD [ARG ...]

  -n                don't commit or prompt for commit (overrides -y)
  -y                assume yes to all prompts (overrides -n)
  -x                prevent network access (by unsharing the network namespace)
  -i PATTERN        ignore paths that match PATTERN on summary and commit
  -D DIR            work in DIR (implies -n)
  -U PATH           path to unionfs helper (e.g., mergerfs, unionfs-fuse)
  -L dir1:dir2:...  specify multiple lower directories to merge (colon-separated, implies -n)
  -v                show version information (and exit)
  -h                show this usage message (and exit)

Subcommands:
  $TRY_COMMAND summary DIR   show the summary for the overlay in DIR
  $TRY_COMMAND commit DIR    commit the overlay in DIR
  $TRY_COMMAND explore DIR   start a shell inside the overlay in DIR
EOF
}

################################################################################
# Main entry point
################################################################################

# "interactive" - show nothing, interactively prompt on commit
# "show"        - show the resulting directory on stdout when we're done
# "quiet"       - do not show the result directory on stdout when we're done
# "commit"      - commit the result directory automatically when we're done
NO_COMMIT="interactive"

# Includes all patterns given using the `-i` flag; will be used with `grep -f`
#
# We have to create this temporary up front.
# We move it to $SANDBOX_DIR/ignore in `try()`, but delete it when we don't move it.
IGNORE_FILE="$(mktemp --suffix ".try-$EXECID")"

while getopts ":yvnhxi:D:U:L:" opt
do
    case "$opt" in
        (y)   NO_COMMIT="commit";;
        (n)   NO_COMMIT="show";;
        (i)   echo "$OPTARG" >>"$IGNORE_FILE";;
        (D)   if ! [ -d "$OPTARG" ]
              then
                  error "could not find sandbox directory '$OPTARG'" 2
              fi
              SANDBOX_DIR="$OPTARG"
              NO_COMMIT="quiet";;
        (L)   if [ -n "$LOWER_DIRS" ]
              then
                  error "the -L option has been specified multiple times" 2
              fi
              LOWER_DIRS="$OPTARG"
              NO_COMMIT="quiet";;
        (v)   echo "$TRY_COMMAND version $TRY_VERSION" >&2
              exit 0;;
        (U)   if ! [ -x "$OPTARG" ]
              then
                  error "could not find executable union helper '$OPTARG'" 2
              fi
              UNION_HELPER="$OPTARG"
              export UNION_HELPER;;
        (x)   EXTRA_NS="--net";;
        (h|*) usage
              exit 0;;
    esac
done

shift $((OPTIND - 1))

if [ "$#" -eq 0 ]
then
    usage
    exit 2
fi

TRY_EXIT_STATUS=1
case "$1" in
    (summary) : "${SANDBOX_DIR=$2}"
              summary
              rm "$IGNORE_FILE" # we didn't move it to the sandbox, so clean up
              ;;
    (commit)  : "${SANDBOX_DIR=$2}"
              commit
              rm "$IGNORE_FILE" # we didn't move it to the sandbox, so clean up
              ;;
    (explore) : "${SANDBOX_DIR=$2}"
              set_TRY_SHELL
              try "$TRY_SHELL";;
    (--)      shift
              try "$@";;
    (*)       try "$@";;
esac

exit "$TRY_EXIT_STATUS"
