【问题标题】:Tcsh and/or bash directory completion with variable hidden root prefix带有可变隐藏根前缀的 Tcsh 和/或 bash 目录完成
【发布时间】:2009-10-07 02:49:59
【问题描述】:

我正在尝试在 tcsh 和/或 bash(两者都在我的站点上使用)中设置目录完成,但稍有改变:对于特定命令“foo”,我希望完成使用自定义函数将第一个 / 分隔的术语与实际的子树节点匹配,然后对任何后续术语遵循正常的目录完成。它是 cdpath 和完成的组合,或者我认为是目录完成的一种形式,其中起点由完成脚本控制。它的工作原理如下:

$ foo xxx<TAB>
(custom completion function produces choices it finds at arbitrary levels in the dir tree)
xxxYYY xxxZZZ xxxBLAH ...
foo xxxYYY/<TAB>
(normal directory completion proceeds from this point on, to produce something like:)
foo scene/shot/element/workspace/user/...

我们愿意这样做是因为我们有一个大型生产开发树(这是一个 CGI 生产设施),精通 shell 的用户一直在导航和跳转。抱怨是树的上层繁琐且多余;他们只需要快速搜索第一个术语即可找到可能的“头”选择并从那里完成目录完成。看起来可编程完成可以提供一种方法来做到这一点,但事实证明它非常难以捉摸。

我已经多次尝试自定义 bash 和 tcsh 补全来做到这一点,但我得到的最接近的是一种“单词补全”形式,用户必须将目录级别视为带有空格的单独单词(例如 foo场景/镜头/元素/工作区/ ...)。我可以继续修改我当前的脚本——但我一直想知道是否有一些我不理解的东西——这是我第一次尝试完成程序,并且在 shell 书籍和互联网上的文档和示例非常薄.如果有任何完成大师可以让我走上正轨,我将不胜感激。

FWIW:这是我到目前为止所得到的(首先是 tcsh,然后是 bash)。请注意,静态根 '/root/sub1/sub2/sub3' 只是搜索功能的占位符,它将在不同级别找到不同的匹配项。如果我能让它工作,我可以稍后加入搜索功能。同样,这两个示例都进行了单词补全,这要求用户在每个匹配项之后输入一个空格(我还必须删除函数中的空格来构造一个实际的路径,哎呀!)

TCSH 示例(注意该函数实际上是一个 bash 脚本):

complete complete_p2 'C@*@`./complete.p2.list.bash $:1 $:2 $:3 $:4 $:5 $:6 $:7 $:8 $:9`@@'

#!/bin/bash --norc

# complete.p2.list.bash - Completion prototype "p2" for shotc command

# Remove spaces from input arguments
ppath=`echo $@ | sed -e 's/ //g'`

# Print basenames (with trailing slashes) of matching dirs for completion
ls -1 -d /root/sub1/sub2/sub3/$ppath* 2>/dev/null | sed -e 's#^.*/##' | awk '{print $1 "/"}'

BASH 示例:

_foo() 
{
    local cur prev opts flist
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    # Get all command words so far (omit command [0] element itself), remove spaces
    terms=`echo ${COMP_WORDS[@]:1} | sed -e 's/ //g'`

    # Get basenames (with trailing slashes) of matching dirs for completion
    flist=`ls -1 -d /root/sub1/sub2/sub3/${terms}* 2>/dev/null | sed -e 's#^.*/##' | awk '{print $1 "/"}' | xargs echo`

    COMPREPLY=( $(compgen -W "${flist}" ${cur}) )
    return 0
}
complete -F _foo foo

【问题讨论】:

  • bash 的 CDPATH 环境变量对你有用吗?
  • 我查看了 CDPATH,但它无法完成。你可以“cd name”,但不能“cd name ...”我真的需要补全。
  • 我认为 bash 补全(在大多数发行版上随 bash 一起提供的包含有用的额外脚本)包括使用 CDPATH 对 cd 补全的更新定义。
  • @Jefromi:你说得对,它似乎能够做我认为 Jeremy 想要做的事情。
  • 感谢您的提示,我会检查一下。然而值得一提的是,我需要做的不仅仅是 cd;例如,即时设置特定于子树的环境。我正计划使用自定义命令,但也许我可以将 cd 别名为其他东西(不过这可能太侵入性了)。

标签: bash tcsh tab-completion bash-completion


【解决方案1】:

这似乎可以满足您的需求:

_foo()
{
    local cur prev opts flist lastword new
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    lastword="${COMP_WORDS[@]: -1}"

    if [[ $lastword =~ / ]]
    then
        new="${lastword##*/}"      # get the part after the slash
        lastword="${lastword%/*}"  # and the part before it
    else
        new="${lastword}"
        lastword=""
    fi

    flist=$( command find /root/sub1/sub2/sub3/$lastword \
      -maxdepth 1 -mindepth 1 -type d -name "${new}*" \
      -printf "%f\n" 2>/dev/null )

    # if we've built up a path, prefix it to 
    #   the proposed completions: ${var:+val}
    COMPREPLY=( $(compgen ${lastword:+-P"${lastword}/"} \
      -S/ -W "${flist}" -- ${cur##*/}) )
    return 0
}
complete -F _foo -o nospace foo

注意事项:

  • 我认为其中一个关键是nospace 选项
  • 我觉得我在上面函数的某个地方重新发明了一个轮子,可能是不使用$COMP_POINT
  • 您(至少)还没有使用$prev(它始终在我的函数中保持值“foo”)
  • 使用$() 代替反引号可以提高可读性和功能性
  • 您应该使用command 来防止运行别名等:flist=$(command ls -1 -d...
  • 我使用 find 而不是 ls,因为它更适合
  • 您可以使用-S/compgen 代替awk 命令添加斜线
  • 您可以使用$cur 而不是$terms,因为您不必去除空格,但我使用的是$lastword$new(两个新变量)
  • 没有必要使用xargs echo,因为带有换行符的数组可以正常工作
  • 我没有使用包含空格或换行符的目录名称对此进行测试

【讨论】:

  • 谢谢丹尼斯!这与我正在寻找的非常接近。我可以通过一些调整来使用它,或者甚至可以使用它。我会看看我是否也可以对 tcsh 应用类似的方法。
  • 我以为我很孤单,需要复杂的完成脚本 :) +1 代码工作!
【解决方案2】:

我的解决方案是一个 800 磅重的锤子,我的解决方案是编写一个 perl 脚本来按照我想要的方式处理完成。在 tcsh...

complete cd 'p|1|`complete_dirs.pl $:1 $cdpath`|/'

#!/usr/bin/perl

my $pattern = shift @ARGV;
my @path = @ARGV;
my @list;

if ($pattern =~ m!^(/.+/|/)(.*)!) {
  @list = &search_dir($1,$2,undef);
} elsif ($pattern =~ m!(.+/|)(.*)!) {
  my $dir; foreach $dir ('.',@path) {
    push(@list,&search_dir("$dir/$1",$2,$1));
  }
}
if (@list) {
  @list = map { &quote_path($_) } @list;
  print join(' ',@list), "\n";
}

sub search_dir {
  my ($dir,$pattern,$prefix) = @_;
  my @list;

  if (opendir(D,$dir)) {
    my $node; while ($node = readdir(D)) {
      next     if ($node =~ /^\./);
      next unless ($node =~ /^$pattern/);
      next unless (-d "$dir$node");

      my $actual; if (defined $prefix) {
        $actual = "$prefix$node";
      } elsif ($dir =~ m!/$!) {
        $actual = "$dir$node";
      } else {
        $actual = "$dir/$node";
      }
      push(@list,$actual);
    }
    closedir(D);
  }
  return @list;
}
sub quote_path {
  my ($string) = @_;

  $string =~ s!(\s)!\\$1!g;
  return $string;
}

【讨论】:

    【解决方案3】:

    所以,这里有一个 tcsh 解决方案。添加完整的 $cdpath 搜索自动完成目录。完整的命令是:

    complete cd 'C@/@d@''p@1@`source $HOME/bin/mycdpathcomplete.csh $:1`@/@'
    

    我有点 tcsh hack,所以字符串操作有点粗糙。但它的开销可以忽略不计...... mycdpathcomplete.csh 看起来像这样:

    #!/bin/tcsh -f
    
    set psuf=""
    set tail=""
    
    if ( $1 !~ "*/*" ) then
        set tail="/$1"
    else
        set argsplit=(`echo "$1" | sed -r "s@(.*)(/[^/]*\')@\1 \2@"`)
        set psuf=$argsplit[1]
        set tail=$argsplit[2]
    endif
    
    set mycdpath=(. $cdpath)
    set mod_cdpath=($mycdpath)
    
    if ($psuf !~ "") then
        foreach i (`seq 1 1 $#mycdpath`)
            set mod_cdpath[$i]="$mycdpath[$i]/$psuf"
            if ( ! -d $mod_cdpath[$i] ) then
                set mod_cdpath[$i]=""
            endif
        end
    endif
    
    # debug statements
    #echo "mycdpath = $mycdpath"
    #echo "mod_cdpath = $mod_cdpath"
    #echo "tail = $tail"
    #echo "psuf = $psuf"
    
    set matches=(`find -L $mod_cdpath -maxdepth 1 -type d -regex ".*${tail}[^/]*" | sed -r "s@.*(/?${psuf}${tail}[^/]*)\'@\1@" | sort -u`)
    
    # prune self matches
    if ($psuf =~ "") then
        foreach match (`seq 1 1 $#matches`)
            set found=0
            foreach cdp ($mod_cdpath)
                if ( -e "${cdp}${matches[$match]}" ) then
                    set found=1;
                    break;
                endif
            end
            if ( $found == 0 ) then
                set matches[$match]=""
            else
                set matches[$match]=`echo "$matches[$match]" | sed -r "s@^/@@"`
            endif
        end
    endif
    
    echo "$matches"
    

    【讨论】:

      【解决方案4】:

      你可以创建一个符号链接到树中第一个有趣的节点。过去,当我无法自动完成大型目录树时,我已经这样做了。

      【讨论】:

      • 不幸的是,“第一个有趣的节点”可能会有很大差异。由于用户在树的不同部分花费时间并在子树周围跳跃,因此无法预测他们可能会锚定哪些级别的完成尝试。除非我们链接每个级别(看起来很麻烦且笨拙),否则我不确定链接能否解决它。
      【解决方案5】:

      基本的tcsh 解决方案很容易实现如下:

          complete foo 'C@*@D:/root/sub1/sub2/sub3@'
      

      不需要bash 脚本依赖项。当然,在这个例子中基本目录是硬连线的。

      【讨论】:

        猜你喜欢
        • 2014-07-11
        • 1970-01-01
        • 1970-01-01
        • 2021-08-26
        • 2015-12-25
        • 2014-02-12
        • 2013-07-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多