2009-07-17 10 views
24

¿Cuál es la forma correcta/mejor de manejar espacios y comillas en la finalización de bash?Manejar los espacios y las comillas correctamente en la finalización de bash

Aquí hay un ejemplo simple. Tengo un comando llamado words (por ejemplo, un programa de búsqueda de diccionario) que toma varias palabras como argumentos. Las 'palabras' apoyados en realidad pueden contener espacios, y se define en un archivo llamado words.dat:

foo 
bar one 
bar two 

Aquí está mi primera solución sugerida:

_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>’ completa correctamente el comando para ‘words foo ’ (con una espacio final), que es bueno, pero para ‘words b<tab>’ sugiere ‘words bar ’. La finalización correcta sería ‘words bar\ ’. Y para ‘words "b<tab>’ y ‘words 'b<tab>’ no ofrece sugerencias.

Esta última parte que he podido resolver. Es posible usar eval para analizar correctamente los caracteres (escapados). Sin embargo, eval no es aficionado a las frases que faltan, así que para obtener todo funcione, he tenido que cambiar el search="$cur" a

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

Esto funciona en realidad. Ambos ‘words "b<tab>’ y ‘words 'b<tab>’ se autocompletan correctamente, y si agrego un ‘o’ y presiono <tab> nuevamente, en realidad completa la palabra y agrega la cita de cierre correcta. Sin embargo, si intento completar ‘words b<tab>’ o incluso ‘words bar\ <tab>’, se autocompleta a ‘words bar ’ en lugar de ‘words bar\ ’, y al agregar, por ejemplo, ‘one’, se produce un error cuando se ejecuta el programa words.

Ahora, obviamente es es posible manejar esto correctamente. Por ejemplo, el comando ls puede hacerlo para los archivos námned ‘foo’‘bar one’ y ‘bar two’ (aunque tiene problemas con algunas formas de expresar los nombres de archivo cuando se usa una combinación (válida) de ", ' y varios escapes). Sin embargo, no pude entender cómo lo hace ls leyendo el código de finalización de bash.

Entonces, ¿alguien sabe cómo manejar esto correctamente? Las cotizaciones de entrada reales no necesitan conservarse; Estaría contento con una solución que cambia ‘words "b<tab>’, ‘words 'b<tab>’ y ‘words b<tab>’ a ‘words bar\ ’, por ejemplo, (aunque preferiría omitir las comillas, como en este ejemplo, en lugar de agregarlas).

Respuesta

7

Esta solución de posprocesamiento no demasiado elegante parece funcionar para mí (GNU bash, versión 3.1.17 (6) -release (i686-pc-cygwin)). (A menos que no haya probado algunos casos de borde como de costumbre :))

No es necesario evaluar cosas, solo hay 2 tipos de citas.

Dado que compgen no quiere escapar espacios para nosotros, los escaparemos nosotros mismos (solo si la palabra no comenzó con un presupuesto). Esto tiene un efecto secundario de la lista completa (en la doble pestaña) que también tiene valores de escape. No estoy seguro si eso es bueno o no, ya que ls no lo hace ...

EDITAR: Se corrigió para manejar los qoutes simples y dobles dentro de las palabras. Esencialmente tenemos que pasar 3 descubrimientos :).Primero para grep, segundo para compgen, y último para palabras en sí mismo cuando se completa la autocompletación.

_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 
} 
+1

Gracias. Esta solución funciona para los ejemplos originales, pero si agrego 'rock' n roll 'a words.dat, falla. Mi uso de la autocompletación en la vida real en realidad involucra palabras con apóstrofes, y esa es la razón por la que originalmente usé 'eval'. Es bastante fácil (aunque no muy elegante) corregir, agregando un "buscar y reemplazar" extra al 'for loop, y luego agregar otro ciclo for para cadenas que comiencen con '. El único problema restante, hasta donde puedo ver, es que la autocompletación no avanza una posición del cursor si ha escrito una palabra completa, incluidas las comillas de cierre. –

+0

Con respecto a mi comentario anterior. Parece que la situación es un poco peor de lo que pensaba. La finalización automática dentro de una palabra que contiene apóstrofes (por ejemplo, intentar autocompletar 'rock' n ro ', ya sea utilizando espacios escabrosos y apóstrofo, o comillas simples o dobles) no funciona. La razón es que la variable 'search' no está en su forma expandida correcta. Algunas sustituciones adicionales parecen posibles, pero no he podido hacer que esto funcione correctamente para las tres formas diferentes de escapar. –

+0

Sí, compgen parece estar eliminando todos los qoutes ... lo que significa que deben escaparse dentro de _find_words – Eugene

10

Me encontré en una situación similar y busqué alto y bajo para la respuesta a esta discrepancia. Aquí está la respuesta que se me ocurrió.

ls, o más bien, la rutina de finalización predeterminado lo hace utilizando la funcionalidad -o filenames, que realiza:. procesamiento-nombre de archivo específico (como la adición de una barra de nombres de directorio o suprimir los espacios finales

por ejemplo

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

Tab

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

Tab Tab

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

Teniendo en cuenta la situación de la OP, la elección parece ser:

  1. Uso -o filenames. Sin embargo, si hay un directorio con el mismo nombre que la palabra correspondiente, la palabra completa tendrá una barra arbitraria al final. (por ejemplo, bar\ one/)
  2. Escapar manualmente como en la respuesta de @ Eugene, y aceptar que los candidatos se mostrarán como escapados.

Por cierto, hay una manera más fácil para escapar de palabras a partir de printf "%q":

por ejemplo, 2

$ bar() { 
>  foo 
>  local IFS=$'\n' 
>  COMPREPLY=($(printf "%q\n" "${COMPREPLY[@]}")) 
> } 
$ complete -F bar words 
$ words ░ 

Tab

$ words bar\ ░ 

TabTab

bar\ one bar\ two  # Ex.3: notice the spaces are displayed escaped 
$ words bar\ ░ 
+0

Su primer ejemplo no funciona realmente. TAB TAB te da una buena lista, pero cuando intentas "palabras barra t " falla. – Orwellophile

+0

Estás malinterpretando el propósito del ejemplo. Si realmente desea implementar una rutina de finalización de bash, se necesita mucho más que solo configurar COMPREPLY a una matriz estática. Estoy seguro de que hay más preguntas sobre SO que se refieren específicamente a esto. – antak

+2

si así lo dice. Me tomé la libertad de proporcionar un ejemplo completamente funcional de su ejemplo, como una respuesta. – Orwellophile

3
_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 
+0

¿Por qué estás escapando '.' en' \ .' en '$ cur'? –

+0

Porque.es un comando de expresión regular para * hacer coincidir cualquier carácter * – Orwellophile

+0

Pero las sustituciones de bash nunca son expresiones regulares, * ¿verdad? * Considere: 'text = 'foo.bar'; echo "$ {text //./_}" '- que funciona como un reemplazo literal del punto en GNU bash 4.3.11 (Linux) y 3.1.20 (Windows). –

1

Pipe _find_words º áspero sed y haga que encierre cada línea entre comillas. Y al escribir una línea de comando, asegúrese de poner " o ' antes de una palabra para completar con pestañas, de lo contrario, este método no funcionará.línea de

_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 

Comando:

$ words "ba░ 

pestaña

$ words "bar ░ 

pestañapestaña

bar one bar two 
$ words "bar o░ 

pestaña

$ words "bar one" ░ 
0

He resuelto mediante la creación de mi propia compgen2 función que maneja el procesamiento adicional cuando la palabra actual no comienza con un carácter de comillas. de lo contrario, funciona de manera similar a 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 
Cuestiones relacionadas