ваша проблема з vim у тому, що ви не розумієте vi

| щоденник, vim, комп'ютери, підказка

це переклад українською детальної підказки з використання деяких функцій vi та vim від джима денніса на stack overflow; джерело: «your problem with vim is that you don’t grok vi».

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

дзен vi в тому, що ви «говорите» певною мовою. перша y — це дієслово. фраза yy — синонім для y_. тут y подвоєно, аби спростити набір на клавіатурі, оскільки це вельми часто повторювана операція.

те саме можна виразити як dd P (видалити поточний рядок та одразу вставити його назад, залишивши копію в регістрі без назви як побочний ефект). «дієслова» y та d приймають будь-який оператор переміщення як «підмет»; таким чином yW означатиме «копіювати звідси (позиція курсора) до кінця поточного чи наступного «великого» слова», а y'a — «копіювати звідси аж до рядка, котрий містить іменовану позначку “a”».

якщо ви розумієте лише базові команди переміщення догори, додолу, ліворуч і праворуч, тоді vi буде для вас не більш продуктивним за звичайний «блокнот» (ну гаразд, ви все-одно ще матимете підсвітку синтаксису та можливість працювати з файлами, більшими за смішні 45 кілобайт, але ви розумієте, про що я).

vi має 26 позначок і 26 «регістрів». позначку можна поставити в будь-якій позиції курсора за допомогою команди m. кожна позначка іменується однією літерою латинки в нижньому регістр; тож ma ставить позначку «a» в поточній позиції, а mz — позначку «z». ви можете миттєво перейти до позначеного рядка, скориставшись командою ' (це одинарна «лапка»). отже, 'a перестрибує на початок рядка, що містить позначку «a». ви можете перемістити курсор точнісінько до позначеної позиції, скориставшись командою ` (зворотня засічка, backtick). таким чином `z перестрибне просто на позначку «z».

оскільки це команди переміщення, їх можна використати як «підмети» у інших «фразах».

отже, одним зі способів вирізати довільний фрагмент тексту буде поставити позначку (зазвичай я використовую «a» як «першу» позначку, тоді «z» як наступну, «b» як іншу й «e» як ще іншу — не пригадую, щоби мені бодай колись доводилося використовувати для інтерактивного редагування більш як чотири позначки за 15 років використання vi; кожен сам собі встановлює зручні звички щодо використання позначок та регістрів у макрокомандах, котрі не плутатимуться з інтерактивним редагуванням). тоді ми переходимо до іншого краю потрібного фрагменту (можна починати з будь-якого кінця, це байдуже), і просто використовуємо d`a для вирізання або y`a для копіювання. таким чином, вся послідовність потребує 5 додаткових натискань клавіш (шість, якщо ми починаємо в режимі вставки й потребуємо ще esc для виходу в командний режим). вирізавши чи скопіювавши, ми вставляємо цей фрагмент однією клавішею: p.

ось вам спосіб вирізати чи скопіювати, але він лише один із багатьох. часто можна ще стисліше описати фрагмент тексту, взагалі не рухаючи курсор і не розставляючи позначнок. приміром, якщо я редагую абзац тексту, я можу використати «рухи» { та } для переходу до початку та кінця абзацу. отже, щоби перенести абзац тексту, я вирізаю його за допомогою { та d} (3 натиски). Якщо мені пощастило вже бути на першому чи останньому рядку абзацу, я можу просто набрати d} або d{ відповідно.

визначення «абзацу» за стандантрих налаштувань vi зазвичай інтуїтивно обґрунтоване, тож часто працює для фрагментів коду так само добре, як і для прози.

частенько буває, що якийсь шаблон (регулярний вираз) може той чи інший кінець потрібного нам фрагменту тексту. пошук і зворотній пошук в тексті є «рухом» в термінах vi, тож і їх можна використати як «підмети» у командних «фразах». я можу використати d/foo, аби вирізати від поточного аж до наступного рядка, що містить текст «foo», а y?bar — щоби скопіювати від поточного до найближчого попереднього рядка, що містить фрагмент «bar». якщо ж мені не потрібні повні рядки, я можу скористатися пошуковими «рухами» (як окремими командами), поставити мітку і тоді скористатися командою `x, як описано вище.

на додачу до «дієслів» та «підметів», vi також має «обставини» (в граматичному сенсі). до цього я лише посилався на використання неіменованих регістрів, — однак можна ж використати й один із 26-ти іменованих, додавши префікс " (подвійні лапки). таким чином, я використовую "add, аби вирізати поточний рядок до регістра «a», а якщо наберу "by/foo, тоді я копіюю текст від поточного рядка до наступного з текстом «foo» до регістру «b». щоби вставити збережений фрагмент з іменованого регістру, я просто додаю той самий префікс до команди вставляння: "ap вставляє копію регістру «a» після курсора, а "bP вставляє копію з регістру «b» вище поточного рядка.

ця ідея «префіксів» додає також аналогію граматичних «прикметників» та «прислівників» до нашої «мови» обробки текстів. більшість команд («дієслова») та переміщень («дієслова» чи «об’єкти» залежно від контексту) можуть приймати також числові префікси. скажімо, 3J означає «приєднати наступні три рядка», а d5} означає «видалити від поточного рядка аж до п’ятого абзацу включно».

все це — середній рівень володіння vi. ніщо з цього не є особливістю саме vim, а vi має ще потужніші можливості. якби ви опанували навіть лише ці інструменти середнього рівня, виявилося б, що й макрокоманди не потрібні, тому що ця мова обробки текстів сама собою досить компактна й потужна, аби легко виконувати більшість необхідних операцій з текстом.

невеликий взірець складніших прийомів

є декілька команд, доступних через нотацію з :, зокрема техніка глобальної заміни :% s/foo/bar/g (це ще не складно, але інші команди з : можуть бути складними). цей набір команд, доступних через :, vi отримав у спадок від ed (рядкового редактора) та ex (розширеного рядкового редактора). власне, і саму назву vi отримав тому, що це візуальний інтерфейс (visual interface) для ex.

команди : зазвичай оперують рядками тексту. ed та ex було написано в епоху, коли термінали з екранами ще не набули поширення, і більшість терміналів були телетайпними пристроями. отож, звичним було працювати з роздрукованими копіями текстів, набираючи команди через надзвичайно повільні інтерфейси (звичайною швидкістю ліній передачі була 110 бодів, або приблизно 11 символів на секунду — це повільніше за темп друку швидкої друкарки; затримки вводу були звичною справою для багатокористувацьких сесій; на додачу, папір зазвичай треба було економити).

отже, синтаксис більшости команд : включав адресу або адресний інтервал (номери рядків), за котрим слідувала команда. зазвичай можна було вживати буквально номери рядків: :127,215 s/foo/bar для заміни першої згадки «foo» на «bar» в кожному рядку починаючи з 127-го аж до 215-го. можна було використовувати деякі скорочення, приміром . та $ на позначення відповідно першого та останнього рядка. можна було використовувати відносні префікси + та - для позначення зміщення додолу чи догори відносно поточного рядка. таким чином, :.,$j означало «від поточного рядка до останнього, з’єднати всі в один рядок». :% було синонімом до :1,$ (всі рядки).

команди :... g та :... v вартують тлумачення, бо вони надзвичайно потужні. :... g є префіксом для глобального (в усьому тексті) застосування наступної команди до всіх рядків, що відповідають певному шаблону (регулярному виразу), а :... v застосовує команду до всіх рядків, котрі не відповідають шаблону (мнемоніка: v від conVerse). подібно до інших команд ex, до цих так само можна було додати адресний префікс; приміром, :.,+21g/foo/d означає «видалити будь-які рядки, що містять текст „foo“, між поточним аж до 21-го наступного рядка», а :.,$v/bar/d означає «звідси і до кінця файлу, видалити будь-які рядки, що не містять текст „bar“».

цікаво, що на створення добре відомої в світі unix команди grep надихнуло те (на це навіть вказує назва), як в документації ex було згадано команду для виводу всіх рядків, що відповідали певному шаблону (регулярному виразу, regular expression, re) :g/re/p. адже команда :p була однією з перших, що її вивчали для користування редакторами ed чи ex, і частенько найпершою, що її застосовували, починаючи редагувати якийсь файл: вона слугувала для друку поточного вмісту (зазвичай одну сторінку за раз, набираючи :.,+25p або щось подібне).

зауважте, що :% g/.../d або (зворотній варіант) :% v/.../d використовують доволі часто; але є й інші команди ex, що їх варто згадати.

можна використовувати m для переміщення рядків, або j для їх з’єднування. приміром, є якийсь перелік, і треба відділити котрісь рядки, що відповідають (або не відповідають) певному шаблону, але не видаляючи їх, можна використати щось таке: :% g/foo/m$, і всі такі рядки буде переміщено додолу, в кінець файлу (зауважте, як можна використовувати кінець файлу як простір для нотаток). це збереже відносний порядок рядків, відділивши їх від основного переліку (це еквівалентно чомусь такому: 1G!GGmap!Ggrep foo<ENTER>1G:1, 'a g/foo'/d, тобно «створити копію всього тексту у «хвості» файлу, відфільтрувати цей «хвіст» за допомогою grep, і видалити зайве з «голови» файлу).

для з’єднання рядків зазвичай я можу визначити шаблон для рядків, які треба приєднати до попередніх (приміром, всі рядки, що починаються з «^ », але не з «^ * » в якомусь ненумерованому переліку); в такім випадку я використаю таке: :% g/^ /-1j (для кожного рядка за шаблоном, піднятися на рядок вище й приєднати наступний).

чи не зайве нагадувати, що можна скористатися знайомою командою s (заміна, substitute) у поєднанні з g чи v (global/converse-global). зазвичай у цім нема потреби; але уявіть випадок, коли треба застосувати заміну лише до рядків, котрі відповідають певному шаблону. часто можна використати досить складний шаблон із пам’яттю фрагментів (capture) та зворотніми посиланнями (back reference) для збереження фрагментів, котрі не треба змінювати. зазвичай, часто простіше відділити шаблон від заміни: :% g/foo/s/bar/zzz/g — в кожнім рядку, де є «foo», замінити «bar» на «zzz» (бо щось подібне до :% s/\(.*foo.*\)bar\(.*\)/\1zzz\2/g працюватиме лише тоді, коли «foo» передує «bar»; команда й так вже заскладна, і зробиться ще незграбнішою, якщо намагатися налаштувати її для виловлювання ще й усіх випадків, коли «bar» іде поперед «foo»).

суть зауваги в тім, що в наборі команд ex є не лише p, s та d.

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

це вже виглядає досить непрозоро (особисто я користувався чимось подібним хіба декілька разів за попередні 15 років). однак я визнаю, що частенько робив щось таке, в ітеративному чи інтерактивному режимі, що можна було би, мабуть, зробити ефективніше якби я витратив трохи більше часу на побудову команди.

інша дуже помічна команда vi чи ex — :r для читання вмісту іншого файлу. :r foo вставить вміст файлу foo в поточний рядок.

але ще потужнішою є команда :r!, — вона читає результат виконання [зовнішньої термінальної] команди. результат такий, як наче ми призупинили сесію vi, запустили якусь команду, зберегли її вивід до тимчасового файлу, повернулися до сесії vi й вставили вміст цього тимчасового файлу.

і ще більш потужнішими є команди ! та :... !. вони так само запускають зовнішні команди й читають їх вивід до поточного тексту; але вони дозволяють застосувати зовнішні команди як фільтр для нашого тексту! наприклад, можна впорядкувати всі рядки нашого файлу за допомогою 1G!Gsort (G є наче командою переходу goto для vi; без адреси вона переходить до останнього рядка файлу, але приймає адресний префікс, як от 1, тобто перший рядок). це рівнозначно такій команді у нотації ex: :1,$!sort. автори часто використовують ! у поєднанні з командами unix fmt або fold для переформатування тексту або застосування автоматичного переносу до фрагментів тексту. поширеною макрокомандою є {!}fmt (переформатувати поточний абзац). програмісти часто застосовують цей підхід, щоби застосувати до свого коду чи його фрагментів автоматичну перевірку відступів чи інші інструменти форматування коду.

використання :r! чи ! означає, що будь-яку зовнішню [консольну] команду можна використати як розширення редактора. час від часу я використовував це зі скриптами, що витягали дані з бази даних, чи із wget або lynx для імпорту даних із веб-сайтів, або з ssh для використання виводу з віддалених комп’ютерів.

інша корисна команда ex — :so (скорочено від «source», джерело); вона читає вміст файлу як послідовність команд ex. коли vi стартує, він насправді виконує команду :source щодо файлу ~/exinitrc (а vim, природньо, читає так файл ~/.vimrc). це може бути корисно, якщо треба «на льоту» змінити налаштування редактора, просто завантаживши файл з новим набором макрокоманд, скорочень або налаштувань. якщо схитрити, можна навіть скористатися цим для зберігання цілих ланцюжків готових команд редагування, які згодом застосовувати до тих чи інших файлів за потреби.

приміром, я маю файл з сімома рядками (лише 36 символів!), котрий пропускає поточний текст через wc та додає коментар у нотації c з інформацією про кількість слів у тексті. я можу застосувати цей «макрос» до будь-якого текстового файлу (наприклад, mytarget) за допомогою команди vim +'so mymacro.ex' ./mytarget.

(опцію + для vi та vim зазвичай використовують, аби на старті помістити курсор на певний рядок; однак мало хто здогадується, що за + може йти будь-яка команда чи вираз ex, як от source у наведеній вище команді; для простого прикладу, я маю скрипти, що викликають vi +'/foo/d|wq!' ~/.ssh/known_hosts для неінтерактивного видалення певного рядка зі списку відомих вузлів ssh, коли я переналаштовую свій парк серверів).

втім, зазвичай такі макрокоманди простіше переписати за допомогою perl, awk чи sed (до слова, ще одна утиліта, натхнена командою ed, подібно до grep).

команда @ є, мабуть, найбільш непрозорою серед усіх команд vi. викладаючи на курсах системного адміністрування для вже досвічених користувачів майже впродовж десяти років, я стрічав дуже мало людей, котрі б колись послуговувалися нею. @ виконує вміст регістру так, наче це команда vi чи ex. приклад: я часто використовую :r!locate ..., щоби знайти якийсь файл на моєму комп’ютері, і вставити його назву в документ. далі я видаляю «зайві» знайдені рядки, залишаючи лише повний шлях до одного потрібного мені файлу. радше як довго перебирати tab‘ом компоненти шляху до файлу (або ще гірше — якщо я на комп’ютері, де tab не працює з якихось причин), я просто роблю так:

1) 0i:r (перетворити поточний рядок на команду :r); 2) "cdd (вирізати рядок і помістити в регістр «c»); 3) @c (виконати команду в регістрі «c»).

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

витверезна заувага

я тут ледь торкнувся поверхні справжньої сили vi, і жодна зі згаданих команд навіть не належить до тих покращень (improvements), за які vim отримав свою назву! все описане повинно працювати в будь-якій версії vi, котрій вже 20 чи 30 років.

є люди, котрі користуються значно більшою часткою можливостей vi, аніж я колись навчусь.