3

I'm making an intelligent alias gc that can differentiate git commit/checkout.

If gc is called without any arguments or with the -a, -m arguments, then git commit is run. Otherwise, git checkout is run (if there is a b flag with an additional argument). If any other variation of gc is called, I'd prefer it throws an error rather than doing something unexpected.

Here is my shell function so far.

gc() {
    args=$@
    commit=false
    checkout=false

    # Check flags
    while getopts ":am:b" opt; do
        case $opt in
            a|m)
                commit=true
                ;;
            b)
                checkout=true
                ;;
            \?)
                echo "Unknown flags passed."
                return 1
                ;;
        esac
    done

    shift "$((OPTIND-1))"

    # Check number of arguments
    if [[ "$#" == 0 ]]; then
        commit=true
    elif [[ "$#" == 1 ]]; then
        checkout=true
    else
        echo "Too many arguments"
        return 1
    fi

    # Finally run respective command
    if [[ $commit == true && $checkout == true ]]; then
        echo "Unable to ascertain which operation you would like to perform."
        return 1
    elif [[ $commit == true ]]; then
        git commit "$args"
    elif [[ $checkout == true ]]; then
        git checkout "$args"
    else
        echo "Undefined behavior"
        return 1
    fi
}

However, this does not work properly. After a bit of experimenting, I found that the assigning $@ to another variable was the root cause but I was unable to understand why and what exactly was going wrong.

Also, as I'm writing shell functions for the first time, highlight any mistakes I've made.

2 Answers2

8

$@ is an array, assign it to an array:

args=("$@")

Then use it as an array:

elif [[ $commit == true ]]; then
    git commit "${args[@]}"
elif [[ $checkout == true ]]; then
    git checkout "${args[@]}"
else

What's happening in your current code is that all of your separate arguments are being stored as a single string. So if you call:

bc -a "foo bar"

That gets assigned to args as:

args='-a foo bar'

Then instead of executing:

git commit -a "foo bar"

You get:

git commit '-a foo bar'

Some other notes:

  • in zsh, you can also use "$args[@]" or "${(@)args}" in place of "${args[@]}", not in bash/ksh. "$args" would work in yash, but in zsh, that's the same as "$args[*]" (elements joined with the first character of $IFS) and in ksh/bash the same as "${args[0]}". In zsh, $args works but removes empty elements if any.
  • when using getopts in a function, you should add some local OPTIND OPTARG (especially OPTIND or at least initialise/unset it) beforehand. zsh does it automatically for OPTIND when not emulating other shells, but ksh/bash/yash don't. The other variables you use in the function should also probably be set local.
  • errors should got to stderr (fd 2): echo>&2 Too many arguments and so on.
jesse_b
  • 37,005
  • 1
    Given the initializations in the question's script, you could probably simplify [[ $commit == true ]] to "$commit". – dhag Mar 25 '20 at 13:19
2

As the other answer is not POSIX, here is an alternative. If you want to crush the position arguments temporarily, you can do something like this:

s1=$(printf '%s\n' "$@")

then when you are ready to restore, do this:

IFS='
'
set -- $s1

Note this assumes the arguments do not contain newlines. If they do, a different delimiter will need to be used.

Side note: If you notice, the last line contains an unquoted variable. This is one of the few cases (and in my opinion, the only case) where it is acceptable to have unquoted variables: where the user has explicitly set IFS. User is essentially says "yes, I know what Im doing, please proceed".

Zombo
  • 1
  • 5
  • 44
  • 63