9

A bit of context, this question is a follow-up of this one: Bash remote autocompletion : change the 'starting' directory

Anyway, I'm writing my custom autocompletion bash script: I want the autocompletion to work just like cd does, except that I want to get the names from a specific directory, not necessarily the current one. It works great except when filenames have spaces in them.

A quick example. Let's say the directory I'm getting the names from has two files in it: a_file and another file (notice the space). This happens:

my_commandTABTAB
a_file file another

The presentation isn't perfect but the idea is that I'm prompted with 3 choices, another file being split in another and file. The desired output would be: file_1 another file. I would also like spaces to be escaped automatically:

my_command anoTAB
my_command another\ file

Here's what my script looks like:

#!/bin/bash

_get_file_list()
{
    dir="/some/path/"
    cd $dir
    find * -maxdepth 0
}

_GetOptMyCommand()
{
    local cur

    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}

    case "$cur" in
    -*)
        COMPREPLY=( $( compgen -W "-h -l --help --list --" -- "$cur" ) );;
    *)
        COMPREPLY=( $( compgen -W "$(_get_file_list)" -- "$cur" ) );;
    esac

    return 0
}

complete -F _GetOptMyCommand my_command

How do I deal with spaces in filenames and make my autocompletion script like cd ?

1
  • 1
    Per Matthew's answer below, local IFS=$'\n' seems to be what you're looking for. It's what worked for me. Commented Jun 27, 2016 at 22:09

3 Answers 3

4

Believe it might be better to use compgen instead of find in this case.

You probably already have a completion script with system. Try e.g.

locate bash_completion

On Debian variants this is probably:

/usr/share/bash-completion/bash_completion

where you find e.g. _filedir. So the simplest way then would be something in the direction of:

*)
    pushd "/some/path" >/dev/null
    _filedir
    popd >/dev/null

If that is not an option this could be a starter:

_comp_by_path()
{
    local opt cur dir
    local IFS=$'\n' x tmp
    local -a tokens

    opt="$1"
    cur="$2"
    dir="$3"

    # Enter target directory
    pushd "$dir" >/dev/null

    # Get directories, filtered against current
    [[ "$opt" != "-f" ]] && \
    x=$( compgen -d -- "$cur" ) &&
    while read -r tmp; do
        tokens+=( "$tmp" )
    done <<< "$x"

    # Get files, filtered against current
    [[ "$opt" != "-d" ]] && \
    x=$( compgen -f -- "$cur" ) &&
    while read -r tmp; do
        tokens+=( "$tmp" )
    done <<< "$x"

    # If anything found
    if [[ ${#tokens[@]} -ne 0 ]]; then
        # Make sure escaping is OK
        compopt -o filenames 2>/dev/null
        COMPREPLY+=( "${tokens[@]}" )
    fi

    # Go back
    popd >/dev/null
}

_GetOptMyCommand()
{
    local cur

    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"

    case "$cur" in
    -*)
        COMPREPLY=( $( compgen -W "-h -l --help --list --" -- "$cur" ) );;
    *)
        _comp_by_path "any" "$cur" "/some/path"
    esac
}

complete -F _GetOptMyCommand my_command

A variant using find could be something in direction of this:

_zaso()
{
    local dir="$1"

    pushd "$dir" >/dev/null
    find * -maxdepth 0 2>/dev/null
    popd >/dev/null
}

_comp_with_find()
{
    local cur dir      
    local IFS=$'\n'

    cur="$1"
    dir="$2"

    compopt -o filenames 2>/dev/null
    COMPREPLY=( $( compgen -W "$(_zaso "$dir")" -- "$cur" ) );
}

Also note that printf in Bash has a %q option. So to generate quoted strings this is an option to play with:

find * -maxdepth 0 2>/dev/null && \
while read -r tmp; do
    printf "%q\n" "$tmp"
done <<< "$x"

Also not that file names can have newline characters in which a lot of this will break. Have not found a way to use \0 with compgen.

3
  • Thanks for the location of the built-in autocomplete, I was actually wondering where it was stored. Once I get my stuff working I'll post the details, thanks again ! Commented May 25, 2013 at 10:45
  • @PierreMourlanne: Yes, there is a lot to learn from reading those scripts. Also look in (probably completions/) subdirectory of where bash_completion reside for command specific completion scripts. There should be quite a long nice list with scripts.
    – Runium
    Commented May 25, 2013 at 10:57
  • Also. I did not think of that on your previous question, but compgen can also be used on your ssh server.
    – Runium
    Commented May 25, 2013 at 11:00
3

I encountered the same problem recently. I was able to make things work properly by changing the IFS variable with local IFS=$'\n' and then using arrays. Try using this:

_GetOptMyCommand(){
    # Backup old nullglob setting
    local shoptbakup="`shopt -p nullglob`"
    shopt -s nullglob

    local cur opts i opt
    local IFS=$'\n'
    cur="${COMP_WORDS[COMP_CWORD]}"

    case "$cur" in
    -*)
        opts=("-h" "-l" "--help" "--list");;
    *)
        # Get Files
        opts=(${cur}*)
    esac

    COMPREPLY=( $( compgen -W "${opts[*]}" -- "$cur" ) )

    # Restore nullglob setting
    eval "$shoptbakup" 2>/dev/null

    return 0
}
complete -o filenames -o bashdefault -F _GetOptMyCommand my_command
0

Try piping the find output through sed 's/ /\\ /'.

Note however that other characters(quotes, $, &.. all the usual suspects) can also trip you up.

sed 's/\([ $&!#*()<>|{}[?`"'"'"']\)/\\\1/'

may be better, as it will escape most "special" characters.

(I haven't figured out how to correctly escape \ yet, though)

2
  • I already tried this kind of things and the output of find does get properly formatted with the escaped space (another\ file). The problem probably lies afterwards, in the compgen -W "$(_get_file_list)" because I still get prompted with the three choices file, another and a_file. Commented May 25, 2013 at 10:12
  • I understood that problem but didn't know how to fix it. You need "$@" style expansion. I just don't know how to get "$@" style expansion.
    – kampu
    Commented May 25, 2013 at 11:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .