Steven Lu
Steven Lu

Reputation: 43427

How to use a parenthesis character inside a command substitution inside a bash snippet inside a git configuration alias?

Here is an excerpt from the config I am editing. This is inside the [alias] section of my .gitconfig:

ignored = !git ls-files -v | grep "^S"
status-with-ignored = "!f() { git status; ignored=$(git ignored | cut -d ' ' -f 2 | sed -e s/^/z/ -e s/$/z/); [ -n \"$ignored\" ] && echo \"git skip-worktree (ignored):\n$ignored\"; };f"

I am basically trying to colorize the output of git ignored as I stick it onto the end of the git status. What I show above works fine, but it does not work (fatal: bad config file line 18 in ~/.gitconfig) if my sed command has inside it any parentheses (which are quite useful for building regexes).

For example, I want to be able to write something having parens, such as sed s/^(.*)$/z\1z/ instead of sed -e s/^/z/ -e s/$/z/.

Since i'm colorizing stuff I would actually be using \x1b[31m, \x1b[m, etc., but you get the idea.

I have tried one, two, three, and four backslashes to escape these parens but nothing works.

Upvotes: 2

Views: 2111

Answers (1)

Jeff Bowman
Jeff Bowman

Reputation: 95634

There are separate problems here: One with your config escaping, one with parentheses in sed, and one with process substitution like <(date) in git aliases.

Config escaping

Firstly, the problem with bad config file isn't command substitution, it's your newline character.

[YOURS] status-with-ignored = "!f() { git status; ignored=$(git ignored | cut -d ' ' -f 2 | sed -e s/^/z/ -e s/$/z/); [ -n \"$ignored\" ] && echo \"git skip-worktree (ignored):\n$ignored\"; };f"
 [MINE] status-with-ignored = "!f() { git status; ignored=$(git ignored | cut -d ' ' -f 2 | sed -e s/^/z/ -e s/$/z/); [ -n \"$ignored\" ] && echo \"git skip-worktree (ignored):\\n$ignored\"; };f"

It looks like your \n is being interpreted as applying to the config file; that's the part that needs escaping, not anything paren-related.

I got the above by letting bash and git config handle the escaping for me, and then substituting cut's " " for ' ':

git config alias.status-with-ignored '!f() { git status; ignored=$(git ignored | cut -d " " -f 2 | sed -e s/^/z/ -e s/$/z/); [ -n "$ignored" ] && echo "git skip-worktree (ignored):\n$ignored"; };f'

(If you want to get clever about escaping single quotes within bash single quotes, you can, but to me it works actively against readability.)

Parentheses in sed

Secondly, Bash and other shells can't handle unquoted parentheses. That's not specific to git aliases, that's just the parsing rules.

echo I am (Batman)    # Doesn't work
echo I am Batman      # Does work

The latter one works; Batman has no parens.

You can either escape the parentheses or quote the string:

echo "I am (Batman)"  # Works
echo I am \(Batman\)  # Also works

Which leaves your config file like this:

status-with-ignored = "!f() { git status; ignored=$(git ignored | cut -d ' ' -f 2 | sed -e \"s/^(.*)$/z\1z/\"); [ -n \"$ignored\" ] && echo \"git skip-worktree (ignored):\\n$ignored\"; };f"

Or your git config statement like this:

git config alias.status-with-ignored '!f() { git status; ignored=$(git ignored | cut -d " " -f 2 | sed -e "s/^(.*)$/z\1z/"); [ -n "$ignored" ] && echo "git skip-worktree (ignored):\n$ignored"; };

Process substitution in git

Finally, it seems that git alias just doesn't handle process substitution. From the git config man page:

Arguments are split by spaces, the usual shell quoting and escaping is supported. A quote pair or a backslash can be used to quote them.

It looks like git is using sh or its own parsing, and not calling out to bash itself, so as a non-portable bash extension process substitution isn't supported natively. You could shell out to bash:

git config alias.sample '!cat <(seq 3)'; git sample
git config alias.sample '!bash -c "cat <(seq 3)"'; git sample

...but at that point you might as well just make a script named git-status-with-ignored and add it to your path.

Upvotes: 6

Related Questions