Курилка

Перевод с английского © Гиви Хакеридзе. Вопрос и ответ на Stack Overflow.
См. также Vim-galore — по-русски.

Каково ваше самое продуктивное клавиатурное сочетание в Vim?

Я много слышал о Vim, и «за», и «против». И действительно, похоже, что с Vim вы можете (как разработчик) быть быстрее, чем с любым другим редактором. Я использую Vim, чтобы делать самые простые вещи, но с Vim я более чем в 10 раз менее продуктивен.

Только о двух вещах следует заботиться, когда речь идёт о скорости (вас это может не сильно беспокоить, хотя должно бы):

  1. Попеременное использование левой и правой руки — самый быстрый способ использования клавиатуры.
  2. Никогда не прикасаться к мыши — второй приём быть быстрым насколько это возможно. Понадобится время, чтобы перенести руку, взять мышь, передвинуть её и вернуть руку обратно (к тому же, часто после этого приходится смотреть на клавиатуру, чтобы убедиться, что рука находится в нужном месте)

Вот два примера, демонстрирующих, почему с Vim я менее продуктивен.

Копирование/Вырезание и вставка. Я делаю это постоянно. Во всех современных редакторах, чтобы выделить текст, вы левой рукой нажимаете Shift, а правой перемещаете курсор. Затем клавишами Ctrl+C копируете, после этого перемещаете курсор и при помощи Ctrl+V вставляете.

В Vim это просто ужасно:

Ещё пример? Поиск и замена.

И всё в Vim происходит похожим образом: видимо, я не понимаю, как с ним работать правильно.

NB : Шпаргалку по Vim я уже изучил. :)

Вот мой вопрос:

Каким образом вы используете Vim так, что он делает вас более продуктивным, чем какой-нибудь современный редактор?

Jim Dennis


Ваша проблема с Vim заключается в том, что вы не въехали в vi

Вы упомянули вырезание при помощи yy и сетуете на то, что обычно вам не нужно вырезать целые строки. Но на практике, программистам, редактирующим исходный код, очень часто приходится работать с целыми строками, диапазонами строк и блоками кода. yy — только один из множества способов скопировать текст в анонимный буфер копирования (или «регистр», как это называется в vi).

«Цзен» vi — в том, что вы разговариваете на некоем языке. Первое y — глагол. Выражение yy — синоним y_. y сдвоено для того, чтобы это было проще печатать, т.к. это очень часто встречающаяся операция.

Это же можно сделать при помощи dd P (удалить текущую строку и вставить её обратно; при этом, в качестве побочного эффекта, оставив её копию в анонимном регистре). y и d — «сказуемые», которые в качестве «подлежащего» принимают любое перемещение. Таким образом, yW это — «скопировать отсюда (где курсор) до конца текущего/следующего (большого) слова», а y'a — «скопировать отсюда до строки, которая содержит метку 'a'».

Если всё, что вы знаете, это только как перемещать курсор вверх, вниз, влево и вправо, то vi никогда не сделает вас продуктивнее, чем при использовании «Блокнота». (Ну хорошо, у вас всё-таки будет подсветка синтаксиса и возможность работы с файлами большими, чем ничтожные ~45KB или что-то около того; но постойте, не уходите).

У vi есть 26 «меток» и 26 «регистров». В метке запоминается позиция курсора при помощи команды m. Каждая метка обозначается одной буквой в нижнем регистре. Команда ma устанавливает метку 'a' в текущей позиции, а mz — метку 'z'. При помощи команды ' (одиночная кавычка) можно переместиться к строке с меткой 'a'. Т.е., 'a перемещает в начало строки, содержащей метку 'a'. Чтобы переместиться к точной позиции метки, можно использовать команду ` (обратная кавычка). Таким образом, `z переместит именно туда, где находится метка 'z'.

Так как все они — перемещения, их можно использовать в качестве субъектов для «выражений».

Поэтому, одним из способов вырезать произвольную часть текста является установка метки. Обычно я использую 'a' в качестве «первой» метки, а 'z' — для следующей, и 'b' для ещё одной и 'e' в качестве пары (при использовании vi в течение 15 лет в интерактивном режиме, мне ни разу не понадобилось более четырёх меток; имеются некоторые соглашения для того, чтобы метки и содержимое регистров, создаваемые интерактивно, не нарушались макро). После этого нужно перейти к другому концу интересуемого текста; не важно, в какую сторону. Затем можно использовать просто d`a, чтобы вырезать, или y`a, чтобы скопировать. Весь процесс требует 5 дополнительных нажатий клавиш (шесть, если начать в режиме «вставки», когда придётся нажать Esc для выхода в нормальный режим). После вырезания или копирования вставка осуществляется одним нажатием: p.

Я говорю, что это один из способов вырезания или копирования текста. Однако, он — лишь один из многих. Часто можно точно сказать, какая часть текста имеется в виду, не перемещая курсора и не выставляя меток. Например, находясь внутри параграфа, можно для перемещения к его началу или концу использовать { и } соответственно. Поэтому для перемещения параграфа я вырезаю его при помощи { d} (3 нажатия). (Ну, а если так получилось, что я нахожусь в первой или последней строке параграфа, я могу использовать просто d} или d{.)

Значение слова «параграф» обычно интуитивно понятно. И оно схоже в применении, как к коду, так и к обычному тексту.

Часто бывает известен некий шаблон (регулярное выражение), который находится на другом конце интересующего нас текста. Поиск вперёд или назад в терминах vi тоже является перемещением. Поэтому в наших «выражениях» он также может быть использован в качестве «субъекта». Поэтому я могу использовать d/foo для вырезания от текущей строки до строки ниже, которая содержит строку «foo», и y?bar, чтобы скопировать всё, начиная с текущей строки до ближайшей выше, содержащей «bar». Если мне не нужно работать с целыми строками, я могу использовать перемещения поиска (как они есть), делать метки и выполнять команды `x, как это было описано выше.

Кроме «сказуемых» и «подлежащих» у vi есть «объекты» (в грамматическом смысле этого слова). Ранее я говорил об использовании анонимного регистра. Однако, у меня есть возможность использовать любой из «именованных» регистров, предваряя ссылку на «объект» при помощи " (модификатором «двойная кавычка»). Таким образом, используя "add, я вырезаю в регистр 'a' текущую строку, а при помощи "by/foo копирую текст в регистр 'b' от текущего положения до строки, содержащей «foo». Для вставки из регистра я просто предваряю команду вставки такой же модифицирующей последовательностью: "ap вставляет копию содержимого регистра 'a' после позиции курсора, а "bP вставляет копию из регистра 'b' перед.

Такие «префиксы» добавляют понятия «прилагательное» и «наречие» к нашему «языку» управления текстом. Большинство команд (глаголов) и перемещений (глаголов или объектов, в зависимости от контекста) могут воспринимать численные префиксы. Например, 3J означает «объединить со следующими тремя строками», а d5} — «удалить, начиная с текущей строки, пять параграфов ниже».

И всё это — лишь промежуточный уровень vi. Здесь нет ничего специфичного для Vim, и нет более продвинутых приёмов vi, если вы готовы с ними познакомиться. Если вы обладаете только этими приёмами среднего уровня, возможно, вам иногда пригодится написать кое-какие макро, потому что язык для манипуляции текстом достаточно лаконичен и выразителен для того, чтобы достаточно просто делать большинство работы при помощи «родного» языка.


Подборка более продвинутых трюков:

Есть несколько :-команд, самая известная из которых — :%s/foo/bar/g — глобальная подстановка. (Это, конечно, не настолько продвинутая, но другие команды, начинающиеся с :, могут таковыми оказаться). Набор команд : vi унаследовал от ed (строчного редактора), а позднее — от набора утилит ex (расширенного строчного редактора). На самом деле, vi был назван именно так за то, что он был визуальным интерфейсом для ex.

Обычно команды : работают со строками текста. ed и ex были написаны в эпоху, когда экранные терминалы были редкостью, и большинство терминалов были устройствами типа «телетайп» (TTY). Поэтому работа с напечатанной копией текста была в порядке вещей, при этом команды вводились через очень медленные устройства (обычная скорость соединения была 110 boud, примерно 11 символов в секунду, что медленнее, чем может напечатать достаточно быстрый наборщик; в многопользовательской среде были частыми сбои; и, кроме того, неплохо было бы экономить бумагу).

Вот поэтому синтаксис большинства команд : включает адрес или диапазон адресов (номеров строк), за которым следует сама команда. Она принимает буквальные номера строк: :127,215 s/foo/bar для замены первого вхождения «foo» на «bar» в каждой строке с 127 по 215-ю. Кроме этого, можно использовать сокращения типа . или $ для текущей и последней строки соответственно. Или относительные префиксы + и - для ссылки по смещению относительно текущей строки. Таким образом, :.,$j означает «от текущей строки до последней, объединить все строки в одну». Для 1,$ есть синоним :% (все строки).

Команды :... g и :... v дают дополнительное уточнение диапазону, т.к. они невероятно сильны. :... g — префикс для «глобального» («globally») применения последующей команды ко всем строкам, которые соответствуют шаблону (регулярному выражению), а :... v применяет такую команду к строкам, которые шаблону НЕ соответствуют («v» из «conVerse» — обратный). Как и в случае с другими командами ex, перед ними так же можно указать ссылки на адрес или диапазон. Таким образом, :.,+21g/foo/d означает «удалить все строки, содержащие строку “foo”, начиная с текущей плюс следующие 21», в то время как :.,$v/bar/d — отсюда до конца файла удалить строки, которые НЕ содержат строку “bar”».

Интересно, что создание распространённой команды Unix grep было вдохновлено этой командой ex (и названа grep в честь того, как была задокументирована :g). Команда ex :g/re/p (grep) использовалась для пояснения способа, как глобально («globally») напечатать («print») строки, содержащие регулярное выражение («regular expression» (re)). Когда использовались ed и ex, команда :p была одной из первых, которую любой изучал, и часто первой при редактировании любого файла. Таким способом распечатывали текущее содержимое (обычно постранично при помощи :.,25 или похожим способом).

Нужно отметить, что :% g/.../d или (его противоположность) : v/.../d — наиболее часто используемые шаблоны использования. Однако существует ещё парочка команд ex, заслуживающие того, чтобы их запомнить:

Для перемещения строк можно использовать m, а для объединения — j. Например, имеется некий список и нужно отделить всё, что соответствует (или, наоборот, не соответствует) некоторому шаблону, не удаляя строки совсем, тогда можно использовать что-то вроде: :% g/foo/m$ ... и все строки, содержащие «foo», будут перенесены в конец файла. (Обратите внимание на ещё один совет — использование конца файла в качестве рабочего пространства). Это действие сохранит относительный порядок строк с «foo», переместив их в конец списка. (Это эквивалентно примерно такому действию: 1G!GGmap!Ggrep foo<ENTER>1G:1,'a g/foo'/d (скопировать весь файл в свой конец (ИМХО, вместо первого ! д.б. yприм. пер.), отфильтровать хвост файла при помощи grep и удалить ненужное с начала файла до метки a (ИМХО, кавычка в регулярном выражении — лишняя — прим. пер.)).

Для объединения строк я обычно нахожу шаблон для тех, которые нужно объединить с предшествующей (например, строки в маркированном списке, которые начинаются с «^ », но не с «^ * »). В этом случае я делаю следующее: :% g/^ /-1j (для всех подходящих строк перейти выше на одну и объединить (ИМХО, шаблон будет совпадать и с «^ * » — прим. пер.)). (BTW: в случае маркированного списка попытка поиска строк с маркой и объединения со следующей работать не будет по ряду причин ... это может объединить одну маркированную строку с другой, но не сможет объединить маркированную строку со всеми последующими; при сравнении он будет работать только попарно).

Можно было бы не упоминать, про то, что вместе с командами g и v (глобально/глобально-с-отрицанием) можно использовать нашего старого друга s (подстановку). Редко бывает, когда это может понадобиться. Однако рассмотрим некоторый пример, когда подстановку необходимо сделать в строках, которые соответствуют другому шаблону. Бывает, приходится использовать сложные шаблоны с захватами и обратными ссылками для того, чтобы сохранить части строки, которые НЕ нужно изменять. Но чаще проще отделить поиск от замены: :% g/foo/s/bar/zzz/g — для каждой строки, содержащей «foo», выполнить замену всех «bar» на «zzz». (Нечто вроде :% s/\(.*foo.*\)bar\(.*\)/\1zzz\2/g будет работать только для тех вхождений «bar», которым ПРЕДШЕСТВУЕТ «foo» в той же строке; это и так выглядит достаточно неуклюже, но и потребует дополнительно усложнить выражение, чтобы отловить случаи, когда «bar» предшествует «foo».)

Дело ещё в том, что само наличие набора команд ex для p, s или d строк, это ещё не всё.

Адресами в : могут быть и метки. То есть можно использовать :'a,'bg/foo/j, чтобы объединить строку, содержащую «foo», с последующей, если она находится между метками 'a' и 'b'. (Да, конечно, во всех предшествующих примерах использования команд ex для ограничения набора строк файла можно использовать подобные выражения.)

Всё это достаточно туманно (за последние 15 лет я использовал подобное всего несколько раз). Мне нетрудно признаться, что часто делаю вещи повторно и интерактивно, что, возможно это же можно было бы сделать более эффективно, если бы я потратил немного времени, чтобы придумать правильное заклинание.

Ещё одна очень полезная команда vi или exr для чтения содержимого другого файла. То есть: :r foo вставляет содержимое файла с именем «foo» после текущей строки.

Ещё более мощная команда — :r!. Она читает вывод команды. Это то же самое, что приостановить сессию vi, выполнить команду, перенаправив её вывод в файл, возобновить сессию vi и прочитать содержимое временного файла.

И ещё более мощные команды: ! (восклицательный знак — бэнг) и :... ! (ex бэнг). Они тоже выполняют внешние команды и читают результат в текущий файл. Но кроме этого, они фильтруют порции нашего текста при помощи этой команды! При помощи 1G!Gsort можно отсортировать строки нашего текста (G — команда «goto» в vi; по умолчанию она переносит к концу файла, но, если перед ней указать номер строки, например, 1, она перенесёт к первой строке).

Это эквивалент варианта ex :1,$!sort. Писатели часто используют ! с утилитами Unix fmt или fold для переформатирования или «переноса по словам» порций текста. Часто встречается макро {!}fmt (переформатирование текущего абзаца). Программисты иногда используют их, что бы пропустить свой код или его часть через indent или другой инструмент для переформатирования.

Использование команд :r! и ! означает, что любая внешняя утилита или фильтр может считаться расширением нашего редактора. Я иногда использую это со скриптами, которые извлекают данные из базы данных, или с командами wget или lynx, которые получают данные с вебсайтов, или с командами ssh, которые получают данные с удалённых систем.

И ещё одна полезная команда ex:so (сокращение от :source). Она читает содержимое файла как серию команд. При запуске обычно vi неявно выполняет :source с файлом ~/.exinitrcVim, конечно же, делает то же самое с ~/.vimrc). Полезность этого в том, что при помощи команды :so можно на лету изменить профиль редактора и настроить новый набор макро, аббревиатур и настроек. Если вы достаточно сообразительны, то, сохранив последовательность команд ex, можно применять их к файлам, когда это потребуется.

Например, у меня есть семистрочный (36 символов) файл, который прогоняет файл через wc и вставляет в начале файла комментарий в стиле C с количеством слов. Я могу применить такое «макро» к файлу при помощи командной строки vim +'so mymacro.ex' ./mytarget (опция + командной строки vi и Vim обычно используется для того, чтобы начать сессию редактирования в строке с определённым номером. Однако существует один малоизвестный факт, что за ней могут следовать любые допустимые команды или выражения ex, например, команда «source», что я здесь и проделал; в качестве простого примера, у меня есть скрипты, которые запускают vi +'/foo/d|wq!' ~/.ssh/known_hosts для неинтерактивного удаления элемента из моего файла известных хостов SSH, когда я пересматриваю набор серверов).

Обычно гораздо проще писать такие «макро» на Perl, AWK или sed (который, на самом деле, как и grep был создан под впечатлением от команды ed).

Возможно, самой непонятной командой vi будет команда @. Нечасто выступая на курсах системных администраторов в течение ближайшего десятилетия, я встречал совсем немного людей, кто ей вообще пользуется. @ выполняет содержимое регистра, так, как если бы это была команда vi или ex. Пример: я часто использую :r!locate ..., чтобы найти файл в моей системе и прочитать его название в мой документ. Потом я удаляю всё ненужное, оставляя полный путь только того файла, который меня интересует. Вместо кропотливых нажатий Tab, чтобы обойти каждый элемент пути (или, что ещё хуже, если бы мне довелось попасть на машину, где копия vi не имеет поддержки дополнения при помощи Tab), я просто использую:

  1. Oi:r (для того, чтобы сделать текущую строку допустимой командой vi),
  2. "cdd (чтобы удалить строку в регистр «c») и
  3. @c выполняю эту команду.

Всего 10 нажатий на клавиши (и выражение "cdd @c практически находится у меня на кончиках пальцев, поэтому я могу набрать его так же быстро, как и слово из шести букв).


Ободряющее замечание

Я только притронулся к поверхности мощи vi, и ничего из того, о чём я здесь писал, не является частью «улучшений», за которые назван vim! Всё, о чём я здесь говорил, должно работать в любой старой копии vi, и 20, и 30 лет назад.

Есть люди, которые используют мощь vi гораздо в большей степени, чем я когда-либо буду.


Jim Dennis,
отредактировано 2011-12-22 11:43:37Z