Как переместить курсор по аргументам в bash?

Как переместить курсор по аргументам в bash?

В bash вы можете использовать M- fи M- bдля перемещения курсора на одно слово вперед и назад, но есть ли способ переместить курсор на одно словоаргументвперед или назад? Если не из коробки, то может быть по какой-то конфигурации?

Другими словами, я хотел бы, чтобы курсор перемещался между отмеченными ниже позициями.

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

решение1

Я знаю, что вы используете bash, и я не уверен, что то, что вы просите, возможно в bash. Я покажу вам, как реализовать запрошенную функцию в ZSH. (ZSH немного похож на улучшенный bash — если вы переключитесь, вы все равно должны оставаться опытным).

В ZSH есть редактор строк ZSH (zle для краткости). Он предоставляет все клавиши перемещения в качестве связываемых команд, как в bash. Дальше идет возможность определять пользовательские команды. Пользовательская команда — это любая функция оболочки, которая была преобразована в виджет.

Эти функции могут выполнять другие команды, а также они получают доступ к нескольким переменным, которые представляют интерес для вашей проблемы. Я расскажу о следующих:

  • $BUFFER — это вся строка, которую вы сейчас редактируете.
  • $CURSOR - это позиция вставки в текущей строке

Также доступны и другие варианты, например:

  • $LBUFFER - это все, что находится перед курсором
  • $RBUFFER - это все, что после курсора

Теперь оказывается, что ZSH не только способен предоставлять пользовательские сочетания клавиш, но и имеет гораздо более полный набор операций, которые вы можете выполнять над переменными. Одна из операций, которая интересна для этой проблемы, это:

  • z - Разбить результат расширения на слова, используя синтаксический анализ оболочки для поиска слов, т.е. принимая во внимание любые кавычки в значении.

Вы можете присвоить расширенный $BUFFER непосредственно переменной, например так:

line=${(z)BUFFER}

(строка теперь является массивом, но, что раздражает, этот массив начинается с индекса 1, в отличие от bash!)

Это не будет выполнять никакого расширения символов подстановки, поэтому оно вернет массив фактических аргументов в вашей текущей строке. Как только вы это получите, вам будет интересно положение начальной точки каждого слова в буфере. К сожалению, у вас может быть несколько пробелов между любыми двумя словами, а также повторяющиеся слова. Лучшее, что я могу придумать на данный момент, это удалить каждое рассматриваемое слово из текущего буфера, как вы его рассматриваете. Что-то вроде:

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

Мы почти у цели! Нужен способ увидеть, какое слово является последним словом перед курсором, а какое — началом следующего слова после курсора.

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

До сих пор я показывал, как можно получить соответствующий индекс для перемещения курсора. Но я не показал, как перемещать курсор или как привязывать эти функции к клавишам.

Переменная $CURSOR может быть обновлена, и если вы это сделаете, то сможете переместить текущую точку вставки. Довольно просто!

Привязка функций к клавишам включает в себя промежуточный этап привязки к виджету:

zle -N WIDGET_NAME FUNCTION_NAME

Затем вы можете привязать виджет к клавише. Вам, вероятно, придется поискать конкретные идентификаторы клавиш, но я обычно просто привязываю к Ctrl-БУКВА, что достаточно просто:

bindkey '^LETTER' WIDGET_NAME

Давайте соберем все это вместе и решим вашу проблему:

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

Насколько я могу судить по собственному тестированию, это, похоже, работает. Вероятно, вам захочется изменить клавиши, к которым он привязан. Для справки, я протестировал его со строкой:

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

и он перемещается между каретками. Он не переносится.

решение2

Да, в man bash сказано, что можно использовать

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.  

Последовательность метасимволов, не заключенных в кавычки, является аргументом.

Там не сказано, что они привязаны к какой-либо клавише по умолчанию, и у меня ее нет в inputrc, но на моей оболочке они привязаны к CMf и CMb соответственно. Если нет, привяжите их в inputrc с помощью

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

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

решение3

Я использую Ctrl+ sи Ctrl+ rдля интерактивного поиска (и перемещения) вперед и назад по командной строке (и по истории команд). Хотя это и не совсем то, о чем просит Андреас, вы можете использовать эти интерактивные функции поиска, чтобы быстро перейти к аргументу по вашему выбору.

Если вы запустите bind -Pbash, вы увидите все bashсвязываемые действия.

Более подробное обсуждение других вариантов перемещения курсора см. по ссылке ниже:
https://stackoverflow.com/q/657130

Связанный контент