Maneira de mover o cursor por argumentos no bash?

Maneira de mover o cursor por argumentos no bash?

No bash, você pode usar M- fe M- bpara mover o cursor uma palavra para frente e para trás, mas existe uma maneira de mover umaargumentopara frente ou para trás? Se não estiver pronto para uso, talvez por alguma configuração?

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

cp "foo bar.txt" "/tmp/directory with space"
^  ^             ^
|  |             |

Responder1

Eu sei que você está usando o bash e não tenho certeza de que o que você pergunta seja possível no bash. O que vou mostrar é como implementar o recurso solicitado no ZSH. (ZSH é um pouco como um bash aprimorado - se você mudar, ainda deverá permanecer proficiente).

No ZSH existe o ZSH Line Editor (abreviadamente zle). Isso fornece todas as teclas de movimento como comandos vinculáveis, assim como o bash. O que vai além é a capacidade de definir comandos personalizados. Um comando personalizado é qualquer função shell que foi transformada em um widget.

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

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

Existem também outros disponíveis, como:

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

Agora acontece que o ZSH não é apenas capaz de fornecer atalhos de teclado personalizados, mas também possui um conjunto muito mais abrangente de operações que você pode executar em variáveis. Um dos que é interessante para este problema é:

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

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

line=${(z)BUFFER}

(line agora é um array, mas irritantemente esse array começa no índice 1, ao contrário do bash!)

Isso não realizará nenhuma expansão de caracteres globais, portanto retornará uma matriz dos argumentos reais em sua linha atual. Depois de fazer isso, você estará interessado na posição do ponto inicial de cada palavra no buffer. Infelizmente, você pode ter vários espaços entre duas palavras, bem como palavras repetidas. A melhor coisa que consigo pensar neste momento é remover cada palavra considerada do buffer atual conforme 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 início 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 qual o cursor se moverá. Mas não mostrei como mover o cursor ou como vincular essas funções às teclas.

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

Vincular funções a chaves envolve uma etapa intermediária de vincular primeiro a um widget:

zle -N WIDGET_NAME FUNCTION_NAME

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

bindkey '^LETTER' WIDGET_NAME

Vamos juntar tudo isso e resolver 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 próprios testes, isso parece funcionar. Você provavelmente desejará alterar as chaves às quais ele está vinculado. Para referência, testei com a linha:

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

e navegou entre os sinais de intercalação. Não embrulha.

Responder2

Sim, no man bash diz que você pode usar

shell-forward-word
              Move forward to the end of the next word.  Words  are  delimited  
              by non-quoted shell metacharacters.

shell-backward-word  
              Move  back  to the start of the current or previous word.  Words  
              are delimited by non-quoted shell metacharacters.  

Uma sequência de metacaracteres não citados é um argumento.

Ele não diz que eles estão vinculados a qualquer chave por padrão e eu não a tenho em meu inputrc, mas no meu shell eles estão vinculados a CMf e CMb respectivamente. Caso contrário, vincule-os em seu inputrc com

"\C-M-f": shell-forward-word

"\C-M-b": shell-backward-word  

Responder3

Eu uso Ctrl+ se Ctrl+ rpara pesquisar (e mover) interativamente para frente e para trás na linha de comando (e no histórico de comandos). Embora não seja exatamente o que Andreas está pedindo, você pode usar esses recursos de pesquisa interativos para passar rapidamente para o argumento de sua escolha.

Se você executar bind -Pno bash, poderá ver todos os bashações vinculáveis.

Veja o link abaixo para mais discussões sobre outras opções de movimento do cursor:
https://stackoverflow.com/q/657130

informação relacionada