Skip to content

Commit 9fb38ca

Browse files
committed
fix(rsync): overescape remote paths if rsync version is < 3.2.4
1 parent e0fc2f3 commit 9fb38ca

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

completions/rsync

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# bash completion for rsync -*- shell-script -*-
22

3+
_comp_cmd_rsync__vercomp()
4+
{
5+
if [[ $1 == "$2" ]]; then
6+
return 0
7+
fi
8+
local IFS=.
9+
local i ver1=($1) ver2=($2)
10+
local n=$((${#ver1[@]} >= ${#ver2[@]} ? ${#ver1[@]} : ${#ver2[@]}))
11+
for ((i = 0; i < n; i++)); do
12+
if ((10#${ver1[i]:-0} > 10#${ver2[i]:-0})); then
13+
return 1
14+
fi
15+
if ((10#${ver1[i]:-0} < 10#${ver2[i]:-0})); then
16+
return 2
17+
fi
18+
done
19+
return 0
20+
}
21+
322
_rsync()
423
{
524
local cur prev words cword split comp_args
@@ -59,7 +78,15 @@ _rsync()
5978
break
6079
fi
6180
done
62-
[[ $shell == ssh ]] && _comp_xfunc ssh scp_remote_files
81+
if [[ $shell == ssh ]]; then
82+
local rsync_version=$("$1" --version 2>/dev/null | sed -n '1s/.*rsync *version \([0-9.]*\).*/\1/p')
83+
_comp_cmd_rsync__vercomp "$rsync_version" "3.2.4"
84+
if (($? == 2)); then
85+
_comp_xfunc ssh scp_remote_files
86+
else
87+
_comp_xfunc ssh scp_remote_files -l
88+
fi
89+
fi
6390
;;
6491
*)
6592
_known_hosts_real -c -a -- "$cur"

completions/ssh

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,29 @@ _sftp()
431431
# shellcheck disable=SC2089
432432
_comp_cmd_scp__path_esc='[][(){}<>"'"'"',:;^&!$=?`\\|[:space:]]'
433433

434-
# Complete remote files with ssh. If the first arg is -d, complete on dirs
435-
# only. Returns paths escaped with three backslashes.
434+
# Complete remote files with ssh. Returns paths escaped with three backslashes
435+
# (unless -l option is provided).
436+
# Options:
437+
# -d Complete on dirs only.
438+
# -l Return paths escaped with one backslash instead of three.
436439
# shellcheck disable=SC2120
437440
_comp_xfunc_ssh_scp_remote_files()
438441
{
442+
local dirs_only=false
443+
local less_escaping=false
444+
445+
local flag OPTIND=1 OPTARG="" OPTERR=0
446+
while getopts "dl" flag "$@"; do
447+
case $flag in
448+
d) dirs_only=true ;;
449+
l) less_escaping=true ;;
450+
*)
451+
echo "bash_completion: $FUNCNAME: usage error: $*" >&2
452+
return 1
453+
;;
454+
esac
455+
done
456+
439457
# remove backslash escape from the first colon
440458
cur=${cur/\\:/:}
441459

@@ -451,20 +469,25 @@ _comp_xfunc_ssh_scp_remote_files()
451469
path=$(ssh -o 'Batchmode yes' "$userhost" pwd 2>/dev/null)
452470
fi
453471

472+
local escape_replacement='\\\\\\&'
473+
if "$less_escaping"; then
474+
escape_replacement='\\&'
475+
fi
476+
454477
local files
455-
if [[ ${1-} == -d ]]; then
478+
if "$dirs_only"; then
456479
# escape problematic characters; remove non-dirs
457480
# shellcheck disable=SC2090
458481
files=$(ssh -o 'Batchmode yes' "$userhost" \
459482
command ls -aF1dL "$path*" 2>/dev/null |
460-
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\&/g' -e '/[^\/]$/d')
483+
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/'"$escape_replacement"'/g' -e '/[^\/]$/d')
461484
else
462485
# escape problematic characters; remove executables, aliases, pipes
463486
# and sockets; add space at end of file names
464487
# shellcheck disable=SC2090
465488
files=$(ssh -o 'Batchmode yes' "$userhost" \
466489
command ls -aF1dL "$path*" 2>/dev/null |
467-
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\&/g' -e 's/[*@|=]$//g' \
490+
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/'"$escape_replacement"'/g' -e 's/[*@|=]$//g' \
468491
-e 's/[^\/]$/& /g')
469492
fi
470493
_comp_split -la COMPREPLY "$files"

test/t/test_rsync.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import pytest
22

3+
from conftest import assert_bash_exec
4+
5+
LIVE_HOST = "bash_completion"
6+
37

48
@pytest.mark.bashcomp(ignore_env=r"^[+-]_comp_cmd_scp__path_esc=")
59
class TestRsync:
@@ -18,3 +22,54 @@ def test_3(self, completion):
1822
@pytest.mark.complete("rsync --", require_cmd=True)
1923
def test_4(self, completion):
2024
assert "--help" in completion
25+
26+
@pytest.mark.parametrize(
27+
"ver1,ver2,result",
28+
[
29+
("1", "1", "="),
30+
("1", "2", "<"),
31+
("2", "1", ">"),
32+
("1.1", "1.2", "<"),
33+
("1.2", "1.1", ">"),
34+
("1.1", "1.1.1", "<"),
35+
("1.1.1", "1.1", ">"),
36+
("1.1.1", "1.1.1", "="),
37+
("2.1", "2.2", "<"),
38+
("3.0.4.10", "3.0.4.2", ">"),
39+
("4.08", "4.08.01", "<"),
40+
("3.2.1.9.8144", "3.2", ">"),
41+
("3.2", "3.2.1.9.8144", "<"),
42+
("1.2", "2.1", "<"),
43+
("2.1", "1.2", ">"),
44+
("5.6.7", "5.6.7", "="),
45+
("1.01.1", "1.1.1", "="),
46+
("1.1.1", "1.01.1", "="),
47+
("1", "1.0", "="),
48+
("1.0", "1", "="),
49+
("1.0.2.0", "1.0.2", "="),
50+
("1..0", "1.0", "="),
51+
("1.0", "1..0", "="),
52+
],
53+
)
54+
def test_vercomp(self, bash, ver1, ver2, result):
55+
output = assert_bash_exec(
56+
bash,
57+
f"_comp_cmd_rsync__vercomp {ver1} {ver2}; echo $?",
58+
want_output=True,
59+
).strip()
60+
61+
if result == "=":
62+
assert output == "0"
63+
elif result == ">":
64+
assert output == "1"
65+
elif result == "<":
66+
assert output == "2"
67+
else:
68+
raise Exception(f"Unsupported comparison result: {result}")
69+
70+
@pytest.mark.complete(f"rsync {LIVE_HOST}:spaces", sleep_after_tab=2)
71+
def test_remote_path_with_spaces(self, completion):
72+
assert (
73+
completion == r"\ in\ filename.txt"
74+
or completion == r"\\\ in\\\ filename.txt"
75+
)

test/t/test_scp.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,7 @@ def test_remote_path_with_nullglob(self, completion):
9595
)
9696
def test_remote_path_with_failglob(self, completion):
9797
assert not completion
98+
99+
@pytest.mark.complete(f"scp {LIVE_HOST}:spaces", sleep_after_tab=2)
100+
def test_remote_path_with_spaces(self, completion):
101+
assert completion == r"\\\ in\\\ filename.txt"

0 commit comments

Comments
 (0)