7

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 ?

  • Per Matthew's answer below, local IFS=$'\n' seems to be what you're looking for. It's what worked for me. – Edward Falk Jun 27 '16 at 22:09
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.

  • 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 ! – Pierre Mourlanne May 25 '13 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 May 25 '13 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 May 25 '13 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)

  • 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. – Pierre Mourlanne May 25 '13 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 May 25 '13 at 11:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.