rsync、scp “tab” 卡顿问题

244 篇文章 11 订阅
在使用rsync或scp时遇到Tab补全卡顿问题,原因可能是bash-completion的补全脚本导致的。通过卸载bash-completion可以解决问题,但会失去命令补全功能。解决方案是修改rsync和scp的补全脚本,注释掉获取远程主机文件列表的代码,或者调整脚本使其在目标主机后使用Tab补全本地文件路径。
摘要由CSDN通过智能技术生成

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值