Frage

Was ist die richtige / beste Art und Weise des Umgangs mit Leerzeichen und Quotes in Bash-Abschluss?

Hier ist ein einfaches Beispiel. Ich habe einen Befehl namens words (zum Beispiel ein Wörterbuch-Lookup-Programm), die verschiedene Wörter als Argumente nimmt. Die unterstützten ‚Worte‘ können tatsächlich enthalten Leerzeichen und werden in einer Datei mit dem Namen words.dat definiert:

foo
bar one
bar two

Hier ist meine erste vorgeschlagene Lösung:

_find_words()
{
search="$cur"
grep -- "^$search" words.dat
}

_words_complete()
{
local IFS=$'\n'

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

COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

}
complete -F _words_complete words

Typing ‘words f<tab>’ vervollständigt den Befehl korrekt zu ‘words foo ’ (mit einem Leerzeichen am Ende), das ist schön, aber für ‘words b<tab>’ es schlägt ‘words bar ’. Die korrekte Fertigstellung würde ‘words bar\ ’. Und für ‘words "b<tab>’ und ‘words 'b<tab>’ es keine Vorschläge bietet.

Dieser letzte Teil ich in der Lage gewesen, zu lösen. Es ist möglich, eval zu verwenden, um richtig die (entkommen) Zeichen zu analysieren. Allerdings eval nicht gern fehlende Anführungszeichen, also alles, was zur Arbeit zu kommen, ich hatte die search="$cur" zu

ändern
search=$(eval echo "$cur" 2>/dev/null ||
eval echo "$cur'" 2>/dev/null ||
eval echo "$cur\"" 2>/dev/null || "")

Das tatsächlich funktioniert. Sowohl ‘words "b<tab>’ und ‘words 'b<tab>’ richtig autocompletes, und wenn ich eine ‘o’ und drücken <tab> wieder hinzufügen, die es vervollständigt tatsächlich das Wort und fügt den korrekten Schlusskurs. Allerdings, wenn ich versuche ‘words b<tab>’ oder sogar ‘words bar\ <tab>’ zu vervollständigen, wird es automatisch vervollständigt statt ‘words bar ’ ‘words bar\ ’ und das Hinzufügen von zum Beispiel ‘one’ würde fehlschlagen, wenn das words Programm ausgeführt wird.

Nun, offensichtlich ist es ist möglich, dies richtig zu handhaben. Zum Beispiel kann der ls Befehl tut es für Dateien namned ‘foo’ ‘bar one’ und ‘bar two’ (obwohl es Probleme mit einigen Ausdrucksweisen der Dateinamen hat, wenn man eine (gültige) Kombination von ", ' und verschiedenen Fluchten verwendet). Allerdings kann ich nicht herausfinden, wie ls tut es durch den Bash-Abschluss-Code zu lesen.

Also, weiß jemand, wie diese richtig umgehen? Die tatsächliche Eingang Anführungszeichen müssen nicht beibehalten werden; Ich würde mit einer Lösung zufrieden sein, die ‘words "b<tab>’, ‘words 'b<tab>’ und ‘words b<tab>’ zu ‘words bar\ ’ ändert, zum Beispiel, (obwohl ich es vorziehen würde von Zitaten Strippen, wie in diesem Beispiel, statt mit ihnen).

War es hilfreich?

Lösung

Das ist nicht zu elegant Nachbearbeitungs Lösung scheint für mich (GNU bash, Version 3.1.17 (6) -release (i686-pc-Cygwin)) zu arbeiten. (Es sei denn, ich habe nicht eine gewisse Grenze Fall wie üblich testen:))

Sie müssen die Dinge nicht eval, gibt es nur zwei Arten von Anführungszeichen.

Da compgen nicht will Räume für uns entkommen, werden wir sie uns entkommen (nur wenn Wort mit einem Zitat beginnen nicht). Dies hat eine Nebenwirkung der vollständigen Liste (auf Doppel tab) Werte als auch entronnen. Nicht sicher, ob das gut ist oder nicht, da ls es nicht tun ...

EDIT: Fest Einzel- und Doppel qoutes in den Worten zu handhaben. Im Wesentlichen müssen wir 3 unescapings passieren :). Zunächst für grep, Sekunde für compgen, und zuletzt für Worte Befehl selbst wenn die automatische Vervollständigung durchgeführt wird.

_find_words()
{
    search=$(eval echo "$cur" 2>/dev/null || eval echo "$cur'" 2>/dev/null || eval echo "$cur\"" 2>/dev/null || "")
    grep -- "^$search" words.dat | sed -e "{" -e 's#\\#\\\\#g' -e "s#'#\\\'#g" -e 's#"#\\\"#g' -e "}"
}

_words_complete()
{
    local IFS=$'\n'

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

    COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

    local escaped_single_qoute="'\''"
    local i=0
    for entry in ${COMPREPLY[*]}
    do
        if [[ "${cur:0:1}" == "'" ]] 
        then
            # started with single quote, escaping only other single quotes
            # [']bla'bla"bla\bla bla --> [']bla'\''bla"bla\bla bla
            COMPREPLY[$i]="${entry//\'/${escaped_single_qoute}}" 
        elif [[ "${cur:0:1}" == "\"" ]] 
        then
            # started with double quote, escaping all double quotes and all backslashes
            # ["]bla'bla"bla\bla bla --> ["]bla'bla\"bla\\bla bla
            entry="${entry//\\/\\\\}" 
            COMPREPLY[$i]="${entry//\"/\\\"}" 
        else 
            # no quotes in front, escaping _everything_
            # [ ]bla'bla"bla\bla bla --> [ ]bla\'bla\"bla\\bla\ bla
            entry="${entry//\\/\\\\}" 
            entry="${entry//\'/\'}" 
            entry="${entry//\"/\\\"}" 
            COMPREPLY[$i]="${entry// /\\ }"
        fi
        (( i++ ))
    done
}

Andere Tipps

The question is rather loaded but this answer attempts to explain each aspect:

  1. How to handle spaces with COMPREPLY.
  2. How does ls do it.

There're also people reaching this question wanting to know how to implement the completion function in general. So:

  1. How how do I implement the completion function and correctly set COMPREPLY?

How does ls do it

Moreover, why does it behave differently to when I set COMPREPLY?

Back in '12 (before I updated this answer), I was in a similar situation and searched high and low for the answer to this discrepancy myself. Here's the answer I came up with.

ls, or rather, the default completion routine does it using the -o filenames functionality. This option performs: filename-specific processing (like adding a slash to directory names or suppressing trailing spaces.

To demonstrate:

$ foo () { COMPREPLY=("bar one" "bar two"); }
$ complete -o filenames -F foo words
$ words ░

Tab

$ words bar\ ░          # Ex.1: notice the space is completed escaped

TabTab

bar one  bar two        # Ex.2: notice the spaces are displayed unescaped
$ words bar\ ░

Immediately there are two points I want to make clear to avoid any confusion:

  • First of all, your completion function cannot be implemented simply by setting COMPREPLY to an array of your word list! The example above is hard-coded to return candidates starting with b-a-r just to show what happens when TabTab is pressed. (Don't worry, we'll get to a more general implementation shortly.)

  • Second, the above format for COMPREPLY only works because -o filenames is specified. For an explanation of how to set COMPREPLY when not using -o filenames, look no further than the next heading.

Also note, there's a downside of using -o filenames: If there's a directory lying about with the same name as the matching word, the completed word automatically gets an arbitrary slash attached to the end. (e.g. bar\ one/)

How to handle spaces with COMPREPLY without using -o filenames

Long story short, it needs to be escaped.

In contrast to the above -o filenames demo:

$ foo () { COMPREPLY=("bar\ one" "bar\ two"); }     # Notice the blackslashes I've added
$ complete -F foo words                             # Notice the lack of -o filenames
$ words ░

Tab

$ words bar\ ░          # Same as -o filenames, space is completed escaped

TabTab

bar\ one  bar\ two      # Unlike -o filenames, notice the spaces are displayed escaped
$ words bar\ ░

How do I actually implement a completion function?

Implementing a completion functions involves:

  1. Representing your word list.
  2. Filtering your word list to just candidates for the current word.
  3. Setting COMPREPLY correctly.

I'm not going to assume to know all the complex requirements there can be for 1 and 2 and the following is only a very basic implementation. I'm providing an explanation for each part so one can mix-and-match to fit their own requirements.

foo() {
    # Get the currently completing word
    local CWORD=${COMP_WORDS[COMP_CWORD]}

    # This is our word list (in a bash array for convenience)
    local WORD_LIST=(foo 'bar one' 'bar two')

    # Commands below depend on this IFS
    local IFS=$'\n'

    # Filter our candidates
    CANDIDATES=($(compgen -W "${WORD_LIST[*]}" -- "$CWORD"))

    # Correctly set our candidates to COMPREPLY
    if [ ${#CANDIDATES[*]} -eq 0 ]; then
        COMPREPLY=()
    else
        COMPREPLY=($(printf '%q\n' "${CANDIDATES[@]}"))
    fi
}

complete -F foo words

In this example, we use compgen to filter our words. (It's provided by bash for this exact purpose.) One could use any solution they like but I'd advise against using grep-like programs simply because of the complexities of escaping regex.

compgen takes the word list with the -W argument and returns the filtered result with one word per line. Since our words can contain spaces, we set IFS=$'\n' beforehand in order to only count newlines as element delimiters when putting the result into our array with the CANDIDATES=(...) syntax.

Another point of note is what we're passing for the -W argument. This argument takes an IFS delimited word list. Again, our words contain spaces so this too requires IFS=$'\n' to prevent our words being broken up. Incidentally, "${WORD_LIST[*]}" expands with elements also delimited with what we've set for IFS and is exactly what we need.

In the example above I chose to define WORD_LIST literally in code.

One could also initialize the array from an external source such as a file. Just make sure to move IFS=$'\n' beforehand if words are going to be line-delimited such as in the original question:

local IFS=$'\n'
local WORD_LIST=($(cat /path/to/words.dat))`

Finally, we set COMPREPLY making sure to escape the likes of spaces. Escaping is quite complicated but thankfully printf's %q format performs all the necessary escaping we need and that's what we use to expand CANDIDATES. (Note we're telling printf to put \n after each element because that's what we've set IFS to.)

Those observant may spot this form for COMPREPLY only applies if -o filenames is not used. No escaping is necessary if it is and COMPREPLY may be set to the same contents as CANDIDATES with COMPREPLY=("$CANDIDATES[@]").

Extra care should be taken when expansions may be performed on empty arrays as this can lead to unexpected results. The example above handles this by branching when the length of CANDIDATES is zero.

_foo ()
{
  words="bar one"$'\n'"bar two"
  COMPREPLY=()
  cur=${COMP_WORDS[COMP_CWORD]}
  prev=${COMP_WORDS[COMP_CWORD-1]}
  cur=${cur//\./\\\.}

  local IFS=$'\n'
  COMPREPLY=( $( grep -i "^$cur" <( echo "$words" ) | sed -e 's/ /\\ /g' ) )
  return 0
}

complete -o bashdefault -o default -o nospace -F _foo words 

Pipe _find_words through sed and have it enclose each line in quotation marks. And when typing a command line, make sure to put either " or ' before a word to be tab-completed, otherwise this method will not work.

_find_words() { cat words.dat; }

_words_complete()
{

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

  local IFS=$'\n'
  COMPREPLY=( $( compgen -W "$( _find_words | sed 's/^/\x27/; s/$/\x27/' )" \
                         -- "$cur" ) )

}

complete -F _words_complete words

Command line:

$ words "ba░

tab

$ words "bar ░

tabtab

bar one  bar two
$ words "bar o░

tab

$ words "bar one" ░

I solved this by creating my own function compgen2 which handles the extra processing when the current word doesn't begin with a quote character. otherwise it works similar to compgen -W.

compgen2() {
    local IFS=$'\n'
    local a=($(compgen -W "$1" -- "$2"))
    local i=""
    if [ "${2:0:1}" = "\"" -o "${2:0:1}" = "'" ]; then
        for i in "${a[@]}"; do
            echo "$i"
        done
    else
        for i in "${a[@]}"; do
            printf "%q\n" "$i"
        done
    fi
}

_foo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local prev=${COMP_WORDS[COMP_CWORD-1]}
    local words=$(cat words.dat)
    local IFS=$'\n'
    COMPREPLY=($(compgen2 "$words" "$cur"))
}

echo -en "foo\nbar one\nbar two\n" > words.dat
complete -F _foo foo
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top