git-prompt: add some missing quotes

The issues which this commit fixes are unlikely to be broken in real life, but the fixes improve correctness, and would prevent bugs in some uncommon cases, such as weird IFS values. Listing some portability guidelines here for future reference. I'm leaving it to someone else to decide whether to include it in the file itself, place it as a new file, or not. --------- The command "local" is non standard, but is allowed in this file: - Quote initialization if it can expand (local x="$y"). See below. - Don't assume initial value after "local x". Either initialize it (local x=..), or set before first use (local x;.. x=..; <use $x>). (between shells, "local x" can unset x, or inherit it, or do x= ) Other non-standard features beyond "local" are to be avoided. Use the standard "test" - [...] instead of non-standard [[...]] . -------- Quotes (some portability things, but mainly general correctness): Quotes prevent tilde-expansion of some unquoted literal tildes (~). If the expansion is undesirable, quotes would ensure that. Tilds expanded: a=~user:~/ ; echo ~user ~/dir not expanded: t="~"; a=${t}user b=\~foo~; echo "~user" $t/dir But the main reason for quoting is to prevent IFS field splitting (which also coalesces IFS chars) and glob expansion in parts which contain parameter/arithmetic expansion or command substitution. "Simple command" (POSIX term) is assignment[s] and/or command [args]. Examples: foo=bar # one assignment foo=$bar x=y # two assignments foo bar # command, no assignments x=123 foo bar # one assignment and a command The assignments part is not IFS-split or glob-expanded. The command+args part does get IFS field split and glob expanded, but only at unquoted expanded/substituted parts. In the command+args part, expanded/substituted values must be quoted. (the commands here are "[" and "local"): Good: [ "$mode" = yes ]; local s="*" x="$y" e="$?" z="$(cmd ...)" Bad: [ $mode = yes ]; local s=* x=$y e=$? z=$(cmd...) The arguments to "local" do look like assignments, but they're not the assignment part of a simple command; they're at the command part. Still at the command part, no need to quote non-expandable values: Good: local x= y=yes; echo OK OK, but not required: local x="" y="yes"; echo "OK" But completely empty (NULL) arguments must be quoted: foo "" is not the same as: foo Assignments in simple commands - with or without an actual command, don't need quoting becase there's no IFS split or glob expansion: Good: s=* a=$b c=$(cmd...)${x# foo }${y- } [cmd ...] It's also OK to use double quotes, but not required. This behavior (no IFS/glob) is called "assignment context", and "local" does not behave with assignment context in some shells, hence we require quotes when using "local" - for compatibility. The value between 'case' and 'in' doesn't IFS-split/glob-expand: Good: case * $foo $(cmd...) in ... ; esac identical: case "* $foo $(cmd...)" in ... ; esac Nested quotes in command substitution are fine, often necessary: Good: echo "$(foo... "$x" "$(bar ...)")" Nested quotes in substring ops are legal, and sometimes needed to prevent interpretation as a pattern, but not the most readable: Legal: foo "${x#*"$y" }" Nested quotes in "maybe other value" subst are invalid, unnecessary: Good: local x="${y- }"; foo "${z:+ $a }" Bad: local x="${y-" "}"; foo "${z:+" $a "}" Outer/inner quotes in "maybe other value" have different use cases: "${x-$y}" always one quoted arg: "$x" if x is set, else "$y". ${x+"$x"} one quoted arg "$x" if x is set, else no arg at all. Unquoted $x is similar to the second case, but it would get split into few arguments if it includes any of the IFS chars. Assignments don't need the outer quotes, and the braces delimit the value, so nested quotes can be avoided, for readability: a=$(foo "$x") a=${x#*"$y" } c=${y- }; bar "$a" "$b" "$c" Signed-off-by: Avi Halachmi (:avih) <avihpit@yahoo.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Avi Halachmi (:avih) committed Aug 20, 2024 at 01:48 UTC b732e08671f037373f615a6d8509da2dbc476322
1 file changed +13 -13
contrib/completion/git-prompt.sh
+13 -13
@@ -246,7 +246,7 @@ __git_ps1_show_upstream ()
246 if [ -n "$count" ] && [ -n "$name" ]; then
247 __git_ps1_upstream_name=$(git rev-parse \
248 --abbrev-ref "$upstream_type" 2>/dev/null)
249 - if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
249 + if [ "$pcmode" = yes ] && [ "$ps1_expanded" = yes ]; then
250 upstream="$upstream \${__git_ps1_upstream_name}"
251 else
252 upstream="$upstream ${__git_ps1_upstream_name}"
@@ -278,12 +278,12 @@ __git_ps1_colorize_gitstring ()
278 local c_lblue=$'\001\e[1;34m\002'
279 local c_clear=$'\001\e[0m\002'
280 fi
281 - local bad_color=$c_red
282 - local ok_color=$c_green
281 + local bad_color="$c_red"
282 + local ok_color="$c_green"
283 local flags_color="$c_lblue"
284
285 local branch_color=""
286 - if [ $detached = no ]; then
286 + if [ "$detached" = no ]; then
287 branch_color="$ok_color"
288 else
289 branch_color="$bad_color"
@@ -360,7 +360,7 @@ __git_sequencer_status ()
360 __git_ps1 ()
361 {
362 # preserve exit status
363 - local exit=$?
363 + local exit="$?"
364 local pcmode=no
365 local detached=no
366 local ps1pc_start='\u@\h:\w '
@@ -379,7 +379,7 @@ __git_ps1 ()
379 ;;
380 0|1) printf_format="${1:-$printf_format}"
381 ;;
382 - *) return $exit
382 + *) return "$exit"
383 ;;
384 esac
385
@@ -427,7 +427,7 @@ __git_ps1 ()
427 rev_parse_exit_code="$?"
428
429 if [ -z "$repo_info" ]; then
430 - return $exit
430 + return "$exit"
431 fi
432
433 local short_sha=""
@@ -449,7 +449,7 @@ __git_ps1 ()
449 [ "$(git config --bool bash.hideIfPwdIgnored)" != "false" ] &&
450 git check-ignore -q .
451 then
452 - return $exit
452 + return "$exit"
453 fi
454
455 local sparse=""
@@ -499,7 +499,7 @@ __git_ps1 ()
499 case "$ref_format" in
500 files)
501 if ! __git_eread "$g/HEAD" head; then
502 - return $exit
502 + return "$exit"
503 fi
504
505 case $head in
@@ -597,10 +597,10 @@ __git_ps1 ()
597 fi
598 fi
599
600 - local z="${GIT_PS1_STATESEPARATOR-" "}"
600 + local z="${GIT_PS1_STATESEPARATOR- }"
601
602 b=${b##refs/heads/}
603 - if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
603 + if [ "$pcmode" = yes ] && [ "$ps1_expanded" = yes ]; then
604 __git_ps1_branch_name=$b
605 b="\${__git_ps1_branch_name}"
606 fi
@@ -612,7 +612,7 @@ __git_ps1 ()
612 local f="$h$w$i$s$u$p"
613 local gitstring="$c$b${f:+$z$f}${sparse}$r${upstream}${conflict}"
614
615 - if [ $pcmode = yes ]; then
615 + if [ "$pcmode" = yes ]; then
616 if [ "${__git_printf_supports_v-}" != yes ]; then
617 gitstring=$(printf -- "$printf_format" "$gitstring")
618 else
@@ -623,5 +623,5 @@ __git_ps1 ()
623 printf -- "$printf_format" "$gitstring"
624 fi
625
626 - return $exit
626 + return "$exit"
627 }