Skip to content

Commit f12785a

Browse files
szedergitster
authored andcommitted
completion: improve handling quoted paths on the command line
Our git-aware path completion doesn't work when it has to complete a word already containing quoted and/or backslash-escaped characters on the command line. The root cause of the issue is that completion functions see all words on the command line verbatim, i.e. including all backslash, single and double quote characters that the shell would eventually remove when executing the finished command. These quoting/escaping characters cause different issues depending on which path component of the word to be completed contains them: - The quoting/escaping is in the prefix path component(s). Let's suppose we have a directory called 'New Dir', containing two untracked files 'file.c' and 'file.o', and we have a gitignore rule ignoring object files. In this case all of these: git add New\ Dir/<TAB> git add "New Dir/<TAB> git add 'New Dir/<TAB> should uniquely complete 'file.c' right away, but Bash offers both 'file.c' and 'file.o' instead. The reason for this behavior is that our completion script uses the prefix directory name like 'git -C "New\ Dir/" ls-files ...", i.e. with the backslash inside double quotes. Git then tries to enter a directory called 'New\ Dir', which (most likely) fails because such a directory doesn't exists. As a result our completion script doesn't list any files, leaves the COMPREPLY array empty, which in turn causes Bash to fall back to its simple filename completion and lists all files in that directory, i.e. both 'file.c' and 'file.o'. - The quoting/escaping is in the path component to be completed. Let's suppose we have two untracked files 'New File.c' and 'New File.o', and we have a gitignore rule ignoring object files. In this case all of these: git add New\ Fi<TAB> git add "New Fi<TAB> git add 'New Fi<TAB> should uniquely complete 'New File.c' right away, but Bash offers both 'New File.c' and 'New File.o' instead. The reason for this behavior is that our completion script uses this 'New\ Fi' or '"New Fi' etc. word to filter matching paths, and of course none of the potential filenames will match because of the included backslash or double quote. The end result is the same as above: the completion script doesn't list any files, Bash falls back to its filename completion, which then lists the matching object file as well. Add the new helper function __git_dequote() [1], which removes (most of[2]) the quoting and escaping from the word it gets as argument. To minimize the overhead of calling this function, store its result in the variable $dequoted_word, supposed to be declared local in the caller; simply printing the result would require a command substitution imposing the overhead of fork()ing a subshell. Use this function in __git_complete_index_file() to dequote the current word, i.e. the path, to be completed, to avoid the above described quoting-related issues, thereby fixing two of the failing quoted path completion tests. [1] The bash-completion project already has a dequote() function, which I hoped I could borrow to deal with this, but unfortunately it doesn't work quite well for this purpose (perhaps that's why even the bash-completion project only rarely uses it). The main issue is that their dequote() is implemented as: eval printf %s "$1" 2> /dev/null where $1 would contain the word to be completed. While it's a short and sweet one-liner, the use of 'eval' requires that $1 is a syntactically valid string, which is not the case when quoting the path like 'git add "New Dir/<TAB>'. This causes 'eval' to fail, because it can't find the matching closing double quote, and the function returns nothing. The result is totally broken behavior, as if the current word were empty, and the completion script would then list all files from the current directory. This is why one of the quoted path completion tests specifically checks the completion of a path with an opening but without a corresponding closing double quote character. Furthermore, the 'eval' performs all kinds of expansions, which may or may not be desired; I think it's the latter. Finally, using this function would require a command substitution. [2] Bash understands the $'string' quoting as well, which "expands to 'string', with backslash-escaped characters replaced as specified by the ANSI C standard" (quoted from Bash manpage). Since shell metacharacters, field separators, globbing, etc. can all be easily entered using standard shell escaping or quoting, this type of quoting comes in handly when dealing with control characters that are otherwise difficult both to "type" and to see on the command line. Because of this difficulty I would assume that people do avoid pathnames with such control characters anyway, so I didn't bother implementing it. This function is already way too long as it is. Signed-off-by: SZEDER Gábor <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 3dfe23b commit f12785a

File tree

2 files changed

+116
-6
lines changed

2 files changed

+116
-6
lines changed

contrib/completion/git-completion.bash

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,70 @@ __git ()
9494
${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null
9595
}
9696

97+
# Removes backslash escaping, single quotes and double quotes from a word,
98+
# stores the result in the variable $dequoted_word.
99+
# 1: The word to dequote.
100+
__git_dequote ()
101+
{
102+
local rest="$1" len ch
103+
104+
dequoted_word=""
105+
106+
while test -n "$rest"; do
107+
len=${#dequoted_word}
108+
dequoted_word="$dequoted_word${rest%%[\\\'\"]*}"
109+
rest="${rest:$((${#dequoted_word}-$len))}"
110+
111+
case "${rest:0:1}" in
112+
\\)
113+
ch="${rest:1:1}"
114+
case "$ch" in
115+
$'\n')
116+
;;
117+
*)
118+
dequoted_word="$dequoted_word$ch"
119+
;;
120+
esac
121+
rest="${rest:2}"
122+
;;
123+
\')
124+
rest="${rest:1}"
125+
len=${#dequoted_word}
126+
dequoted_word="$dequoted_word${rest%%\'*}"
127+
rest="${rest:$((${#dequoted_word}-$len+1))}"
128+
;;
129+
\")
130+
rest="${rest:1}"
131+
while test -n "$rest" ; do
132+
len=${#dequoted_word}
133+
dequoted_word="$dequoted_word${rest%%[\\\"]*}"
134+
rest="${rest:$((${#dequoted_word}-$len))}"
135+
case "${rest:0:1}" in
136+
\\)
137+
ch="${rest:1:1}"
138+
case "$ch" in
139+
\"|\\|\$|\`)
140+
dequoted_word="$dequoted_word$ch"
141+
;;
142+
$'\n')
143+
;;
144+
*)
145+
dequoted_word="$dequoted_word\\$ch"
146+
;;
147+
esac
148+
rest="${rest:2}"
149+
;;
150+
\")
151+
rest="${rest:1}"
152+
break
153+
;;
154+
esac
155+
done
156+
;;
157+
esac
158+
done
159+
}
160+
97161
# The following function is based on code from:
98162
#
99163
# bash_completion - programmable completion functions for bash 3.2+
@@ -406,13 +470,17 @@ __git_index_files ()
406470
# The exception is --committable, which finds the files appropriate commit.
407471
__git_complete_index_file ()
408472
{
409-
local pfx="" cur_="$cur"
473+
local dequoted_word pfx="" cur_
410474

411-
case "$cur_" in
475+
__git_dequote "$cur"
476+
477+
case "$dequoted_word" in
412478
?*/*)
413-
pfx="${cur_%/*}/"
414-
cur_="${cur_##*/}"
479+
pfx="${dequoted_word%/*}/"
480+
cur_="${dequoted_word##*/}"
415481
;;
482+
*)
483+
cur_="$dequoted_word"
416484
esac
417485

418486
__gitcomp_file "$(__git_index_files "$1" "$pfx")" "$pfx" "$cur_"

t/t9902-completion.sh

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,46 @@ test_expect_success '__gitdir - remote as argument' '
400400
test_cmp expected "$actual"
401401
'
402402

403+
404+
test_expect_success '__git_dequote - plain unquoted word' '
405+
__git_dequote unquoted-word &&
406+
verbose test unquoted-word = "$dequoted_word"
407+
'
408+
409+
# input: b\a\c\k\'\\\"s\l\a\s\h\es
410+
# expected: back'\"slashes
411+
test_expect_success '__git_dequote - backslash escaped' '
412+
__git_dequote "b\a\c\k\\'\''\\\\\\\"s\l\a\s\h\es" &&
413+
verbose test "back'\''\\\"slashes" = "$dequoted_word"
414+
'
415+
416+
# input: sin'gle\' '"quo'ted
417+
# expected: single\ "quoted
418+
test_expect_success '__git_dequote - single quoted' '
419+
__git_dequote "'"sin'gle\\\\' '\\\"quo'ted"'" &&
420+
verbose test '\''single\ "quoted'\'' = "$dequoted_word"
421+
'
422+
423+
# input: dou"ble\\" "\"\quot"ed
424+
# expected: double\ "\quoted
425+
test_expect_success '__git_dequote - double quoted' '
426+
__git_dequote '\''dou"ble\\" "\"\quot"ed'\'' &&
427+
verbose test '\''double\ "\quoted'\'' = "$dequoted_word"
428+
'
429+
430+
# input: 'open single quote
431+
test_expect_success '__git_dequote - open single quote' '
432+
__git_dequote "'\''open single quote" &&
433+
verbose test "open single quote" = "$dequoted_word"
434+
'
435+
436+
# input: "open double quote
437+
test_expect_success '__git_dequote - open double quote' '
438+
__git_dequote "\"open double quote" &&
439+
verbose test "open double quote" = "$dequoted_word"
440+
'
441+
442+
403443
test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
404444
sed -e "s/Z$//g" >expected <<-EOF &&
405445
with-trailing-space Z
@@ -1437,7 +1477,7 @@ _git_test_path_comp ()
14371477
__git_complete_index_file --others
14381478
}
14391479

1440-
test_expect_failure 'complete files - escaped characters on cmdline' '
1480+
test_expect_success 'complete files - escaped characters on cmdline' '
14411481
test_when_finished "rm -rf \"New|Dir\"" &&
14421482
mkdir "New|Dir" &&
14431483
>"New|Dir/New&File.c" &&
@@ -1453,11 +1493,13 @@ test_expect_failure 'complete files - escaped characters on cmdline' '
14531493
"New|Dir/New&File.c"
14541494
'
14551495

1456-
test_expect_failure 'complete files - quoted characters on cmdline' '
1496+
test_expect_success 'complete files - quoted characters on cmdline' '
14571497
test_when_finished "rm -r \"New(Dir\"" &&
14581498
mkdir "New(Dir" &&
14591499
>"New(Dir/New)File.c" &&
14601500
1501+
# Testing with an opening but without a corresponding closing
1502+
# double quote is important.
14611503
test_completion "git test-path-comp \"New(D" "New(Dir" &&
14621504
test_completion "git test-path-comp \"New(Dir/New)F" \
14631505
"New(Dir/New)File.c"

0 commit comments

Comments
 (0)