0

Hey I'm writing a wrapper script for git that applies one git command to all its submodules e.g.: supergit commit -m "change message" commits to all submodules.
The script essentially does:

function git_foreach () {
    git submodule foreach "git \"$@\" || : "
}

git_foreach "$@"

The problem is when the supergit call contains an argument with spaces (like in the commit message above) the space separated calls are interpreted as multiple arguments. I read in this answer that the way to do it is to use "$@" but that doesn't work within a string.

Is there a way to expand $@ to keep the quotes so that my function works as expected?

EDIT:
What I want is to pass the arguments to git submodule foreach, with supergit commit -m "commit message" I want to run:
git submodule foreach "git commit -m \"commit message\""

iaquobe
  • 555
  • 1
  • 6
  • 23
  • What happens if you double the backslashes, i.e., use `\\"$@\\"`? – jjramsey May 10 '22 at 13:12
  • this doesn't work, it only escapes the backslashes. The call looks like this afterwards: `git \commit -m new pipeline\ ` – iaquobe May 10 '22 at 13:27
  • 1
    Do you know which shell `git submodule foreach` invokes to parse the command line given? (If it's starting sh instead of bash, there are some caveats that need to be handled). – Charles Duffy May 10 '22 at 14:57
  • 1
    ...okay, yes, it _is_ `sh`. That makes things trickier. Can we assume bash 5.0 or newer being used to call it, or do you need compatibility with older shells? – Charles Duffy May 10 '22 at 15:01
  • 1
    BTW, my spelling of `||:` without any whitespace is more idiomatic than the `|| :` shown in git's documentation; it's a deliberate stylistic change. (As another adjustment with more compatibility impact, the `function` keyword is a holdout from 1980s pre-POSIX ksh and should never be used -- see https://wiki.bash-hackers.org/scripting/obsolete) – Charles Duffy May 10 '22 at 15:07
  • @CharlesDuffy thanks about the info on `function` it looked stupid but I saw it in a few places. Won't use it again. And why is `||:` more idiomatic? – iaquobe May 10 '22 at 15:26
  • 1
    Looks more like syntax :) -- it's "more idiomatic" insofar as it's the formulation I see more widely. If you wanted to make it look _less_ like syntax, you could write out `|| true`, but someone using `:` instead of `true` is already aiming for terseness. (BTW, it's a fairly common pattern to give `:` arguments that are otherwise ignored but show up in `set -x` logging; so `||: 'explanation of why we ignore this error'`; that same thing also works for `true`, which ignores its arguments as well, but it's most commonly done for `:`) – Charles Duffy May 10 '22 at 15:29
  • 1
    ...btw, that give-`:`-arguments pattern works in other contexts too. I pretty often write something like `for item in "${array[@]}"; do : "item=$item"` as one line, so when I'm using `set -x` to trace the loop, there's a line showing exactly what `item` is set to every time it's executed. – Charles Duffy May 10 '22 at 15:35

1 Answers1

1

If Your /bin/sh Is Provided By Bash

printf %q will generate a version of your data correctly escaped to be parsed by a shell.

printf '%q ' "$@" generates a string containing an individually shell-escaped word for each argument in your array, with spaces after each word.

Thus:

git_foreach() {
    local cmd_q
    printf -v cmd_q '%q ' "$@"
    git submodule foreach "git $cmd_q ||:"
}

...or, with bash 5.0 or newer (which adds a ${var@Q} expansion), we can make this one line:

git_foreach() { git submodule foreach "git ${@@Q} ||:"; }

With current implementations of printf %q, git_foreach commit -m "commit message" invokes git submodule foreach 'git commit -m commit\ message' or a semantic equivalent; the escaping isn't identical to how you would write it by hand, but the effect of the command is exactly the same.


If You Need Broader Compatibility

Unfortunately, both printf %q and the newer ${variable@Q} expansion are able to generate strings that use bash-only syntax (particularly if your string contains newlines, tabs, or similar). If you don't control which shell git starts to run the foreach commands, then we need to generate a string that's escaped for consumption by any POSIX-compliant shell.

Bash doesn't have a feature for doing that... but Python does!

posix_escape() {
  python3 -c 'import sys, shlex; print(" ".join([shlex.quote(s) for s in sys.argv[1:]]))' "$@"
}

git_foreach() {
  local cmd_q
  cmd_q=$(posix_escape "$@")
  git submodule foreach "git $cmd_q ||:"
}
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441