01问题描述
不知道大家有没有遇见这样一个问题,在使用 rsync 或者 scp 同步传输数据时,在目标主机后使用 tab 补全就会很卡顿,甚至直接就卡死。这一度让笔者的导师很苦恼,看笔者骨骼精奇,就让笔者来看看解决一下。
02场景还原
先简单分析一下吧,既然是按 tab 键后出现卡顿,那么很有可能是 bash-completion 搞的鬼。并且 tab 出来的远程目标主机的文件路径,那么多半是 bash-completion 到远程主机去获取文件路径从而造成的卡顿。当然这只是猜测,下面先测试一下。
测试环境(CentOS)
A机
主机名:localhost
操作系统:CentOS 7.5
B机
主机名:Touchl
操作系统:CentOS 7.3
1、在 A 机上安装有 bash-completion-2.7-5.el8.noarch 时
2、使用 rsync 同步文件到 B 机,tab IP 后的目录 呈现的是远程目录 明显卡顿
3、使用 scp 传输文件到 B 机,tab IP 后的目录 呈现的是远程目录 明显卡顿
卸载 A 机上的 bash-completion-2.1-6.el7.noarch 后,重登终端
使用 rsync 同步文件到 B 机,tab IP 后的目录 呈现的仍是本地目录 没有卡顿
使用 scp 传输文件到 B 机,tab IP 后的目录 呈现的仍是本地目录 没有卡顿。
测试环境(ubuntu)
A机
主机名:controller
系统版本:Ubuntu 14.04.6 LTS
B机
主机名:compute
系统版本:Ubuntu 14.04.6 LTS
A 机装有 bash-completion 时
使用 rsync 同步文件到 B 机,tab IP 后的目录 出现的是远程目录下的文件 明显卡顿
使用 scp 传输文件到 B 机,tab IP 后的目录,出现的也是远程目录下的文件 明显卡顿
A 机卸载 bash-completion 然后,重登终端
使用 rsync 同步文件到 B 机,tab IP 后的目录, 出现的是本地文件
使用 scp 传输文件到 B 机,tab IP 后的目录, 出现的是本地文件
难道我们需要将 bash-completion 卸载来解决这个问题吗?
那我还要 “tab” 干嘛,那么接下来的目标是在保留 bash-completion 的情况下,消除卡顿。
03bash-completion 概述
那么我们先来了解一下 bash-completion 吧
bash-completion 是 bash shell 的一个命令行命令补全的集合,其用途是规定参数怎么自动补全。
其是通过一个复杂的脚本来实现可编程的补全程序,减少系统管理员日常维护工作,提高工作效率。
如果是用 apt-get 或者 yum 进行安装的,那么 bash-completion 的脚本位置位于
/usr/share/bash-completion/
其中 bash_completion 是主配置文件,定义了许多通用函数;completions 目录是用于存储规定各命令怎样补全的脚本。
那么对于bash-completion默认不支持补全功能的一些命令,就可以自行编写或下载对应的补全脚本放在 /usr/share/bash-completion/completions 目录下即可。
04解决方案
根据上文,我们知道 /usr/share/bash-completion/completions/ 目录下的文件即是用于存储各命令补全的脚本,那么解决卡顿问题最简单粗暴的方法就是删除对应的补全脚本文件。
mv /usr/share/bash-completion/completions/rsync /tmp/rsync
mv /usr/share/bash-completion/completions/scp /tmp/scp
退出终端后,测试,果然 rsync 与 scp 的 tab 补全失效了
这虽然也解决了问题,但是却让 rsync 与 scp 的全部命令补全功能失效,因此方案需要进一步改进。
接下来查看一下 rsync 命令补全文件:
# bash completion for rsync -*- shell-script -*-
_rsync()
{
# cur 表示当前光标下的单词
# prev 表示上一个单词
# words 数组,存放当前命令行中输入的所有单词
# cword 整数,当前光标下输入的单词位于 words 数组的索引
local cur prev words cword split
# 调用主配置文件的 init_completion() 函数初始化命令补全
_init_completion -s -n : || return
# 匹配当前光标的上一个单词
case $prev in
--config|--password-file|--include-from|--exclude-from|--files-from|\
--log-file|--write-batch|--only-write-batch|--read-batch)
compopt +o nospace
# 调用 filedir() 函数输出当前目录下的文件与目录
_filedir
return
;;
--temp-dir|--compare-dest|--backup-dir|--partial-dir|--copy-dest|\
--link-dest|-!(-*)T)
compopt +o nospace
# 调用 filedir() 函数加 -d 参数输出当前目录下的目录
_filedir -d
return
;;
--rsh|-!(-*)e)
compopt +o nospace
# COMPREPLY:候选的补全结果
# compgen 内置补全命令,根据不同的参数,生成匹配单词的候选补全列表
# -W 参数为指定空格分隔的单词列表
COMPREPLY=( $(compgen -W 'rsh ssh' -- "$cur") )
return
;;
--compress-level)
compopt +o nospace
COMPREPLY=( $(compgen -W '{1..9}' -- "$cur") )
return
;;
esac
$split && return
_expand || return
# 匹配当前光标下的单词
case $cur in
-*)
COMPREPLY=( $(compgen -W '--verbose --quiet --no-motd --checksum
...(此处省略)
' -- "$cur") )
[[ $COMPREPLY == *= ]] || compopt +o nospace
;;
*:*)
# 获取远程主机下的文件路径
# find which remote shell is used
local i shell=ssh
for (( i=1; i < cword; i++ )); do
if [[ "${words[i]}" == -@(e|-rsh) ]]; then
shell=${words[i+1]}
break
fi
done
[[ $shell == ssh ]] && _xfunc ssh _scp_remote_files
;;
*)
# 调用 known_hosts_real() 函数,补全已知的主机名列表
_known_hosts_real -c -a -- "$cur"
# 调用命令补全脚本 ssh 下的 scp_local_files() 函数,补全本地文件列表
_xfunc ssh _scp_local_files
;;
esac
} &&
complete -F _rsync -o nospace rsync
# ex: filetype=sh
scp 命令补全脚本基本也一致,其获取目标主机下的文件列表的代码如下
case $cur in
!(*:*)/*|[.~]*) ;; # looks like a path
*:*) _scp_remote_files ; return 0 ;;
esac
此时就有一个解决方案就是注释掉 rsync 和 scp 的获取目标主机下文件列表的代码,这样就不会造成卡顿,rsync 和 scp 的其它命令补全仍然可用。但是这样也有一个缺点就是,在目标主机后使用 tab 就什么都 tab 不出来。
而线上服务器的很多文件路径都是一致的,因此想让在目标主机后 tab 出现的是本地文件路径。
那么下面让我们再来看看补全脚本中补全目标主机下的文件路径与补全本地文件路径的代码。
rsync:
*:*)
# 获取远程主机下的文件路径
# find which remote shell is used
local i shell=ssh
for (( i=1; i < cword; i++ )); do
if [[ "${words[i]}" == -@(e|-rsh) ]]; then
shell=${words[i+1]}
break
fi
done
[[ $shell == ssh ]] && _xfunc ssh _scp_remote_files
;;
*)
# 调用 known_hosts_real() 函数,补全已知的主机名列表
_known_hosts_real -c -a -- "$cur"
# 调用命令补全脚本 ssh 下的 scp_local_files() 函数,补全本地文件列表
_xfunc ssh _scp_local_files
;;
scp:
case $cur in
!(*:*)/*|[.~]*) ;; # looks like a path
*:*) _scp_remote_files ; return 0 ;;
esac
补全远程主机下的文件路径主要是通过调用 ssh 的 scp_remote_files() 函数,而补全本地文件路径则是通过调用 ssh scp_local_files() 函数。
下面先看一下 scp_remote_files 函数:
# Complete remote files with ssh. If the first arg is -d, complete on dirs
# only. Returns paths escaped with three backslashes.
_scp_remote_files()
{
local IFS=$'\n'
# remove backslash escape from the first colon
# 将当前光标下的单词中的 "\:" 转换为 ":"
cur=${cur/\\:/:}
# 获取目标主机名
local userhost=${cur%%?(\\):*}
# 获取路径
local path=${cur#*:}
# unescape (3 backslashes to 1 for chars we escaped)
path=$( sed -e 's/\\\\\\\('$_scp_path_esc'\)/\\\1/g' <<<"$path" )
# default to home dir of specified user on remote host
if [[ -z $path ]]; then
path=$(ssh -o 'Batchmode yes' $userhost pwd 2>/dev/null)
fi
local files
# 到远程去补全路径
if [[ $1 == -d ]]; then
# escape problematic characters; remove non-dirs
files=$( ssh -o 'Batchmode yes' $userhost \
command ls -aF1dL "$path*" 2>/dev/null | \
sed -e 's/'$_scp_path_esc'/\\\\\\&/g' -e '/[^\/]$/d' )
else
# escape problematic characters; remove executables, aliases, pipes
# and sockets; add space at end of file names
files=$( ssh -o 'Batchmode yes' $userhost \
command ls -aF1dL "$path*" 2>/dev/null | \
sed -e 's/'$_scp_path_esc'/\\\\\\&/g' -e 's/[*@|=]$//g' \
-e 's/[^\/]$/& /g' )
fi
COMPREPLY+=( $files )
}
scp_local_files() 函数如下
# This approach is used instead of _filedir to get a space appended
# after local file/dir completions, and -o nospace retained for others.
# If first arg is -d, complete on directory names only. The next arg is
# an optional prefix to add to returned completions.
_scp_local_files()
{
local IFS=$'\n'
local dirsonly=false
if [[ $1 == -d ]]; then
dirsonly=true
shift
fi
if $dirsonly ; then
COMPREPLY+=( $( command ls -aF1dL $cur* 2>/dev/null | \
sed -e "s/$_scp_path_esc/\\\\&/g" -e '/[^\/]$/d' -e "s/^/$1/") )
else
COMPREPLY+=( $( command ls -aF1dL $cur* 2>/dev/null | \
sed -e "s/$_scp_path_esc/\\\\&/g" -e 's/[*@|=]$//g' \
-e 's/[^\/]$/& /g' -e "s/^/$1/") )
fi
}
从上述看出,当执行下列命令后,bash-completion 首先会调用 rsync 命令补全脚本进行匹配,然后通过一个判断选择匹配到 “:” 项,调用 scp_remote_file 函数获取目标主机路径。scp_remote_file() 函数中,此时 cur 变量的值为"compute:",那么 path 变量的值为空 ,在通过一个 if 语句判断如果 path 值为空,则到 ssh 到远程目标主机执行 pwd 命令并将结果赋值给 path,然后通过 ssh 远程到目标主机执行 ls -aF1dL “$path*” 命令获取所有以 $path 开头的最小路径。
rsync -av compute:[按 tab键]
scp_local_files() 函数也是一样,不过只是少了 ssh 到远程主机执行对应的补全命令。
因此我们若是想要让在目标主机后按 tab 键补全的是本地文件路径,那么我们就可以调用 scp_local_files 函数进行补全,不过在调用之前,需要将 cur 的值进行一个切割,只保留其后面的路径部分,因此对于 rsync 命令补全脚本,我们可以这么改。
# 将 /usr/share/bash-completion/completions/rsync 配置文件中 *:*) 选项下的命令更改为
*:*)
cur=${cur#*:}
_xfunc ssh _scp_local_files
# # find which remote shell is used
# local i shell=ssh
# for (( i=1; i < cword; i++ )); do
# if [[ "${words[i]}" == -@(e|-rsh) ]]; then
# shell=${words[i+1]}
# break
# fi
# done
# [[ $shell == ssh ]] && _xfunc ssh _scp_remote_files
;;
同样 scp 命令补全脚本,也只需要做同样的改动。
# 将 /usr/share/bash-completion/completions/scp 配置文件中的
case $cur in
!(*:*)/*|[.~]*) ;; # looks like a path
*:*) _scp_remote_files ; return ;;
esac
# 更改为
case $cur in
!(*:*)/*|[.~]*) ;; # looks like a path
*:*) cur=${cur#*:}; _xfunc ssh _scp_local_files ;;
esac
# 即可
参考链接 :
rsync、scp “tab” 卡顿问题 :
https://mp.weixin.qq.com/s/lkdKADQx2NgeNk5ShZYBDw