Skip to content

Commit a60f0b4

Browse files
Rogachakinomyoga
authored andcommitted
fix(rsync): overescape remote paths if rsync version is < 3.2.4
1 parent 52cb37c commit a60f0b4

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
_comp_cmd_rsync()
423
{
524
local cur prev words cword was_split comp_args
@@ -81,7 +100,15 @@ _comp_cmd_rsync()
81100
break
82101
fi
83102
done
84-
[[ $shell == ssh ]] && _comp_compgen -x scp remote_files
103+
if [[ $shell == ssh ]]; then
104+
local rsync_version=$("$1" --version 2>/dev/null | sed -n '1s/.*rsync *version \([0-9.]*\).*/\1/p')
105+
_comp_cmd_rsync__vercomp "$rsync_version" "3.2.4"
106+
if (($? == 2)); then
107+
_comp_compgen -x scp remote_files
108+
else
109+
_comp_compgen -x scp remote_files -l
110+
fi
111+
fi
85112
;;
86113
*)
87114
_comp_compgen_known_hosts -c -a -- "$cur"

completions/ssh

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,12 +459,30 @@ _comp_cmd_sftp()
459459
# shellcheck disable=SC2089
460460
_comp_cmd_scp__path_esc='[][(){}<>"'"'"',:;^&!$=?`\\|[:space:]]'
461461

462-
# Complete remote files with ssh. If the first arg is -d, complete on dirs
463-
# only. Returns paths escaped with three backslashes.
462+
# Complete remote files with ssh. Returns paths escaped with three backslashes
463+
# (unless -l option is provided).
464+
# Options:
465+
# -d Complete on dirs only.
466+
# -l Return paths escaped with one backslash instead of three.
464467
# @since 2.12
465468
# shellcheck disable=SC2120
466469
_comp_xfunc_scp_compgen_remote_files()
467470
{
471+
local _dirs_only=""
472+
local _less_escaping=""
473+
474+
local _flag OPTIND=1 OPTARG="" OPTERR=0
475+
while getopts "dl" _flag "$@"; do
476+
case $_flag in
477+
d) _dirs_only=set ;;
478+
l) _less_escaping=set ;;
479+
*)
480+
echo "bash_completion: $FUNCNAME: usage error: $*" >&2
481+
return 1
482+
;;
483+
esac
484+
done
485+
468486
# remove backslash escape from the first colon
469487
cur=${cur/\\:/:}
470488

@@ -480,20 +498,25 @@ _comp_xfunc_scp_compgen_remote_files()
480498
_path=$(ssh -o 'Batchmode yes' "$_userhost" pwd 2>/dev/null)
481499
fi
482500

501+
local _escape_replacement='\\\\\\&'
502+
if [[ $_less_escaping ]]; then
503+
_escape_replacement='\\&'
504+
fi
505+
483506
local _files
484-
if [[ ${1-} == -d ]]; then
507+
if [[ $_dirs_only ]]; then
485508
# escape problematic characters; remove non-dirs
486509
# shellcheck disable=SC2090
487510
_files=$(ssh -o 'Batchmode yes' "$_userhost" \
488511
command ls -aF1dL "$_path*" 2>/dev/null |
489-
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\&/g' -e '/[^\/]$/d')
512+
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/'"$_escape_replacement"'/g' -e '/[^\/]$/d')
490513
else
491514
# escape problematic characters; remove executables, aliases, pipes
492515
# and sockets; add space at end of file names
493516
# shellcheck disable=SC2090
494517
_files=$(ssh -o 'Batchmode yes' "$_userhost" \
495518
command ls -aF1dL "$_path*" 2>/dev/null |
496-
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/\\&/g' -e 's/[*@|=]$//g' \
519+
command sed -e 's/'"$_comp_cmd_scp__path_esc"'/'"$_escape_replacement"'/g' -e 's/[*@|=]$//g' \
497520
-e 's/[^\/]$/& /g')
498521
fi
499522
_comp_compgen_split -l -- "$_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)