tags:

views:

89

answers:

6

I am using this function in Bash

function parse_git_branch {
  git_status="$(git status 2> /dev/null)"
  pattern="^# On branch ([^${IFS}]*)"
  if [[ ! ${git_status}} =~ "working directory clean" ]]; then
    state="*"
  fi
  # add an else if or two here if you want to get more specific

  if [[ ${git_status} =~ ${pattern} ]]; then
    branch=${BASH_REMATCH[1]}
    echo "(${branch}${state})"
  fi
}

but I'm determined to use zsh. While I can use this perfectly as a shell script (even without a shebang) in my .zshrc the error is a parse error on this line if [[ ! ${git_status}}...

What do I need to do to get it ready for zshell?

Edit: The "actual error" I'm getting is " parse error near } and it refers to the line with the strange double }}, which works on Bash.

Edit: Here's the final code, just for fun:

parse_git_branch() {
    git_status="$(git status 2> /dev/null)"
pattern="^# On branch ([^[:space:]]*)"
    if [[ ! ${git_status} =~ "working directory clean" ]]; then
        state="*"
    fi
    if [[ ${git_status} =~ ${pattern} ]]; then
      branch=${match[1]}
      echo "(${branch}${state})"
    fi
}

setopt PROMPT_SUBST
PROMPT='$PR_GREEN%n@$PR_GREEN%m%u$PR_NO_COLOR:$PR_BLUE%2c$PR_NO_COLOR%(!.#.$)'
RPROMPT='$PR_GREEN$(parse_git_branch)$PR_NO_COLOR'

Thanks to everybody for your patience and help.

Edit: The best answer has schooled us all: git status is porcelain (UI). Good scripting goes against GIT plumbing. Here's the final function:

# The latest version of Chris' function below

PROMPT='$PR_GREEN%n@$PR_GREEN%m%u$PR_NO_COLOR:$PR_BLUE%2c$PR_NO_COLOR%(!.#.$)'
RPROMPT='$PR_GREEN$(parse_git_branch)$PR_NO_COLOR'

Note that only the prompt is zsh-specific. In Bash it would be your prompt plus "\$(parse_git_branch)".

This might be slower (more calls to GIT, but that's an empirical question) but it won't be broken by changes in GIT (they don't change the plumbing). And that is very important for a good script moving forward.

+2  A: 

Get rid of the extra }? ${git_status}} should be ${git_status}.


Once the extra } is removed, the only potential issue that I see is the use of ${BASH_REMATCH[1]}. You can use that in zsh, but it requires enabling the option to do so. As zsh docs on conditional expressions show, you would need to use something like

if [[ ${git_status} =~ ${pattern} ]]; then
  branch=${match[1]}
  echo "(${branch}${state})"
fi
jamessan
tried that already, then it becomes a different error on the same line. Basically this will require someone who knows zsh, methinks. Thanks though
Yar
Then why not update your question to remove that obvious mistake and post the actual error you are getting?
jamessan
it's not a mistake. In the bash script it works perfectly.
Yar
adjusted the question, though, thanks
Yar
That doesn't mean it's not a mistake. That simply means that bash isn't as strict about its parsing.
jamessan
Bash treats `${git_status}}` as `"whatever_git_status_expands_to}"` while Zsh treats `${git_status}}` as a parse error because you have a stray `}`. If you want them to be the same, then put `${git_status}}` in quotes -- `"${git_status}}"`.
jamessan
Did that. Now error is `parse_git_branch:8: failed to compile regex: illegal byte sequence`... thanks for helping out with this one, @jamessan. If we manage to get it working as an in-memory function, that would be cool.
Yar
The changes I suggested in my answer work just fine for me, so I'd suggest updating your question with your current function.
jamessan
I made two changes to the function above based on your edits. 1) I removed the second `}` and 2) I used your code instead of the second `if` clause, but it rejects this `[[ ${git_status} =~ ${pattern} ]]; ` and the error message is ` failed to compile regex: illegal byte sequence`
Yar
Thanks for the help. I'm posting the final code above, thanks to your and @ZyX's help we got this thing figured out.
Yar
Then I'd be curious what you actually have `$IFS` set to. The default value, `\x20\t\n\x00`, works fine for me. Alternatively, you could use `[^[:space:]]` instead of `[^${IFS}]`.
jamessan
Unfortunately, as I found out earlier today for another thing, my IFS value is weird and unprintable (it has a line break in it). Not sure how to get that to you.
Yar
`echo -n $IFS | od -a`. It's very normal for `$IFS` to have a newline.
jamessan
okay, it worked with `pattern="^# On branch ([^[:space:]]*)"`...! and that command reveals `0000000 sp ht nl nul 0000004`
Yar
A: 

May be this zsh Git repository will contain Tests which can give you some clue:

VonC
Thanks VonC as always. I'm kind of looking for an easy way out, if possible, but we'll see how this question goes.
Yar
+1  A: 

If you don't want to learn zsh, I'd suggest you to use another language, like Python, for that kind of parsing.

Check out the git status Python parser and the zsh-git-prompt project on github to see how to get a nice zsh prompt for git.

Olivier
The point is that it has to be an inline function. As I state in the question, as an `sh` script it runs fine. While I could do this again in Ruby, it wouldn't make it faster to load, which is the point.
Yar
While I've seen the zsh-git-prompt-project, it seems that it doesn't parse every time you hit return...?
Yar
Yes, it does parse every time you hit return.
Olivier
Okay, I will check it out, it's got some definite advantages over the present script (above).
Yar
+1  A: 

If you are getting error failed to compile regex: illegal byte sequence then remove NULL from IFS. (Replace ${IFS} with ${IFS//$'\0'/}).

ZyX
An `$IFS` with `\0` works fine on the zsh I'm using -- 4.3.10.
jamessan
@ZyX, though your answer essentially solved the problem for me, jamessan's been working with me on this all day :) THANKS to you both for the help.
Yar
Zsh 4.3.4 also added `[:IFSSPACE:]`.
jamessan
cool, that might be nicer in the code. I'll check that out (in a bit, this is getting distracting from real work). Thanks again for your patience and help.
Yar
+2  A: 

You can use the array match instead of $BASH_REMATCH. You can also escape the extra closing curly brace.

Untested:

function parse_git_branch {
  git_status="$(git status 2> /dev/null)"
  pattern="^# On branch ([^${IFS}]*)"
  if [[ ! ${git_status}\} =~ "working directory clean" ]]; then
    state="*"
  fi
  # add an else if or two here if you want to get more specific

  if [[ ${git_status} =~ ${pattern} ]]; then
    branch=${match[1]}
    echo "(${branch}${state})"
  fi
}

Give that a try and see if it helps.

Dennis Williamson
Thanks Dennis, for now it's all good, match did work as suggested by jamessan in one of his revisions.
Yar
+2  A: 

You should really use Git “plumbing” commands to extract the info you want. The output from the “porcelain” commands (e.g.git status) may change over time, but the behavior of the “plumbing” commands is much more stable.

With the porcelain interfaces, it can also be done without “bashisms” or “zshisms” (i.e. the =~ matching operator):

parse_git_branch() {
    in_wd="$(git rev-parse --is-inside-work-tree 2>/dev/null)" || return
    test "$in_wd" = true || return
    state=''
    git update-index --refresh -q >/dev/null # avoid false positives with diff-index
    if git rev-parse --verify HEAD >/dev/null 2>&1; then
        git diff-index HEAD --quiet 2>/dev/null || state='*'
    else
        state='#'
    fi
    (
        d="$(git rev-parse --show-cdup)" &&
        cd "$d" &&
        test -z "$(git ls-files --others --exclude-standard .)"
    ) >/dev/null 2>&1 || state="${state}+"
    branch="$(git symbolic-ref HEAD 2>/dev/null)"
    test -z "$branch" && branch='<detached-HEAD>'
    echo "${branch#refs/heads/}${state}"
}

Integrating the output into the prompt is still shell specific (i.e. escape or quote the $ (for both bash and zsh) and set PROMPT_SUBST (for zsh)).

Chris Johnsen
it looks good, thanks. What's the best way to have it test for non-git directories (and not echo anything)?
Yar
`if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; then ...; fi`
jamessan
@jamessan, okay remixed that and put it in the question above. Not sure we're going in a good direction here, three git commands vs. one. On the other hand, no reg-ex... perhaps it's all fast enough anyway.
Yar
@yar: If you absolutely can not afford the extra processes, then you may have to go with in-shell parsing of `git status`. The problem is that parsing porcelain output (e.g. `git status`) leaves you vulnerable to behavior/output changes (porcelain commands of newer/older versions of Git might not have the same behavior). The Git developers are very careful to not change the behavior of existing plumbing commands/options. Thus, they constitute the recommended “scripting” interface. See “Low-level commands (plumbing)” in the [git manpage](http://www.kernel.org/pub/software/scm/git/docs/).
Chris Johnsen
This is awesome, and we've all learned a lot from this. Originally I thought that the question shouldn't even have the GIT tag. Thanks for taking this answer in a different direction. Here's my blog post where I thank you more :) http://linkee.com/Ihc
Yar
@Chris Johnsen, I've been using this for a few days and I must say: you are right, use the plumbing, not the porcelain. But then you run into another problem: now you're writing your own porcelain, which is a lot of work. `diff-index HEAD` is NOT always the same as checking status against "working directory clean" (for instance, if there are new files).
Yar
@yar, I have updated the script to indicate untracked files (shows as a “+”).
Chris Johnsen
@Chris Johnsen, thanks for the patch!
Yar
Hi @Chris Johnsen, um, sorry to bother, but: there are certain conditions where `git status` reveals nothing yet the prompt shows an asterisk. If I do a `git add .` and then a `git commit -a` response is `nothing to commit` and then the prompt corrects. Sorry I don't provide the most important piece of info (what the plumbing commands are revealing), but... any ideas?
Yar
@yar, Hard to tell exactly what you are seeing, but it does ring a bell. `git diff-index` does not automatically update the stat(2) information stored in the index, which can cause it to have false positives (e.g. if a timestamp of a file is updated, but its contents remain unchanged with respect to HEAD). Try adding `git update-index --refresh` before `git diff-index` (the answer has been updated to reflect this).
Chris Johnsen
@Chris Johnsen, thanks again, I've plugged in the new version and I'll see how it goes and report back. Seems like it might be getting slower with each new `git` call added.
Yar
New script is problematic (showing me "needs update" in the prompt for certain files)... so instead of writing "against" the porcelain -- git status -- we're now making our own complex porcelain, and debugging it and everything. I don't want to be a wimp here, but parsing `git status` (subject to change) compared to WRITING git status....
Yar
@yar: “needs update” is coming from *git update-index*. I should have suppressed its output. Yes, a program that uses Git plumbing is a kind of new porcelain. If you want the speed and are willing to trade away potential upwards compatibility, you should free free to parse *git status* (and change your accepted answer).If a full speed, cross-Git-version compatible solution to this type of problem is important enough to enough users, someone could conceivably write and shepherd a new C-based plumbing-level command/option into git.git.
Chris Johnsen
Thanks Chris, I'll leave the accepted answer as is. It will serve as an historical lesson (what it teaches I don't know). Thanks again for going so many rounds on this one.
Yar