Question

I'm working on a bash completion script for a dot file management utility. The tool has a command dots diff [filename] that will show the difference between the installed dot file and the source dot file. It also has a command dots files which lists the paths of all managed dot files (relative to the source directory). I would like to complete the diff command with the output of the files command.

Here's an example of the files output

X11/xkb/symbols/evan-custom
X11/xorg.conf.d/10-dual-monitors.conf
X11/xorg.conf.d/10-keylayout.conf
bash/aliases
bash/bashrc
bash/completion.d/dots
bash/profiles/standard-user
bash/profiles/systemd-user
bspwm/bspwmrc
compton/compton.conf
fontconfig/fonts.conf
git/config
git/ignore
gtk-2.0/gtkrc
gtk-3.0/settings.ini
mysql/config
mysql/grcat
ncmpcpp/config
pulse/client.conf
pulse/daemon.conf
pulse/default.pa
ssh/config
sublime-text-3/Packages/User/Preferences.sublime-settings
sxhkd/sxhkdrc
termite/config
transmission-daemon/settings.json
vim/vimrc

Using something like this

COMPREPLY=( $(compgen -W "$(dots files)" -- $cur) )

Works, however when readline lists the available options it lists out the full paths (The list above).

I would like for it to treat the words as if they were file paths and when listing suggestions only list up to the first forward slash.

For example, if I typed dots diff [tab][tab] the following should be printed

X11/
bash/
bspwm/
compton/
fontconfig/
git/
gtk-2.0/
gtk-3.0/
mysql/
ncmpcpp/
pulse/
ssh/
sublime-text-3/
sxhkd/
termite/
transmission-daemon/
vim/

If for example I then typed dots diff bash/[tab][tab] then it would show

aliases
bashrc
completion.d/
profiles/

Ideally I would like it to actually treat it as a path so that changing the readline option mark-directories to off would exclude the trailing slashes.

I've tried setting compopt -o filenames but this instead gives suggestions for the file names, instead of the paths initially.

Here is the completion script I have so far

Was it helpful?

Solution

I've solved this.

The trick was to use compopt -o filename and then slice off the portion of the path being completed that is a sub-directory of the directory being completed.

Here's the code

# Do completion from a passed list of paths
#
# Accepts 2 arguments
# 1. The list of paths to complete from
# 2. The current word being completed
__dots_path_comp()
{
    # This forces readline to only display the last item separated by a slash
    compopt -o filenames

    local IFS=$'\n'
    local k="${#COMPREPLY[@]}"

    for path in $(compgen -W "$1" -- $2)
    do
        local trailing_trim

        # Determine what to trim from the end
        trailing_trim="${path#${2%/*}/}/"
        trailing_trim="${trailing_trim#*/}"
        trailing_trim="${trailing_trim%/}"

        # Don't add a space if there is more to complete
        [[ "$trailing_trim" != "" ]] && compopt -o nospace

        # Remove the slash if mark-directories is off
        if ! _rl_enabled mark-directories
        then
            # If The current typed path doesnt have a slash in it yet check if
            # it is the full first portion of a path and ignore everything after
            # if it is. We don't have to do this once the typed path has a slash
            # in it as the logic above will pick up on it
            [[ "$2" != */* && "$path" == ${2}/* ]] && path="$2/$trailing_trim"    

            trailing_trim="/$trailing_trim"
        fi

        COMPREPLY[k++]="${path%%${trailing_trim}}"
    done
}

OTHER TIPS

A dirty hack would be to replace your $(dots files) with

$(dots files | sed 's,/.*/,,' | uniq)

so that the full thing would read as

COMPREPLY=( $(compgen -W "$(dots files | sed 's,/.*/,,' | uniq)" -- "$cur") )

Now I personally don't like this approach very much. Instead, could you modify your script to accept, e.g., a basedirs statement that will output just what's needed?

Besides, you should be aware that the completion will not work as expected if there are spaces or other funny symbols in the names.

I think you can get what you need with complete's -F argument. From the docs:

Any function specified with -F is invoked first. The function may use any of the shell facilities, including the compgen and compopt builtins described below (see Programmable Completion Builtins), to generate the matches. It must put the possible completions in the COMPREPLY array variable.

So, something like this to set it up:

complete -F _dots_complete dots

Then your function:

function _dots_complete {
    # your filtering logic to calculate the results array
    COMPREPLY=( "${results[@]}" )
}

In your function $1 is position of current word, $2 .. $n are the words themselves.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top