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() {

    # Check flags
    while getopts ":am:b" opt; do
        case $opt in
                echo "Unknown flags passed."
                return 1

    shift "$((OPTIND-1))"

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

    # 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"
        echo "Undefined behavior"
        return 1

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.

  • 3
    Related: Pass $@ to command, preserving quotes Mar 24, 2020 at 17:29
  • 1
    You could always put the option parsing in a different function, and do the shift "$(OPTIND - 1))" in the main function. That would also work in shells which don't have arrays. Anyways, set OPTIND=1 before if you're going to call your gc function multiple times.
    – user313992
    Mar 24, 2020 at 22:35

2 Answers 2


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


Then use it as an array:

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

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.
  • 1
    Given the initializations in the question's script, you could probably simplify [[ $commit == true ]] to "$commit".
    – dhag
    Mar 25, 2020 at 13:19

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:

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".

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .