7

我正在尝试在 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 scene/shot /元素/工作区/ ...)。我可以继续修改我当前的脚本——但我一直想知道是否有什么我不理解的东西——这是我第一次尝试完成程序,并且在 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 "/"}'

重击示例:

_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
4

5 回答 5

4

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

_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/您可以使用withcompgen而不是awk命令添加斜杠
  • 您可以使用$cur代替,$terms因为您不必去除空格,但我正在使用$lastwordand $new(两个新变量)
  • 没有必要使用xargs echo,因为带有换行符的数组可以正常工作
  • 我没有使用包含空格或换行符的目录名称对此进行测试
于 2009-10-08T07:27:34.643 回答
1

我的解决方案是一个 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;
}
于 2010-02-10T22:27:21.130 回答
1

所以,这是一个 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"
于 2017-06-08T19:41:00.383 回答
0

基本tcsh解决方案很容易实现,如下所示:

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

不需要bash脚本依赖。当然,在此示例中,基本目录是硬连线的。

于 2012-05-17T20:50:09.967 回答
0

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

于 2009-10-07T03:13:42.763 回答