Skip to content

Commit d19863b

Browse files
committed
Merge branch 'ah/git-prompt-portability'
The command line prompt support used to be littered with bash-isms, which has been corrected to work with more shells. * ah/git-prompt-portability: git-prompt: support custom 0-width PS1 markers git-prompt: ta-da! document usage in other shells git-prompt: don't use shell $'...' git-prompt: add some missing quotes git-prompt: replace [[...]] with standard code git-prompt: don't use shell arrays git-prompt: fix uninitialized variable git-prompt: use here-doc instead of here-string
2 parents a9bc27f + fbcdfab commit d19863b

File tree

1 file changed

+126
-65
lines changed

1 file changed

+126
-65
lines changed

contrib/completion/git-prompt.sh

Lines changed: 126 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
# To enable:
99
#
1010
# 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh).
11-
# 2) Add the following line to your .bashrc/.zshrc:
12-
# source ~/.git-prompt.sh
11+
# 2) Add the following line to your .bashrc/.zshrc/.profile:
12+
# . ~/.git-prompt.sh # dot path/to/this-file
1313
# 3a) Change your PS1 to call __git_ps1 as
1414
# command-substitution:
1515
# Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
@@ -30,6 +30,8 @@
3030
# Optionally, you can supply a third argument with a printf
3131
# format string to finetune the output of the branch status
3232
#
33+
# See notes below about compatibility with other shells.
34+
#
3335
# The repository status will be displayed only if you are currently in a
3436
# git repository. The %s token is the placeholder for the shown status.
3537
#
@@ -106,38 +108,78 @@
106108
# directory is set up to be ignored by git, then set
107109
# GIT_PS1_HIDE_IF_PWD_IGNORED to a nonempty value. Override this on the
108110
# repository level by setting bash.hideIfPwdIgnored to "false".
111+
#
112+
# Compatibility with other shells (beyond bash/zsh):
113+
#
114+
# We require posix-ish shell plus "local" support, which is most
115+
# shells (even pdksh), but excluding ksh93 (because no "local").
116+
#
117+
# Prompt integration might differ between shells, but the gist is
118+
# to load it once on shell init with '. path/to/git-prompt.sh',
119+
# set GIT_PS1* vars once as needed, and either place $(__git_ps1..)
120+
# inside PS1 once (0/1 args), or, before each prompt is displayed,
121+
# call __git_ps1 (2/3 args) which sets PS1 with the status embedded.
122+
#
123+
# Many shells support the 1st method of command substitution,
124+
# though some might need to first enable cmd substitution in PS1.
125+
#
126+
# When using colors, each escape sequence is wrapped between byte
127+
# values 1 and 2 (control chars SOH, STX, respectively), which are
128+
# invisible at the output, but for bash/readline they mark 0-width
129+
# strings (SGR color sequences) when calculating the on-screen
130+
# prompt width, to maintain correct input editing at the prompt.
131+
#
132+
# To replace or disable the 0-width markers, set GIT_PS1_COLOR_PRE
133+
# and GIT_PS1_COLOR_POST to other markers, or empty (nul) to not
134+
# use markers. For instance, some shells support '\[' and '\]' as
135+
# start/end markers in PS1 - when invoking __git_ps1 with 3/4 args,
136+
# but it may or may not work in command substitution mode. YMMV.
137+
#
138+
# If the shell doesn't support 0-width markers and editing behaves
139+
# incorrectly when using colors in __git_ps1, then, other than
140+
# disabling color, it might be solved using multi-line prompt,
141+
# where the git status is not at the last line, e.g.:
142+
# PS1='\n\w \u@\h$(__git_ps1 " (%s)")\n\$ '
109143

110144
# check whether printf supports -v
111145
__git_printf_supports_v=
112146
printf -v __git_printf_supports_v -- '%s' yes >/dev/null 2>&1
113147

148+
# like __git_SOH=$'\001' etc but works also in shells without $'...'
149+
eval "$(printf '
150+
__git_SOH="\001" __git_STX="\002" __git_ESC="\033"
151+
__git_LF="\n" __git_CRLF="\r\n"
152+
')"
153+
114154
# stores the divergence from upstream in $p
115155
# used by GIT_PS1_SHOWUPSTREAM
116156
__git_ps1_show_upstream ()
117157
{
118158
local key value
119-
local svn_remote svn_url_pattern count n
159+
local svn_remotes="" svn_url_pattern="" count n
120160
local upstream_type=git legacy="" verbose="" name=""
161+
local LF="$__git_LF"
121162

122-
svn_remote=()
123163
# get some config options from git-config
124164
local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
125165
while read -r key value; do
126166
case "$key" in
127167
bash.showupstream)
128168
GIT_PS1_SHOWUPSTREAM="$value"
129-
if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
169+
if [ -z "${GIT_PS1_SHOWUPSTREAM}" ]; then
130170
p=""
131171
return
132172
fi
133173
;;
134174
svn-remote.*.url)
135-
svn_remote[$((${#svn_remote[@]} + 1))]="$value"
175+
svn_remotes=${svn_remotes}${value}${LF} # URI\nURI\n...
136176
svn_url_pattern="$svn_url_pattern\\|$value"
137177
upstream_type=svn+git # default upstream type is SVN if available, else git
138178
;;
139179
esac
140-
done <<< "$output"
180+
done <<-OUTPUT
181+
$output
182+
OUTPUT
141183

142184
# parse configuration values
143185
local option
@@ -154,33 +196,45 @@ __git_ps1_show_upstream ()
154196
case "$upstream_type" in
155197
git) upstream_type="@{upstream}" ;;
156198
svn*)
157-
# get the upstream from the "git-svn-id: ..." in a commit message
158-
# (git-svn uses essentially the same procedure internally)
159-
local -a svn_upstream
160-
svn_upstream=($(git log --first-parent -1 \
161-
--grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
162-
if [[ 0 -ne ${#svn_upstream[@]} ]]; then
163-
svn_upstream=${svn_upstream[${#svn_upstream[@]} - 2]}
164-
svn_upstream=${svn_upstream%@*}
165-
local n_stop="${#svn_remote[@]}"
166-
for ((n=1; n <= n_stop; n++)); do
167-
svn_upstream=${svn_upstream#${svn_remote[$n]}}
168-
done
199+
# successful svn-upstream resolution:
200+
# - get the list of configured svn-remotes ($svn_remotes set above)
201+
# - get the last commit which seems from one of our svn-remotes
202+
# - confirm that it is from one of the svn-remotes
203+
# - use $GIT_SVN_ID if set, else "git-svn"
169204

170-
if [[ -z "$svn_upstream" ]]; then
205+
# get upstream from "git-svn-id: UPSTRM@N HASH" in a commit message
206+
# (git-svn uses essentially the same procedure internally)
207+
local svn_upstream="$(
208+
git log --first-parent -1 \
209+
--grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null
210+
)"
211+
212+
if [ -n "$svn_upstream" ]; then
213+
# extract the URI, assuming --grep matched the last line
214+
svn_upstream=${svn_upstream##*$LF} # last line
215+
svn_upstream=${svn_upstream#*: } # UPSTRM@N HASH
216+
svn_upstream=${svn_upstream%@*} # UPSTRM
217+
218+
case ${LF}${svn_remotes} in
219+
*"${LF}${svn_upstream}${LF}"*)
220+
# grep indeed matched the last line - it's our remote
171221
# default branch name for checkouts with no layout:
172222
upstream_type=${GIT_SVN_ID:-git-svn}
173-
else
223+
;;
224+
*)
225+
# the commit message includes one of our remotes, but
226+
# it's not at the last line. is $svn_upstream junk?
174227
upstream_type=${svn_upstream#/}
175-
fi
176-
elif [[ "svn+git" = "$upstream_type" ]]; then
228+
;;
229+
esac
230+
elif [ "svn+git" = "$upstream_type" ]; then
177231
upstream_type="@{upstream}"
178232
fi
179233
;;
180234
esac
181235

182236
# Find how many commits we are ahead/behind our upstream
183-
if [[ -z "$legacy" ]]; then
237+
if [ -z "$legacy" ]; then
184238
count="$(git rev-list --count --left-right \
185239
"$upstream_type"...HEAD 2>/dev/null)"
186240
else
@@ -192,8 +246,8 @@ __git_ps1_show_upstream ()
192246
for commit in $commits
193247
do
194248
case "$commit" in
195-
"<"*) ((behind++)) ;;
196-
*) ((ahead++)) ;;
249+
"<"*) behind=$((behind+1)) ;;
250+
*) ahead=$((ahead+1)) ;;
197251
esac
198252
done
199253
count="$behind $ahead"
@@ -203,7 +257,7 @@ __git_ps1_show_upstream ()
203257
fi
204258

205259
# calculate the result
206-
if [[ -z "$verbose" ]]; then
260+
if [ -z "$verbose" ]; then
207261
case "$count" in
208262
"") # no upstream
209263
p="" ;;
@@ -229,10 +283,10 @@ __git_ps1_show_upstream ()
229283
*) # diverged from upstream
230284
upstream="|u+${count#* }-${count% *}" ;;
231285
esac
232-
if [[ -n "$count" && -n "$name" ]]; then
286+
if [ -n "$count" ] && [ -n "$name" ]; then
233287
__git_ps1_upstream_name=$(git rev-parse \
234288
--abbrev-ref "$upstream_type" 2>/dev/null)
235-
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
289+
if [ "$pcmode" = yes ] && [ "$ps1_expanded" = yes ]; then
236290
upstream="$upstream \${__git_ps1_upstream_name}"
237291
else
238292
upstream="$upstream ${__git_ps1_upstream_name}"
@@ -251,25 +305,29 @@ __git_ps1_show_upstream ()
251305
# their own color.
252306
__git_ps1_colorize_gitstring ()
253307
{
254-
if [[ -n ${ZSH_VERSION-} ]]; then
308+
if [ -n "${ZSH_VERSION-}" ]; then
255309
local c_red='%F{red}'
256310
local c_green='%F{green}'
257311
local c_lblue='%F{blue}'
258312
local c_clear='%f'
259313
else
260-
# Using \001 and \002 around colors is necessary to prevent
261-
# issues with command line editing/browsing/completion!
262-
local c_red=$'\001\e[31m\002'
263-
local c_green=$'\001\e[32m\002'
264-
local c_lblue=$'\001\e[1;34m\002'
265-
local c_clear=$'\001\e[0m\002'
314+
# \001 (SOH) and \002 (STX) are 0-width substring markers
315+
# which bash/readline identify while calculating the prompt
316+
# on-screen width - to exclude 0-screen-width esc sequences.
317+
local c_pre="${GIT_PS1_COLOR_PRE-$__git_SOH}${__git_ESC}["
318+
local c_post="m${GIT_PS1_COLOR_POST-$__git_STX}"
319+
320+
local c_red="${c_pre}31${c_post}"
321+
local c_green="${c_pre}32${c_post}"
322+
local c_lblue="${c_pre}1;34${c_post}"
323+
local c_clear="${c_pre}0${c_post}"
266324
fi
267-
local bad_color=$c_red
268-
local ok_color=$c_green
325+
local bad_color="$c_red"
326+
local ok_color="$c_green"
269327
local flags_color="$c_lblue"
270328

271329
local branch_color=""
272-
if [ $detached = no ]; then
330+
if [ "$detached" = no ]; then
273331
branch_color="$ok_color"
274332
else
275333
branch_color="$bad_color"
@@ -298,7 +356,7 @@ __git_ps1_colorize_gitstring ()
298356
# variable, in that order.
299357
__git_eread ()
300358
{
301-
test -r "$1" && IFS=$'\r\n' read -r "$2" <"$1"
359+
test -r "$1" && IFS=$__git_CRLF read -r "$2" <"$1"
302360
}
303361

304362
# see if a cherry-pick or revert is in progress, if the user has committed a
@@ -346,7 +404,7 @@ __git_sequencer_status ()
346404
__git_ps1 ()
347405
{
348406
# preserve exit status
349-
local exit=$?
407+
local exit="$?"
350408
local pcmode=no
351409
local detached=no
352410
local ps1pc_start='\u@\h:\w '
@@ -365,7 +423,7 @@ __git_ps1 ()
365423
;;
366424
0|1) printf_format="${1:-$printf_format}"
367425
;;
368-
*) return $exit
426+
*) return "$exit"
369427
;;
370428
esac
371429

@@ -403,7 +461,7 @@ __git_ps1 ()
403461
# incorrect.)
404462
#
405463
local ps1_expanded=yes
406-
[ -z "${ZSH_VERSION-}" ] || [[ -o PROMPT_SUBST ]] || ps1_expanded=no
464+
[ -z "${ZSH_VERSION-}" ] || eval '[[ -o PROMPT_SUBST ]]' || ps1_expanded=no
407465
[ -z "${BASH_VERSION-}" ] || shopt -q promptvars || ps1_expanded=no
408466

409467
local repo_info rev_parse_exit_code
@@ -413,29 +471,30 @@ __git_ps1 ()
413471
rev_parse_exit_code="$?"
414472

415473
if [ -z "$repo_info" ]; then
416-
return $exit
474+
return "$exit"
417475
fi
418476

477+
local LF="$__git_LF"
419478
local short_sha=""
420479
if [ "$rev_parse_exit_code" = "0" ]; then
421-
short_sha="${repo_info##*$'\n'}"
422-
repo_info="${repo_info%$'\n'*}"
480+
short_sha="${repo_info##*$LF}"
481+
repo_info="${repo_info%$LF*}"
423482
fi
424-
local ref_format="${repo_info##*$'\n'}"
425-
repo_info="${repo_info%$'\n'*}"
426-
local inside_worktree="${repo_info##*$'\n'}"
427-
repo_info="${repo_info%$'\n'*}"
428-
local bare_repo="${repo_info##*$'\n'}"
429-
repo_info="${repo_info%$'\n'*}"
430-
local inside_gitdir="${repo_info##*$'\n'}"
431-
local g="${repo_info%$'\n'*}"
483+
local ref_format="${repo_info##*$LF}"
484+
repo_info="${repo_info%$LF*}"
485+
local inside_worktree="${repo_info##*$LF}"
486+
repo_info="${repo_info%$LF*}"
487+
local bare_repo="${repo_info##*$LF}"
488+
repo_info="${repo_info%$LF*}"
489+
local inside_gitdir="${repo_info##*$LF}"
490+
local g="${repo_info%$LF*}"
432491

433492
if [ "true" = "$inside_worktree" ] &&
434493
[ -n "${GIT_PS1_HIDE_IF_PWD_IGNORED-}" ] &&
435494
[ "$(git config --bool bash.hideIfPwdIgnored)" != "false" ] &&
436495
git check-ignore -q .
437496
then
438-
return $exit
497+
return "$exit"
439498
fi
440499

441500
local sparse=""
@@ -485,14 +544,16 @@ __git_ps1 ()
485544
case "$ref_format" in
486545
files)
487546
if ! __git_eread "$g/HEAD" head; then
488-
return $exit
547+
return "$exit"
489548
fi
490549

491-
if [[ $head == "ref: "* ]]; then
550+
case $head in
551+
"ref: "*)
492552
head="${head#ref: }"
493-
else
553+
;;
554+
*)
494555
head=""
495-
fi
556+
esac
496557
;;
497558
*)
498559
head="$(git symbolic-ref HEAD 2>/dev/null)"
@@ -528,8 +589,8 @@ __git_ps1 ()
528589
fi
529590

530591
local conflict="" # state indicator for unresolved conflicts
531-
if [[ "${GIT_PS1_SHOWCONFLICTSTATE-}" == "yes" ]] &&
532-
[[ $(git ls-files --unmerged 2>/dev/null) ]]; then
592+
if [ "${GIT_PS1_SHOWCONFLICTSTATE-}" = "yes" ] &&
593+
[ "$(git ls-files --unmerged 2>/dev/null)" ]; then
533594
conflict="|CONFLICT"
534595
fi
535596

@@ -581,10 +642,10 @@ __git_ps1 ()
581642
fi
582643
fi
583644

584-
local z="${GIT_PS1_STATESEPARATOR-" "}"
645+
local z="${GIT_PS1_STATESEPARATOR- }"
585646

586647
b=${b##refs/heads/}
587-
if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
648+
if [ "$pcmode" = yes ] && [ "$ps1_expanded" = yes ]; then
588649
__git_ps1_branch_name=$b
589650
b="\${__git_ps1_branch_name}"
590651
fi
@@ -596,7 +657,7 @@ __git_ps1 ()
596657
local f="$h$w$i$s$u$p"
597658
local gitstring="$c$b${f:+$z$f}${sparse}$r${upstream}${conflict}"
598659

599-
if [ $pcmode = yes ]; then
660+
if [ "$pcmode" = yes ]; then
600661
if [ "${__git_printf_supports_v-}" != yes ]; then
601662
gitstring=$(printf -- "$printf_format" "$gitstring")
602663
else
@@ -607,5 +668,5 @@ __git_ps1 ()
607668
printf -- "$printf_format" "$gitstring"
608669
fi
609670

610-
return $exit
671+
return "$exit"
611672
}

0 commit comments

Comments
 (0)