Search code examples
evalaliasfish

Creating an alias for `git commit` accepting N arbitrary `-m` arguments


Using the Fish shell, I would like to create a function (gcom) to use as an alias for the git commit command, accepting an arbitrary number of string arguments and passing them as -m options, so that
git commit -m "a" -m "b" -m "c" could be written, in short, gcom "a" "b" "c".

The gcom function should accept N arguments, and run git command with the respective N -m options.

This could be done with a loop over the $argv array, constructing a command manually using string join and executing such command with eval.

However, that looks clunky, and I would love to find a sleeker alternative.

Even better if additional gcom -? options are passed through unmodified!


Solution

  • git commit -mfoo uses "foo" as the message, so you can simply prefix every argument with -m:

    function gcom
        git commit -m$argv
    end
    

    Since $argv is a list, this will add "-m" to every argument in it.

    Unlike other shells, there is no word-splitting here. $argv is not split on whitespace, $argv is the list of all arguments as they have been given. So it works with spaces, newlines, etc. The arguments simply need to be quoted where you give them to gcom:

    gcom "first argument with spaces" "second"\n"argument"\n"with"\n"newlines"
    

    will run the equivalent of

    git commit -m"first argument with spaces" -m "second
    argument
    with
    newlines"
    

    Try it out with printf:

    set -l mylist "first argument with spaces" "second"\n"argument"\n"with"\n"newlines"
    printf '<%s>\n' -m$mylist
    

    will print

    <-mfirst argument with spaces>
    <-msecond
    argument
    with
    newlines>
    

    Even better if additional gcom -? options are passed through unmodified!

    If you wanted to do that, you might leave all arguments starting with - in an additional $args list without adding a -m. This would be broken if any of the options itself takes an argument, like --author.

    Think about:

    gcom --author "mycoolemail@example.com" these are my message words
    

    This should execute like

    git commit --author mycoolemail@example.com -m these -m are -m my -m message -m words
    

    But the only way to figure out that the "mycoolemail@example.com" belongs to --author and therefore shouldn't have a corresponding -m is to know that --author takes options.

    To do that you could use fish's argparse builtin, and you would have to tell it about all of git-commit's options. This would be something like

    function gcom
        # The part before the `--` are descriptions for the options that git-commit takes
        # A "=" means that the option takes a mandatory argument and so it can be written like "--author foo",
        # A "=?" means it takes an optional argument and needs to be written like "-ufoo".
        argparse author= a interactive patch s v 'u=?' -- $argv
    
        # Argparse leaves all the non-option arguments in $argv.
        # For each option it gives you a $_flag_option variable,
        # but that only includes the *value* for the options that take arguments
        #
        # If we wanted to pass all --author= options we could use the `string split -m1` trick from above,
        # but here we assume that only the last --author is of use.
        #
        # This needs to be done for all the options that take arguments,
        # simple boolean flags are just stored as the flags
        # - `$_flag_patch` will contain "--patch"
        set -l flags $_flag_a $_flag_interactive $_flag_patch
        if set -q _flag_author
            set -a flags --author $_flag_author[-1]
        end
        if set -q _flag_u
            set -a flags -u$_flag_u[-1]
        end
    
        # [ now do the -m trick with the leftover $argv ]
        # ...
        git commit $flags -m$argv
    end
    

    Unfortunately, there is no way around listing all the options that take arguments, because otherwise the argument to the option is confused with a normal message argument. There simply isn't anything that fish could do here, that's just how argument parsing works.

    You could run argparse --ignore-unknown, which would tell argparse to leave unknown options in $argv, and then still skip everything starting with a -. This would break if you ever passed an option group (think ls -lah, which has three short-options) and the last option takes an argument but you haven't listed one of the options before.

    Because fish wouldn't be able to figure out that e.g. -sFfile means -s -F=file if you hadn't told it that "-s" exists - because it can't know that -s takes no options. If it did, this would be the same as -s=Ffile. So it can only leave the entire group intact.