2

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.

Steven Lu
  • 41,389
  • 58
  • 210
  • 364
  • The example is bad since you can write: `sed s/.*/z\0z/` – hek2mgl Aug 05 '15 at 08:28
  • BTW i know I can work around this in a large number of other ways (`echo` comes to mind) but the real question is, does there exist a way to make it let me use parens – Steven Lu Aug 05 '15 at 08:28
  • @hek2mgl Thanks for reminding me that `\0` works to fetch the full match but that is just another of the other workarounds – Steven Lu Aug 05 '15 at 08:29
  • Probably I'm missing something, but I don't see the problem. Let's say you have `sed 's/foo\(.*\)/\1/' <<< 'foobar'` , the you can simply use `echo "$(sed 's/foo\(.*\)/\1/' <<< 'foobar')"` – hek2mgl Aug 05 '15 at 08:32
  • For example now I want to use the `<(file)` redirector but git is complaining there is a parse error in the config now. – Steven Lu Aug 05 '15 at 08:33
  • @hek2mgl that's exactly it, no amount of backslash escaping of those parens will get git to stop complaining about not being able to parse. – Steven Lu Aug 05 '15 at 08:33
  • This works too: `echo $(sed 's/foo\(.*\)/\1/' <(echo 'foobar'))`. I guess the problem is that you need to double escape some characters since the string will be parsed twice: Once by git when reading it from config file and secondly by bash when executing it as a command. Meaning you need \\ instead of \ . But I'm not 100% sure what you are doing, where do you want to place that commands? .bashrc? .git/config? – hek2mgl Aug 05 '15 at 08:35
  • `.gitconfig` (similar to `.git/config`) – Steven Lu Aug 05 '15 at 08:45
  • I see the issue (have tried it). Let me check that and come back to you later if it has not answered before. – hek2mgl Aug 05 '15 at 09:17
  • For my actual application I was able to finagle something together using pipes. I had wanted to use shell input redirection `<()` but that has parens, so I opted for pipes instead – Steven Lu Aug 05 '15 at 19:31
  • The same for me.. I didn't had so much time today but using pipes was also the only thing that worked for me. I also couldn't make something like this `<(echo foo)` working.. I can't say why atm. Good question! ;) – hek2mgl Aug 05 '15 at 19:33

1 Answers1

6

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.

Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • this is very confusing because i am 100% sure the newline has nothing to do with the problem. In fact I try a lot of variations of the version you suggest with the single quotes on the outside and I always get back `fatal: Bad alias.status-with-ignored string: unclosed quote` – Steven Lu Aug 06 '15 at 16:40
  • Oh I see now u are saying to call `git config alias` to do the escapement for me.. Ok but the newline is not the issue because I can take it out and the same behavior happens – Steven Lu Aug 06 '15 at 16:42
  • @Steven: Oh, I see what's going on: If you want parentheses in a sed argument or anywhere else, you need to quote the argument, which is just an artifact of Bash tokenization. I'm updating my answer now. – Jeff Bowman Aug 06 '15 at 19:51
  • thanks, you went over many methods to deal with the various issues and its now totally clear how to bring full bash capability to git aliases. Sweet. – Steven Lu Aug 06 '15 at 21:00