# HG changeset patch # User Siddharth Agarwal # Date 2014-08-14 06:21:52 # Node ID bc2132dfc0a4eab7c80ff1da9995d1b11f123110 # Parent bd45d92883f966ebf47dee8a9599791814c45f8d alias: expand "$@" as list of parameters quoted individually (BC) (issue4200) Before this patch, there was no way to pass in all the positional parameters as separate words down to another command. (1) $@ (without quotes) would expand to all the parameters separated by a space. This would work fine for arguments without spaces, but arguments with spaces in them would be split up by POSIX shells into separate words. (2) '$@' (in single quotes) would expand to all the parameters within a pair of single quotes. POSIX shells would then treat the entire list of arguments as one word. (3) "$@" (in double quotes) would expand similarly to (2). With this patch, we expand "$@" (in double quotes) as all positional parameters, quoted individually with util.shellquote, and separated by spaces. Under standard field-splitting conditions, POSIX shells will tokenize each argument into exactly one word. This is a backwards-incompatible change, but the old behavior was arguably a bug: Bourne-derived shells have expanded "$@" as a tokenized list of positional parameters for a very long time. I could find this behavior specified in IEEE Std 1003.1-2001, and this probably goes back to much further before that. diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py --- a/mercurial/dispatch.py +++ b/mercurial/dispatch.py @@ -331,6 +331,27 @@ def aliasargs(fn, givenargs): args = shlex.split(cmd) return args + givenargs +def aliasinterpolate(name, args, cmd): + '''interpolate args into cmd for shell aliases + + This also handles $0, $@ and "$@". + ''' + # util.interpolate can't deal with "$@" (with quotes) because it's only + # built to match prefix + patterns. + replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args)) + replacemap['$0'] = name + replacemap['$$'] = '$' + replacemap['$@'] = ' '.join(args) + # Typical Unix shells interpolate "$@" (with quotes) as all the positional + # parameters, separated out into words. Emulate the same behavior here by + # quoting the arguments individually. POSIX shells will then typically + # tokenize each argument into exactly one word. + replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args) + # escape '\$' for regex + regex = '|'.join(replacemap.keys()).replace('$', r'\$') + r = re.compile(regex) + return r.sub(lambda x: replacemap[x.group()], cmd) + class cmdalias(object): def __init__(self, name, definition, cmdtable): self.name = self.cmd = name @@ -376,10 +397,7 @@ class cmdalias(object): % (int(m.groups()[0]), self.name)) return '' cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:]) - replace = dict((str(i + 1), arg) for i, arg in enumerate(args)) - replace['0'] = self.name - replace['@'] = ' '.join(args) - cmd = util.interpolate(r'\$', replace, cmd, escape_prefix=True) + cmd = aliasinterpolate(self.name, args, cmd) return util.system(cmd, environ=env, out=ui.fout) self.fn = fn return diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt --- a/mercurial/help/config.txt +++ b/mercurial/help/config.txt @@ -229,8 +229,9 @@ repository in the same manner as the pur Positional arguments like ``$1``, ``$2``, etc. in the alias definition expand to the command arguments. Unmatched arguments are removed. ``$0`` expands to the alias name and ``$@`` expands to all -arguments separated by a space. These expansions happen before the -command is passed to the shell. +arguments separated by a space. ``"$@"`` (with quotes) expands to all +arguments quoted individually and separated by a space. These expansions +happen before the command is passed to the shell. Shell aliases are executed in an environment where ``$HG`` expands to the path of the Mercurial that was used to execute the alias. This is diff --git a/tests/test-alias.t b/tests/test-alias.t --- a/tests/test-alias.t +++ b/tests/test-alias.t @@ -30,6 +30,7 @@ > echo1 = !printf '\$1\n' > echo2 = !printf '\$2\n' > echo13 = !printf '\$1 \$3\n' + > echotokens = !printf "%s\n" "\$@" > count = !hg log -r "\$@" --template=. | wc -c | sed -e 's/ //g' > mcount = !hg log \$@ --template=. | wc -c | sed -e 's/ //g' > rt = root @@ -241,6 +242,22 @@ simple shell aliases foo baz $ hg echo2 foo + $ hg echotokens + + $ hg echotokens foo 'bar $1 baz' + foo + bar $1 baz + $ hg echotokens 'test $2' foo + test $2 + foo + $ hg echotokens 'test $@' foo '$@' + test $@ + foo + $@ + $ hg echotokens 'test "$@"' foo '"$@"' + test "$@" + foo + "$@" $ echo bar > bar $ hg commit -qA -m bar $ hg count .