getopt erroneously caches arguments

2019-08-20 22:35发布

问题:

I've created a script in my bash_aliases to make SSH'ing onto servers easier. However, I'm getting some odd behavior that I don't understand. The below script works as you'd expect, except for when it's re-used.

If I use it like this for this first time in a shell, it works exactly as expected:

$>sdev -s myservername
ssh -i ~/.ssh/id_rsa currentuser@myservername.devdomain.com

However, if I run that a second time, without specifying -s|--server, it will use the server name from the last time I ran this, having seemingly cached it:

$>sdev
ssh -i ~/.ssh/id_rsa currentuser@myservername.devdomain.com

It should have exited with an error and output this message: /bin/bash: A server name (-s|--server) is required.

This happens with any of the arguments; that is, if I specify an argument, and then the next time I don't, this method will use the argument from the last time it was supplied.

Obviously, this is not the behavior I want. What's responsible in my script for doing that, and how do I fix it?

#!/bin/bash

sdev() {
    getopt --test > /dev/null

    if [[ $? -ne 4 ]]; then
        echo "`getopt --test` failed in this environment"
        exit 1
    fi

    OPTIONS=u:,k:,p,s:
    LONGOPTIONS=user:,key:,prod,server:

    # -temporarily store output to be able to check for errors
    # -e.g. use “--options” parameter by name to activate quoting/enhanced mode
    # -pass arguments only via   -- "$@"   to separate them correctly
    PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTIONS --name "$0" -- "$@")

    if [[ $? -ne 0 ]]; then
        # e.g. $? == 1
        #  then getopt has complained about wrong arguments to stdout
        exit 2
    fi

    # read getopt’s output this way to handle the quoting right:
    eval set -- "$PARSED"

    domain=devdomain
    user="$(whoami)"
    key=id_rsa

    # now enjoy the options in order and nicely split until we see --
    while true; do
        case "$1" in
            -u|--user)
                user="$2"
                shift 2
                ;;
            -k|--key)
                key="$2".pem
                shift 2
                ;;
            -p|--prod)
                domain=proddomain
                shift
                ;;
            -s|--server)
                server="$2"
                shift 2
                ;;
            --)
                shift
                break
                ;;
            *)
                echo "Programming error"
                exit 3
                ;;
        esac
    done

    if [ -z "$server" ]; then
        echo "$0: A server name (-s|--server) is required."
        kill -INT $$
    fi

    echo "ssh -i ~/.ssh/$key.pem $user@$server.$domain.com"
    ssh -i ~/.ssh/$key $user@$server.$domain.com
}

回答1:

server is a global shell variable, so it's shared between runs of the function (as long as they're run in the same shell). That is, when you run sdev -s myservername, it sets the variable server to "myservername". Later, when you run just sdev, it checks to see if $server is empty, finds it's not, and goes ahead and uses it.

Solution: use local variables! Actually, it'd be best to declare all of the variables you use in the function as local; that way, you don't run the risk of interfering with something else that's trying to use the same variable name. I'd also recommend avoiding all-caps variable names (like OPTIONS, LONGOPTIONS, and PARSED) -- there are a bunch of all-caps variables that have special meanings to the shell and/or other programs, and if you use one of those by mistake it can cause weird problems.

Anyway, here's the simple solution: add this near the beginning of the script:

local server=""