5

I'm trying to write a bash completion that will let me complete directory names that other shells are in.

For example, suppose I have another shell open in /very/long/path/name, and I'm currently in a directory that contains subdirs foo and bar. When I type cd <Tab>, I want to see:

$ cd <Tab>
foo/  bar/  /very/long/path/name

I have this command to produce the list of potential completions:

ps -Cbash -opid= | xargs pwdx | cut -d" " -f2 | sort -u | while read; do echo ${REPLY#$PWD/}; done | grep -v "^$"

For brevity, I'll write this as ...pipeline....

On my system there's a _cd function that produces the regular completion:

$ complete -p cd
complete -o nospace -F _cd cd

I would like to reuse this _cd function, because it's nontrivial (~30 lines of code, according to type _cd). Bonus points if the solution reuses whatever completion is already defined, whether or not it's based on a function called _cd.

I thought the -C option to complete sounded promising, but I can't get it to work:

$ complete -C '...pipeline...' cd
$ cd <Tab>grep: cd: No such file or directory
grep: : No such file or directory
grep: cd: No such file or directory

Writing my own wrapper function for -F, which appends to the COMPREPLY array, also didn't quite work:

$ function _cd2() { _cd; COMPREPLY=( ${COMPREPLY[@]} $(...pipeline...) ); }
$ cd <Tab>
foo/  bar/  name/

It strips off all the path components except the last. I figure it must be something set up by _cd that does this, but I'm not sure how to counteract it.

If I remove the _cd call from _cd2, I do see the completions, but they don't properly complete partial directory names. If I type cd /ve<Tab>, it still shows the full path, without actually completing my command line.

How can I get this to do what I want?


Appendix: the full definition of _cd:

$ type _cd
_cd is a function
_cd () 
{ 
    local cur prev words cword;
    _init_completion || return;
    local IFS='
' i j k;
    compopt -o filenames;
    if [[ -z "${CDPATH:-}" || "$cur" == ?(.)?(.)/* ]]; then
        _filedir -d;
        return 0;
    fi;
    local -r mark_dirs=$(_rl_enabled mark-directories && echo y);
    local -r mark_symdirs=$(_rl_enabled mark-symlinked-directories && echo y);
    for i in ${CDPATH//:/'
'};
    do
        k="${#COMPREPLY[@]}";
        for j in $( compgen -d $i/$cur );
        do
            if [[ ( -n $mark_symdirs && -h $j || -n $mark_dirs && ! -h $j ) && ! -d ${j#$i/} ]]; then
                j+="/";
            fi;
            COMPREPLY[k++]=${j#$i/};
        done;
    done;
    _filedir -d;
    if [[ ${#COMPREPLY[@]} -eq 1 ]]; then
        i=${COMPREPLY[0]};
        if [[ "$i" == "$cur" && $i != "*/" ]]; then
            COMPREPLY[0]="${i}/";
        fi;
    fi;
    return 0
}
Thomas
  • 174,939
  • 50
  • 355
  • 478
  • Looks like your custom `_cd` calls `_init_completion`, `_filedir` and maybe some other functions: what are those functions' definitions? – bishop Oct 24 '14 at 15:52
  • Oof, they each call into a handful of yet other functions... I'm on a Ubuntu system, if that helps, so if you have access to one you should be able to dig deeper. – Thomas Oct 24 '14 at 15:54
  • Sadly, no Ubuntu for me. CentOS, Gentoo, Arch, Slack... – bishop Oct 24 '14 at 16:09

1 Answers1

1

You need to evaluate the current match against the list of combined options. Here's a test script that illustrates the moving parts:

#!/bin/bash

mkdir -p {my,other}/path/to/{a,b,c}

function _cd() {
    COMPREPLY=( my/path/to/a my/path/to/b );
}
complete -o nospace -F _cd cd

function _cd2() {
    local cur opts;
    cur="${COMP_WORDS[COMP_CWORD]}";
    _cd;
    opts="${COMPREPLY[@]} other/path/to/c";        # here we combine options
    COMPREPLY=($(compgen -W "${opts}" -- ${cur})); # here is the secret sauce
}
complete -F _cd2 cd

complete -p cd

The most important points are in the compgen portion of _cd2: that chooses the most suitable options from the combined set of choices (in $opts).

bishop
  • 37,830
  • 11
  • 104
  • 139
  • Huh, even which such a trivial `_cd` function? I hadn't expected that. Interesting indeed... – Thomas Oct 24 '14 at 15:35
  • @Thomas: Yep. I think I've figured it out. Refresh and checkout my edit. – bishop Oct 24 '14 at 15:37
  • That's great, it almost works! However, if I substitute the real `_cd`, it doesn't show my nice paths anymore, just the stock ones. Added the definition of `_cd` to the question. – Thomas Oct 24 '14 at 15:50
  • Ah, `compopt +o filenames` undoes that effect. But now I've run into a wall: for `/v`, `/very/long/path/name` and `/very` are both valid completions, so it only completes `/very`, defeating the purpose. Will need to think more about how I want this to work... – Thomas Oct 24 '14 at 15:53
  • 1
    @Thomas, I'd consider using `//` for your usage-based prefixes, and `/` otherwise -- providing a clean way to distinguish. Granted, `compopt filenames` may still try to complete the former, but it gives you a clear place to start in distinguishing user intent. – Charles Duffy Oct 24 '14 at 16:07
  • That's a good idea. I ended up just writing a `function ocd() { cd "@$"; }` and using that instead. The good thing about that is that I now have a command called `ocd` :) – Thomas Oct 25 '14 at 15:20