使用可变隐藏根前缀完成 Tcsh 和/或 bash 目录

发布于 2024-08-07 03:02:00 字数 2152 浏览 13 评论 0原文

我正在尝试在 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 "/"}'

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

I'm trying to set up directory completion in tcsh and/or bash (both are used at my site) with a slight twist: for a particular command "foo", I'd like to have completion use a custom function to match the first /-delimited term to an actual subtree node, and then follow normal directory completion for any successive terms. It is sort of a combination of cdpath and completion, or I suppose a form of directory completion where the starting point is controlled by the completion script. It would work as follows:

$ 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/...

We'd like to do this because we have a large production development tree (this is a CGI production facility), that shell-savvy users are navigating and jumping around in all the time. The complaint is that the upper levels of the tree are cumbersome and redundant; they just need a quick search on the first term to find possible "head" choices and do directory completion from there. It seems like programmable completion could offer a way to do this, but it is turning out to be pretty elusive.

I've made several attempts of custom bash and tcsh completion to do this, but the closest I've gotten is a form of "word completion" where the user must treat the directory levels as separate words with spaces (e.g. foo scene/ shot/ element/ workspace/ ...). I could continue hacking at my current scripts--but I've been wondering if there's something I'm not understanding--this is my first attempt to program completion, and the docs and examples are pretty thin in shell books and on the internet. If there's any completion-guru's out there that can get me on the right track, I'd appreciate it.

FWIW: here is what I've got so far (in tcsh first, then bash). Note that the static root '/root/sub1/sub2/sub3' is just a placeholder for a search function that would find different matches in different levels. If I can get that to work, I can sub in the search feature later. Again, both examples do word completion, which requires user to type a space after each matching term (I also have to remove the spaces in the function to construct an actual path, yuck!)

TCSH EXAMPLE (note the function is actually a bash script):

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 EXAMPLE:

_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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

伏妖词 2024-08-14 03:02:00

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

_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< 添加斜线/code> 而不是 awk 命令
  • 您可以使用 $cur 而不是 $terms 因为您不必删除空格,但是我正在使用 $lastword$new (两个新变量)
  • 没有必要使用 xargs echo 因为带有换行符的数组工作正常
  • 我尚未使用其中包含空格或换行符的目录名称对此进行测试

This seems like it might do what you're looking for:

_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

Notes:

  • I think one of the keys is the nospace option
  • I feel like I've reinvented a wheel somewhere in the function above, possibly by not using $COMP_POINT
  • You're not (yet, at least) using $prev (which always maintains the value "foo" in my function)
  • Readability and functionality can be improved by using $() instead of backticks
  • You should use command to prevent running aliases and such: flist=$(command ls -1 -d...
  • I'm using find instead of ls because it's better suited
  • You can add the slash using -S/ with compgen instead of your awk command
  • You can use $cur instead of $terms since you don't have to strip out spaces, but I'm using $lastword and $new (two new variables)
  • It's not necessary to use xargs echo since an array with newlines works fine
  • I have not tested this with directory names having spaces or newlines in them
離殇 2024-08-14 03:02:00

我的解决方案(无疑是一把 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 { "e_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;
}

my solution, which is admittedly an 800-lb hammer, was to write a perl script to handle the completion the way i wanted it to. in 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 { "e_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;
}
浅沫记忆 2024-08-14 03:02:00

所以,这是一个 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"

So, here is a tcsh solution. Adds full $cdpath searching for autocompleting directories. The complete command is:

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

I am a bit of a tcsh hack, so the string manipulation is a bit crude. But it works with negligible overhead... mycdpathcomplete.csh looks like this:

#!/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"
不…忘初心 2024-08-14 03:02:00

您可以创建指向树中第一个有趣节点的符号链接。过去,当我懒得自动完成大型目录树时,我就这样做过。

You could just make a symlink to the first interesting node in the tree. I've done this in the past when I couldn't be bothered auto-completing large directory trees.

豆芽 2024-08-14 03:02:00

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

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

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

The basic tcsh solution is very easy to implement as follows:

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

There is no bash script dependency required. Of course the base directory is hardwired in this example.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文