Merge branch 'sg/complete-paths'

Command line completion (in contrib/) learned to complete pathnames
for various commands better.

* sg/complete-paths:
  t9902-completion: exercise __git_complete_index_file() directly
  completion: don't return with error from __gitcomp_file_direct()
  completion: fill COMPREPLY directly when completing paths
  completion: improve handling quoted paths in 'git ls-files's output
  completion: remove repeated dirnames with 'awk' during path completion
  t9902-completion: ignore COMPREPLY element order in some tests
  completion: use 'awk' to strip trailing path components
  completion: let 'ls-files' and 'diff-index' filter matching paths
  completion: improve handling quoted paths on the command line
  completion: support completing non-ASCII pathnames
  completion: simplify prefix path component handling during path completion
  completion: move __git_complete_index_file() next to its helpers
  t9902-completion: add tests demonstrating issues with quoted pathnames
This commit is contained in:
Junio C Hamano 2018-05-30 14:04:08 +09:00
commit 4ce72180ab
3 changed files with 362 additions and 28 deletions

View file

@ -94,6 +94,70 @@ __git ()
${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null
}
# Removes backslash escaping, single quotes and double quotes from a word,
# stores the result in the variable $dequoted_word.
# 1: The word to dequote.
__git_dequote ()
{
local rest="$1" len ch
dequoted_word=""
while test -n "$rest"; do
len=${#dequoted_word}
dequoted_word="$dequoted_word${rest%%[\\\'\"]*}"
rest="${rest:$((${#dequoted_word}-$len))}"
case "${rest:0:1}" in
\\)
ch="${rest:1:1}"
case "$ch" in
$'\n')
;;
*)
dequoted_word="$dequoted_word$ch"
;;
esac
rest="${rest:2}"
;;
\')
rest="${rest:1}"
len=${#dequoted_word}
dequoted_word="$dequoted_word${rest%%\'*}"
rest="${rest:$((${#dequoted_word}-$len+1))}"
;;
\")
rest="${rest:1}"
while test -n "$rest" ; do
len=${#dequoted_word}
dequoted_word="$dequoted_word${rest%%[\\\"]*}"
rest="${rest:$((${#dequoted_word}-$len))}"
case "${rest:0:1}" in
\\)
ch="${rest:1:1}"
case "$ch" in
\"|\\|\$|\`)
dequoted_word="$dequoted_word$ch"
;;
$'\n')
;;
*)
dequoted_word="$dequoted_word\\$ch"
;;
esac
rest="${rest:2}"
;;
\")
rest="${rest:1}"
break
;;
esac
done
;;
esac
done
}
# The following function is based on code from:
#
# bash_completion - programmable completion functions for bash 3.2+
@ -346,6 +410,24 @@ __gitcomp_nl ()
__gitcomp_nl_append "$@"
}
# Fills the COMPREPLY array with prefiltered paths without any additional
# processing.
# Callers must take care of providing only paths that match the current path
# to be completed and adding any prefix path components, if necessary.
# 1: List of newline-separated matching paths, complete with all prefix
# path componens.
__gitcomp_file_direct ()
{
local IFS=$'\n'
COMPREPLY=($1)
# use a hack to enable file mode in bash < 4
compopt -o filenames +o nospace 2>/dev/null ||
compgen -f /non-existing-dir/ >/dev/null ||
true
}
# Generates completion reply with compgen from newline-separated possible
# completion filenames.
# It accepts 1 to 3 arguments:
@ -365,7 +447,8 @@ __gitcomp_file ()
# use a hack to enable file mode in bash < 4
compopt -o filenames +o nospace 2>/dev/null ||
compgen -f /non-existing-dir/ > /dev/null
compgen -f /non-existing-dir/ >/dev/null ||
true
}
# Execute 'git ls-files', unless the --committable option is specified, in
@ -375,10 +458,12 @@ __gitcomp_file ()
__git_ls_files_helper ()
{
if [ "$2" == "--committable" ]; then
__git -C "$1" diff-index --name-only --relative HEAD
__git -C "$1" -c core.quotePath=false diff-index \
--name-only --relative HEAD -- "${3//\\/\\\\}*"
else
# NOTE: $2 is not quoted in order to support multiple options
__git -C "$1" ls-files --exclude-standard $2
__git -C "$1" -c core.quotePath=false ls-files \
--exclude-standard $2 -- "${3//\\/\\\\}*"
fi
}
@ -389,12 +474,103 @@ __git_ls_files_helper ()
# If provided, only files within the specified directory are listed.
# Sub directories are never recursed. Path must have a trailing
# slash.
# 3: List only paths matching this path component (optional).
__git_index_files ()
{
local root="${2-.}" file
local root="$2" match="$3"
__git_ls_files_helper "$root" "$1" |
cut -f1 -d/ | sort | uniq
__git_ls_files_helper "$root" "$1" "$match" |
awk -F / -v pfx="${2//\\/\\\\}" '{
paths[$1] = 1
}
END {
for (p in paths) {
if (substr(p, 1, 1) != "\"") {
# No special characters, easy!
print pfx p
continue
}
# The path is quoted.
p = dequote(p)
if (p == "")
continue
# Even when a directory name itself does not contain
# any special characters, it will still be quoted if
# any of its (stripped) trailing path components do.
# Because of this we may have seen the same direcory
# both quoted and unquoted.
if (p in paths)
# We have seen the same directory unquoted,
# skip it.
continue
else
print pfx p
}
}
function dequote(p, bs_idx, out, esc, esc_idx, dec) {
# Skip opening double quote.
p = substr(p, 2)
# Interpret backslash escape sequences.
while ((bs_idx = index(p, "\\")) != 0) {
out = out substr(p, 1, bs_idx - 1)
esc = substr(p, bs_idx + 1, 1)
p = substr(p, bs_idx + 2)
if ((esc_idx = index("abtvfr\"\\", esc)) != 0) {
# C-style one-character escape sequence.
out = out substr("\a\b\t\v\f\r\"\\",
esc_idx, 1)
} else if (esc == "n") {
# Uh-oh, a newline character.
# We cant reliably put a pathname
# containing a newline into COMPREPLY,
# and the newline would create a mess.
# Skip this path.
return ""
} else {
# Must be a \nnn octal value, then.
dec = esc * 64 + \
substr(p, 1, 1) * 8 + \
substr(p, 2, 1)
out = out sprintf("%c", dec)
p = substr(p, 3)
}
}
# Drop closing double quote, if there is one.
# (There isnt any if this is a directory, as it was
# already stripped with the trailing path components.)
if (substr(p, length(p), 1) == "\"")
out = out substr(p, 1, length(p) - 1)
else
out = out p
return out
}'
}
# __git_complete_index_file requires 1 argument:
# 1: the options to pass to ls-file
#
# The exception is --committable, which finds the files appropriate commit.
__git_complete_index_file ()
{
local dequoted_word pfx="" cur_
__git_dequote "$cur"
case "$dequoted_word" in
?*/*)
pfx="${dequoted_word%/*}/"
cur_="${dequoted_word##*/}"
;;
*)
cur_="$dequoted_word"
esac
__gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
}
# Lists branches from the local repository.
@ -713,26 +889,6 @@ __git_complete_revlist_file ()
esac
}
# __git_complete_index_file requires 1 argument:
# 1: the options to pass to ls-file
#
# The exception is --committable, which finds the files appropriate commit.
__git_complete_index_file ()
{
local pfx="" cur_="$cur"
case "$cur_" in
?*/*)
pfx="${cur_%/*}"
cur_="${cur_##*/}"
pfx="${pfx}/"
;;
esac
__gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
}
__git_complete_file ()
{
__git_complete_revlist_file
@ -3232,6 +3388,15 @@ if [[ -n ${ZSH_VERSION-} ]]; then
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
}
__gitcomp_file_direct ()
{
emulate -L zsh
local IFS=$'\n'
compset -P '*[=:]'
compadd -Q -f -- ${=1} && _ret=0
}
__gitcomp_file ()
{
emulate -L zsh

View file

@ -93,6 +93,15 @@ __gitcomp_nl_append ()
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
}
__gitcomp_file_direct ()
{
emulate -L zsh
local IFS=$'\n'
compset -P '*[=:]'
compadd -Q -f -- ${=1} && _ret=0
}
__gitcomp_file ()
{
emulate -L zsh

View file

@ -84,10 +84,11 @@ test_completion ()
then
printf '%s\n' "$2" >expected
else
sed -e 's/Z$//' >expected
sed -e 's/Z$//' |sort >expected
fi &&
run_completion "$1" &&
test_cmp expected out
sort out >out_sorted &&
test_cmp expected out_sorted
}
# Test __gitcomp.
@ -400,6 +401,46 @@ test_expect_success '__gitdir - remote as argument' '
test_cmp expected "$actual"
'
test_expect_success '__git_dequote - plain unquoted word' '
__git_dequote unquoted-word &&
verbose test unquoted-word = "$dequoted_word"
'
# input: b\a\c\k\'\\\"s\l\a\s\h\es
# expected: back'\"slashes
test_expect_success '__git_dequote - backslash escaped' '
__git_dequote "b\a\c\k\\'\''\\\\\\\"s\l\a\s\h\es" &&
verbose test "back'\''\\\"slashes" = "$dequoted_word"
'
# input: sin'gle\' '"quo'ted
# expected: single\ "quoted
test_expect_success '__git_dequote - single quoted' '
__git_dequote "'"sin'gle\\\\' '\\\"quo'ted"'" &&
verbose test '\''single\ "quoted'\'' = "$dequoted_word"
'
# input: dou"ble\\" "\"\quot"ed
# expected: double\ "\quoted
test_expect_success '__git_dequote - double quoted' '
__git_dequote '\''dou"ble\\" "\"\quot"ed'\'' &&
verbose test '\''double\ "\quoted'\'' = "$dequoted_word"
'
# input: 'open single quote
test_expect_success '__git_dequote - open single quote' '
__git_dequote "'\''open single quote" &&
verbose test "open single quote" = "$dequoted_word"
'
# input: "open double quote
test_expect_success '__git_dequote - open double quote' '
__git_dequote "\"open double quote" &&
verbose test "open double quote" = "$dequoted_word"
'
test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
sed -e "s/Z$//g" >expected <<-EOF &&
with-trailing-space Z
@ -1168,6 +1209,124 @@ test_expect_success 'teardown after ref completion' '
git remote remove other
'
test_path_completion ()
{
test $# = 2 || error "bug in the test script: not 2 parameters to test_path_completion"
local cur="$1" expected="$2"
echo "$expected" >expected &&
(
# In the following tests calling this function we only
# care about how __git_complete_index_file() deals with
# unusual characters in path names. By requesting only
# untracked files we dont have to bother adding any
# paths to the index in those tests.
__git_complete_index_file --others &&
print_comp
) &&
test_cmp expected out
}
test_expect_success 'setup for path completion tests' '
mkdir simple-dir \
"spaces in dir" \
árvíztűrő &&
touch simple-dir/simple-file \
"spaces in dir/spaces in file" \
"árvíztűrő/Сайн яваарай" &&
if test_have_prereq !MINGW &&
mkdir BS\\dir \
'$'separators\034in\035dir'' &&
touch BS\\dir/DQ\"file \
'$'separators\034in\035dir/sep\036in\037file''
then
test_set_prereq FUNNYNAMES
else
rm -rf BS\\dir '$'separators\034in\035dir''
fi
'
test_expect_success '__git_complete_index_file - simple' '
test_path_completion simple simple-dir && # Bash is supposed to
# add the trailing /.
test_path_completion simple-dir/simple simple-dir/simple-file
'
test_expect_success \
'__git_complete_index_file - escaped characters on cmdline' '
test_path_completion spac "spaces in dir" && # Bash will turn this
# into "spaces\ in\ dir"
test_path_completion "spaces\\ i" \
"spaces in dir" &&
test_path_completion "spaces\\ in\\ dir/s" \
"spaces in dir/spaces in file" &&
test_path_completion "spaces\\ in\\ dir/spaces\\ i" \
"spaces in dir/spaces in file"
'
test_expect_success \
'__git_complete_index_file - quoted characters on cmdline' '
# Testing with an opening but without a corresponding closing
# double quote is important.
test_path_completion \"spac "spaces in dir" &&
test_path_completion "\"spaces i" \
"spaces in dir" &&
test_path_completion "\"spaces in dir/s" \
"spaces in dir/spaces in file" &&
test_path_completion "\"spaces in dir/spaces i" \
"spaces in dir/spaces in file"
'
test_expect_success '__git_complete_index_file - UTF-8 in ls-files output' '
test_path_completion á árvíztűrő &&
test_path_completion árvíztűrő/С "árvíztűrő/Сайн яваарай"
'
test_expect_success FUNNYNAMES \
'__git_complete_index_file - C-style escapes in ls-files output' '
test_path_completion BS \
BS\\dir &&
test_path_completion BS\\\\d \
BS\\dir &&
test_path_completion BS\\\\dir/DQ \
BS\\dir/DQ\"file &&
test_path_completion BS\\\\dir/DQ\\\"f \
BS\\dir/DQ\"file
'
test_expect_success FUNNYNAMES \
'__git_complete_index_file - \nnn-escaped characters in ls-files output' '
test_path_completion sep '$'separators\034in\035dir'' &&
test_path_completion '$'separators\034i'' \
'$'separators\034in\035dir'' &&
test_path_completion '$'separators\034in\035dir/sep'' \
'$'separators\034in\035dir/sep\036in\037file'' &&
test_path_completion '$'separators\034in\035dir/sep\036i'' \
'$'separators\034in\035dir/sep\036in\037file''
'
test_expect_success FUNNYNAMES \
'__git_complete_index_file - removing repeated quoted path components' '
test_when_finished rm -r repeated-quoted &&
mkdir repeated-quoted && # A directory whose name in itself
# would not be quoted ...
>repeated-quoted/0-file &&
>repeated-quoted/1\"file && # ... but here the file makes the
# dirname quoted ...
>repeated-quoted/2-file &&
>repeated-quoted/3\"file && # ... and here, too.
# Still, we shold only list the directory name only once.
test_path_completion repeated repeated-quoted
'
test_expect_success 'teardown after path completion tests' '
rm -rf simple-dir "spaces in dir" árvíztűrő \
BS\\dir '$'separators\034in\035dir''
'
test_expect_success '__git_get_config_variables' '
cat >expect <<-EOF &&
name-1
@ -1365,6 +1524,7 @@ test_expect_success 'complete files' '
echo "expected" > .gitignore &&
echo "out" >> .gitignore &&
echo "out_sorted" >> .gitignore &&
git add .gitignore &&
test_completion "git commit " ".gitignore" &&