Maneira de mover o cursor por argumentos no bash?

5

No bash, você pode usar M-f e M-b para mover o cursor uma palavra para frente e para trás, mas existe uma maneira de mover um argumento para frente ou para trás? Se não fora da caixa, talvez por alguma configuração?

Em outras palavras, eu gostaria de usar o cursor para navegar entre as posições marcadas abaixo.

cp "foo bar.txt" "/tmp/directory with space"
^  ^             ^
|  |             |
    
por Andreas Järliden 24.01.2013 / 20:23

1 resposta

5

Eu sei que você está usando o bash, e eu não estou confiante de que o que você pergunta é possível no bash. O que vou mostrar é como implementar o recurso solicitado no ZSH. (ZSH é um pouco como um bash melhorado - se você mudar, então você ainda deve permanecer proficiente).

No ZSH existe o ZSH Line Editor (abreviado para zle). Isso fornece todas as teclas de movimento como comandos ligáveis, bem como o bash. Para onde vai mais longe, está a capacidade de definir comandos personalizados. Um comando personalizado é qualquer função do shell que foi transformada em um widget.

Essas funções podem executar outros comandos e também obter acesso a diversas variáveis que são de interesse para o seu problema. Os que eu vou falar são:

  • $ BUFFER - esta é a linha inteira que você está editando no momento
  • $ CURSOR - esta é a posição da inserção na linha atual

Existem também outras disponíveis, como:

  • $ LBUFFER - isso é tudo antes do cursor
  • $ RBUFFER - isso é tudo depois do cursor

Agora acontece que o ZSH não só é capaz de fornecer associações de teclas personalizadas, como também tem um conjunto de operações muito mais abrangente que você pode executar em variáveis. Um dos que é interessante para esse problema é:

  • z - Divida o resultado da expansão em palavras usando a análise de shell para encontrar as palavras, ou seja, levando em conta qualquer cotação no valor.

Você pode atribuir o $ BUFFER expandido diretamente a uma variável, assim:

line=${(z)BUFFER}

(a linha é agora uma matriz, mas irritantemente esta matriz começa no índice 1, ao contrário do bash!)

Isso não executará nenhuma expansão de caracteres globbing, portanto, ele retornará uma matriz dos argumentos reais em sua linha atual. Depois de ter isso, você está interessado na posição do ponto inicial de cada palavra no buffer. Infelizmente você pode ter vários espaços entre duas palavras, assim como palavras repetidas. A melhor coisa que posso pensar neste momento é remover cada palavra sendo considerada a partir do buffer atual como você a considera. Algo como:

buffer=$BUFFER
words=${(z)buffer}
for word in $words[@]
do
    # doing regular expression matching here,
    # so need to quote every special char in $word.
    escaped_word=${(q)word}
    # Fancy ZSH to the rescue! (q) will quote the special characters in a string.

    # Pattern matching like this creates $MBEGIN $MEND and $MATCH, when successful
    if [[ ! ${buffer} =~ ${${(q)word}:gs#\\'#\'#} ]]
    then
        echo "Something strange happened... no match for current word"
        return 1
    fi
    buffer=${buffer[$MEND,-1]}
done

Estamos quase lá agora! O que é necessário é uma maneira de ver qual palavra é a última palavra antes do cursor, e qual palavra é o começo da próxima palavra após o cursor.

buffer=$BUFFER
words=${(z)buffer}
index=1
for word in $words[@]
do
    if [[ ! ${buffer} =~ ${${(q)word}:gs#\\'#\'#} ]]
    then
        echo "Something strange happened... no match for current word"
        return 1
    fi

    old_length=${#buffer}
    buffer=${buffer[$MEND,-1]}
    new_length=${#buffer}
    old_index=$index
    index=$(($index + $old_length - $new_length))

    if [[ $old_index -lt $CURSOR && $index -ge $CURSOR ]]
    then
        # $old_index is the start of the last argument.
        # you could move back to it.
    elif [[ $old_index -le $CURSOR && $index -gt $CURSOR ]]
    then
        # $index is the start of the next argument.
        # you could move forward to it.
    fi
    # Obviously both of the above conditions could be true, you would
    # have to have a way to tell which one you wanted to test - but since
    # you will have two widgets (one forward, one back) you can tell quite easily. 
done

Até agora, mostrei como você pode derivar o índice apropriado para o cursor para mover. Mas eu não mostrei a você como mover o cursor ou como vincular essas funções a chaves.

A variável $ CURSOR pode ser atualizada e, se você fizer isso, poderá mover o ponto de inserção atual. Muito fácil!

A vinculação de funções a chaves envolve uma etapa intermediária de vinculação a um widget primeiro:

zle -N WIDGET_NAME FUNCTION_NAME

Você pode vincular o widget a uma chave. Você provavelmente terá que procurar os identificadores de chave específicos, mas eu costumo apenas ligar para Ctrl-LETTER, o que é bastante fácil:

bindkey '^LETTER' WIDGET_NAME

Vamos colocar tudo isso junto e corrigir seu problema:

function move_word {
    local direction=$1

    buffer=$BUFFER
    words=${(z)buffer}
    index=1
    old_index=0
    for word in $words[@]
    do
        if [[ ! ${buffer} =~ ${${(q)word}:gs#\\'#\'#} ]]
        then
            echo "Something strange happened... no match for current word $word in $buffer"
            return 1
        fi

        old_length=${#buffer}
        buffer=${buffer[$MEND,-1]}
        new_length=${#buffer}
        index=$(($index + $old_length - $new_length))

        case "$direction" in
            forward)
                if [[ $old_index -le $CURSOR && $index -gt $CURSOR ]]
                then
                    CURSOR=$index
                    return
                fi
                ;;
            backward)
                if [[ $old_index -lt $CURSOR && $index -ge $CURSOR ]]
                then
                    CURSOR=$old_index
                    return
                fi
                ;;
        esac
        old_index=$index
    done
    case "$direction" in
        forward)
            CURSOR=${#BUFFER}
            ;;
        backward)
            CURSOR=0
            ;;
    esac
}

function move_forward_word {
    move_word "forward"
}

function move_backward_word {
    move_word "backward"
}

zle -N my_move_backwards move_backward_word
zle -N my_move_forwards move_forward_word
bindkey '^w' my_move_backwards
bindkey '^e' my_move_forwards

No que diz respeito aos meus testes, isso parece fazer o trabalho. Você provavelmente desejará alterar as chaves às quais ele se liga. Para referência, testei com a linha:

one 'two three' "four five"    "'six seven' eight nine" * **/ **/**/*
^  ^           ^           ^                           ^ ^   ^       ^

e navegou entre os carets. Não quebra.

    
por 30.01.2013 / 22:38