Главная причина учить :s всерьёз, а не запоминать пару заклинаний: это декларативный инструмент. Вы один раз описываете что и где изменить, и Vim применяет правило к сотням строк за миллисекунды - без макросов, без ручного перебора. А диалект регулярных выражений Vim, при всей своей исторической причудливости, умеет вещи, которых нет в обычном PCRE: точечные границы совпадения через \zs/\ze, вычисляемые замены через \=, преобразование регистра прямо в строке замены.
В этой главе мы разберём синтаксис от и до, а в главе 12 поднимемся на уровень выше - к :global, которая использует :s как одну из своих "боеголовок".
Анатомия команды
Общий синтаксис выглядит так:
Код: Выделить всё
:[range]s/{pattern}/{string}/[flags] [count]
- [range] - на каких строках работать (по умолчанию - только текущая строка);
- {pattern} - что искать (регулярное выражение в диалекте Vim);
- {string} - на что заменять (с поддержкой ссылок на группы и спецзамен);
- [flags] - модификаторы поведения (g, c, i, ...);
- [count] - сколько строк обработать, начиная с последней строки диапазона.
Код: Выделить всё
:s#/usr/local#/opt# " удобно для путей
:s,old,new,g " запятая тоже годится
Критичный момент для новичков: без флага g команда :s заменяет только первое совпадение в каждой строке. Это историческое наследие ed/sed, и оно постоянно подводит. Хотите заменить все вхождения в строке - добавляйте g (от global within line, не путать с командой :global).
Код: Выделить всё
Было: foo foo foo
:s/foo/bar/
Стало: bar foo foo
Было: foo foo foo
:s/foo/bar/g
Стало: bar bar bar
Диапазон - это то, что превращает :s из точечной правки в массовый инструмент. Адреса строк разделяются запятой.
Код: Выделить всё
Range | Значение
---------------+-----------------------------------------------------------------
(пусто) | только текущая строка
% | весь файл (от первой до последней строки)
1,$ | то же, что % ($ - последняя строка)
.,$ | от текущей строки до конца
.,+5 | текущая строка и пять следующих
-3,. | три строки выше и текущая
'<,'> | визуальное выделение (появляется автоматически)
'a,'b | от метки a до метки b (о метках - [глава 9](09-marks-jumps.md))
10,20 | строки с 10 по 20
/start/,/end/ | от строки со start до строки с end
Код: Выделить всё
" выделили несколько строк через V, нажали :
:'<,'>s/TODO/DONE/g
Флаги
Флаги пишутся слитно после третьего разделителя и меняют поведение команды.
Код: Выделить всё
Флаг | Действие
------+----------------------------------------------------------------------
g | заменить все вхождения в строке, а не только первое
c | спрашивать подтверждение перед каждой заменой
i | игнорировать регистр в этом поиске (ignorecase локально)
I | учитывать регистр, даже если включён ignorecase
n | не заменять, а только посчитать совпадения
e | не выдавать ошибку, если паттерн не найден
& | сохранить флаги предыдущей :s (должен стоять первым в списке флагов)
Флаг c - интерактивный. Vim останавливается на каждом совпадении и ждёт ответа:
Код: Выделить всё
Клавиша | Ответ
---------------+---------------------------------------------
y | заменить это
n | пропустить
a | заменить это и все оставшиеся (all)
l | заменить это и остановиться (last)
q | прекратить
<C-e> / <C-y> | прокрутить экран вверх/вниз, не теряя место
Код: Выделить всё
:%s/TODO//gn
" -> "17 matches on 12 lines" - файл не изменился
Диалект регулярных выражений Vim управляется опцией magic. По умолчанию ., *, [] - спецсимволы, а вот +, ?, (), {}, | нужно экранировать обратным слешем. Это раздражает: \(\d\+\) вместо привычного (\d+).
Решение - very magic, режим \v. Поставьте \v в начало паттерна, и почти всё, кроме букв, цифр и _, становится спецсимволом - синтаксис приближается к Perl/PCRE.
Код: Выделить всё
Хочу | Без \v (magic) | С \v (very magic)
-----------------+------------------+---------------------+-----+------+--
группа | \(...\) | (...)
один или больше | \+ | +
ноль или один | \? | ?
альтернатива | \ | | \ | -> |
количество | \{2,4} | {2,4}
граница слова | \<word\> | <word>
Код: Выделить всё
" Поменять местами два слова. Сравните читаемость:
:%s/\(\w\+\) \(\w\+\)/\2 \1/ " magic
:%s/\v(\w+) (\w+)/\2 \1/ " very magic
Код: Выделить всё
" Найти буквально C:\path\to (точки и слеши не спецсимволы)
:%s/\Vorder.total/order.subtotal/g
Группы захвата и обратные ссылки
Группа \(...\) (или (...) в \v) запоминает совпавший кусок. В строке замены к нему обращаются через \1, \2, ... \9 - по порядку открывающих скобок. А \0 или & означают всё совпадение целиком.
Код: Выделить всё
Было: Ivan Petrov
:s/\v(\w+) (\w+)/\2, \1/
Стало: Petrov, Ivan
Код: Выделить всё
Было: 2026-06-23
:s/\v(\d{4})-(\d{2})-(\d{2})/\3.\2.\1/
Стало: 23.06.2026
Код: Выделить всё
Было: important
:s/important/[&]/
Стало: [important]
Было: 42
:s/\d\+/<&>/
Стало: <42>
\zs и \ze - изобретение Vim вместо lookaround
Вот фича, которой стоит научиться отдельно, потому что в обычных regex такого нет. \zs задаёт начало совпадения, \ze - его конец. Текст до \zs и после \ze обязан совпасть с паттерном (это контекст), но в само совпадение - а значит, и в замену - не входит.
Это даёт эффект lookbehind/lookahead, но проще и быстрее. У Vim есть и настоящие ассерции (\@<= для lookbehind, \@= для lookahead), но они медленнее из-за бэктрекинга и читаются тяжело. На практике 2026 в подавляющем большинстве позиционных задач берут именно \zs/\ze.
Код: Выделить всё
" Заменить bar только когда перед ним foo (а перед foo - ничего особого)
Было: foo bar baz
:s/foo \zsbar/BAR/
Стало: foo BAR baz
" "foo " осталось нетронутым, потому что оно до \zs
Код: Выделить всё
" Поднять регистр названия только внутри тега, не трогая скобки
Было: <name>cyberlake</name>
:s/<name>\zs.\{-}\ze<\/name>/\U&/
Стало: <name>CYBERLAKE</name>
Вычисляемая замена через \=
Когда статической строки замены недостаточно, начните строку замены с \= - и всё, что после, Vim вычислит как Vimscript-выражение. Внутри выражения доступна функция submatch(): submatch(0) - всё совпадение, submatch(1)...submatch(9) - группы. В Neovim \= тоже вычисляет именно Vimscript; чтобы дотянуться до Lua-функции, обращайтесь к ней через мост v:lua.<func>() или luaeval('...') - напрямую писать Lua внутри \= нельзя.
Код: Выделить всё
" Удвоить каждое число в файле
Было: x=3 y=10
:%s/\d\+/\=submatch(0)*2/g
Стало: x=6 y=20
Код: Выделить всё
" Пронумеровать строки: добавить порядковый номер в начало.
" line('.') даёт номер текущей строки.
:%s/^/\=line('.').'. '/
Код: Выделить всё
" Прибавить 1 к каждому захваченному числу
Было: item5 item9
:%s/\vitem(\d+)/\='item'.(submatch(1)+1)/g
Стало: item6 item10
Спецзамены: регистр и переносы строк
В строке замены доступны управляющие последовательности, которые меняют регистр или вставляют спецсимволы. Это чисто "выходная" магия - она не влияет на поиск, только на то, что подставляется.
Код: Выделить всё
Последовательность | Действие
--------------------+----------------------------------------------------------------------------------------------
\u | следующий символ - в верхний регистр
\l | следующий символ - в нижний регистр
\U | всё дальнейшее - в ВЕРХНИЙ, пока не встретит \E
\L | всё дальнейшее - в нижний, пока не встретит \E
\E | завершить действие \U/\L
\r | вставить перенос строки (разбить строку на две)
\n | вставить NUL-байт (а не перевод строки!)
\\ | литеральный обратный слеш
\t | табуляция
\& | литеральный амперсанд (иначе & = всё совпадение; при включённом magic, то есть по умолчанию)
Код: Выделить всё
" Сделать заголовок: каждое слово с заглавной буквы
Было: привет мир cyberlake
:s/\v<(\w)/\u\1/g
Стало: Привет Мир Cyberlake
" Весь матч в верхний регистр
Было: константа
:s/\w\+/\U&/
Стало: КОНСТАНТА
" Разбить строку по запятой на несколько строк
Было: a,b,c
:s/,/\r/g
Стало: a
b
c
Код: Выделить всё
" CamelCase: убрать дефисы и поднять следующую букву
Было: my-long-name
:s/\v-(\w)/\u\1/g
Стало: myLongName
По умолчанию паттерн :s живёт внутри одной строки: . не совпадает с переводом строки, классы \s, \w и т.п. тоже им ограничены. Но иногда замена должна перешагнуть границу строк - например, схлопнуть перенос или вырезать кусок, занимающий две строки.
- \n в паттерне ищет перевод строки (не путать с \n в замене, где это NUL - см. ловушку выше).
- \_. - "точка, включая перевод строки": любой символ, в том числе конец строки.
- \_s, \_w, \_[...] - любой класс с приставкой \_ дополнительно захватывает перевод строки. Так \_s - это "пробельный символ ИЛИ конец строки".
Код: Выделить всё
" Склеить строку, оканчивающуюся на запятую, со следующей
Было: foo,
bar
:%s/,\n/, /
Стало: foo, bar
" Удалить перенос внутри тега, охватывающего две строки
Было: <a
href>
:%s/<a\_s\+href>/<a href>/
Стало: <a href>
gdefault: ловушка, инвертирующая флаг g
Опция 'gdefault' меняет смысл флага g на противоположный: при :set gdefault команда :s/foo/bar/ заменяет все вхождения в строке, а :s/foo/bar/g - только первое. Идея в том, чтобы не дописывать g постоянно, но цена - сломанная переносимость: любой ваш :s начинает зависеть от чужой настройки, а в скрипте g означает то одно, то другое.
Практика 2026: не включайте gdefault. Это давно считается анти-паттерном (её даже убрали из некоторых дистрибутивов конфигов). Если хотите "по умолчанию global", заведите маппинг или абревиатуру, а сам флаг g оставьте однозначным. Когда читаете чужой :s и поведение g кажется "перевёрнутым" - первым делом проверьте :verbose set gdefault?.
:s всегда построчна (даже из visual-block)
Частое заблуждение: если выделить колонку в блочном визуальном режиме (<C-v>) и нажать :, кажется, что замена ограничится столбцом. Это не так. : подставляет диапазон '<,'> - а это всегда строковый диапазон от строки начала выделения до строки конца. Колонки теряются: :s отработает по всей ширине каждой из этих строк, как если бы вы выделяли их через V.
Код: Выделить всё
" Выделили <C-v> только первый столбец из трёх строк, нажали :
:'<,'>s/x/y/g " заменит x во ВСЕЙ строке, а не только в столбце
Повтор замены: :s, :&, g&
После того как вы один раз написали :s/.../.../, не нужно набирать её заново. Vim хранит последний паттерн (общий с поиском /) и последнюю замену.
Код: Выделить всё
Команда | Действие
------------+---------------------------------------------------------------------------------------------------------------------
:s | повторить последнюю :s на текущей строке с теми же паттерном и заменой, но без флагов
:s {flags} | повторить с заново указанными флагами (например :s g); прежние флаги при этом не наследуются
:& | повторить последнюю :s без флагов (синоним голой :s)
:&& | повторить с сохранением флагов предыдущей :s (единственный "короткий" способ их унаследовать)
g& | повторить последнюю :s на всём файле (%) и с последним поисковым паттерном @/, сохраняя флаги - фактически :%s//~/&
& (normal) | то же, что :s - повтор на текущей строке без флагов
g& особенно мощна. Представьте: вы сделали :s/foo/bar/g на одной строке, проверили результат, и хотите применить ко всему файлу. Просто нажмите g&. Она ещё и подхватывает текущий поисковый паттерн (@/), а не тот, что был в момент :s - поэтому ей можно "скормить" новый паттерн через /, * или let @/=..., оставив прежнюю замену.Важно про флаги при повторе. Голая :s, :& и :s {flags} флаги предыдущей подстановки не сохраняют (:help :&&, :s_flags: "Note that :s and :& don't keep the flags."). :s g - это не "повтор с теми же флагами", а повтор с единственным флагом g, заданным заново. Унаследовать прежний набор флагов можно только через ведущий флаг &: это :&& либо явная форма :s/.../.../&.
Код: Выделить всё
:s/old/new/g " попробовали на одной строке
" результат устроил - применяем ко всему файлу:
g&
Живой предпросмотр: inccommand (только Neovim)
Эксклюзивная фича Neovim, которой нет в классическом Vim, - inccommand. Она показывает результат :s прямо по мере набора команды.
Код: Выделить всё
vim.o.inccommand = 'split' -- или 'nosplit'
- nosplit - подсвечивает изменения инлайн, в самом буфере;
- split - дополнительно открывает окно предпросмотра со всеми совпадениями, в том числе за пределами экрана.
Рецепты на каждый день
Несколько готовых шаблонов, которые стоит держать в памяти.
Код: Выделить всё
" Убрать пробелы в конце строк
:%s/\s\+$//
" Схлопнуть несколько пробелов в один
:%s/\s\{2,}/ /g
" Удалить пустые строки (только из пробелов - тоже)
:%s/^\s*$// " очистит, но строки останутся; для удаления см. :g в главе 12
" Поменять местами поля CSV (первое и второе)
:%s/\v^([^,]+),([^,]+)/\2,\1/
" Обернуть каждое число в кавычки
:%s/\v<(\d+)>/'\1'/g
" snake_case -> camelCase
:%s/\v_(\w)/\u\1/g
" Заменить слово под курсором во всём файле (через *, затем пустой паттерн)
" 1) навели курсор, нажали * 2):
:%s//replacement/g
" Посчитать вхождения, ничего не меняя
:%s/pattern//gn
Лучшие практики 2026
- Начинайте нетривиальные паттерны с \v. Very magic делает выражения читаемыми и переносимыми. В скриптах фиксация магического режима (\v/\m/\M/\V) обязательна - иначе паттерн зависит от чужой опции magic.
- Предпочитайте \zs/\ze ассерциям \@<=/\@=. Они быстрее (нет дорогого бэктрекинга) и понятнее. Настоящие lookaround держите для редких случаев, где контекст сам по себе сложный.
- Используйте inccommand=split в Neovim. Живой предпросмотр заменяет "слепую" отправку команды и снижает потребность во флаге c. Для классического Vim такой опции нет - там полагайтесь на c.
- Связывайте поиск и замену через пустой паттерн. Найдите цель через / или *, проверьте навигацией, затем :%s//новое/g - это безопаснее, чем набирать паттерн дважды и ошибиться.
- Считайте перед тем, как менять. :%s/pat//gn показывает масштаб операции. Особенно важно перед g& или заменой во всём файле.
- \= для всего, что не статично. Арифметика, нумерация, printf - не пытайтесь вытащить это статической заменой, переходите на выражение.
- Для рефакторинга в одном файле - cgn+., а не :s///c. Точечный, повторяемый, визуально контролируемый способ (детали - глава 14).
- Забыли флаг g. Заменилось только первое вхождение в каждой строке. Самая частая жалоба новичка.
- Перепутали \n и \r в замене. В строке замены перевод строки - это \r, а \n вставляет NUL-байт и портит файл. В паттерне всё наоборот: \n ищет перевод строки.
- Жадный .* вместо ленивого .\{-}. :s/<.*>// на строке <a> <b> удалит всё от первого < до последнего >. Нужен \{-}: :s/<.\{-}>//g.
- Зависимость от magic в скрипте. Паттерн без явного режима ломается у пользователя с нестандартным magic. Всегда фиксируйте режим в коде.
- g& подхватывает текущий @/, а не паттерн из последней :s. Если вы между делом что-то искали, g& возьмёт новый поисковый паттерн. Иногда это удобно, иногда - сюрприз.
- & в строке замены вставляет всё совпадение. Чтобы получить буквальный амперсанд, экранируйте: \&.
- Диапазон по умолчанию - одна строка. Без % или явного range :s тронет только текущую строку, и кажется, что "не сработало".
- Lookaround через \@<!/\@= ради позиции. Чаще всего это переусложнение - \zs/\ze решают ту же задачу проще.
- Включённый gdefault переворачивает g. При :set gdefault :s/.../.../ меняет все вхождения, а g - наоборот, одно. Не включайте эту опцию; если поведение g странное - проверьте :verbose set gdefault?.
- Ждать, что :s из visual-block тронет только столбец. '<,'> - всегда строковый диапазон; :s отработает по всей строке. Для колонок - блочные операции или атом \%V.
- \n в паттерне vs в замене. В паттерне \n - перевод строки (так делают многострочные замены), в замене \n - NUL. Для переноса в замене нужен \r.
- Перестановка имён. В файле список вида Имя Фамилия по строке. Одной командой получите Фамилия, Имя. (Подсказка: \v, две группы, ссылки \1/\2.)
- Даты. Превратите все YYYY-MM-DD в DD.MM.YYYY. Проверьте, что не трогаются строки без дат.
- Удвоение чисел. Дан текст с числами. Используя \= и submatch(), умножьте каждое число на 10.
- Заголовочный регистр. Строку один два три cyberlake приведите к виду Один Два Три Cyberlake (каждое слово с заглавной). Затем отдельной командой сделайте всю строку ВЕРХНИМ регистром.
- Тег наружу, содержимое внутрь. В строке <title>привет</title> поднимите в верхний регистр только текст между тегами, не трогая сами теги. (Подсказка: \zs, \ze, \{-}, \U/\E.)
- Повтор и распространение. Сделайте :s/foo/bar/g на одной строке, убедитесь в результате, затем примените ко всему файлу одним нажатием. Какой клавишей? А как посчитать, сколько foo осталось бы заменить во всём файле, ничего не меняя?
- :[range]s/pat/rep/flags - описываете правило, Vim применяет его к диапазону. По умолчанию диапазон - текущая строка, замена - первое вхождение.
- Диапазон делает команду массовой: % (весь файл), '<,'> (выделение), .,+N (отсюда и ниже).
- Флаги: g (все вхождения), c (подтверждение), i/I (регистр), n (только посчитать), e (без ошибки).
- \v (very magic) превращает диалект Vim в PCRE-подобный - учите паттерны с ним; \V - для почти буквального текста.
- Группы \(...\)/(...) и ссылки \1...\9, &/\0 - всё совпадение. \zs/\ze точно режут границы совпадения - фирменная замена lookaround.
- \= запускает вычисляемую замену с submatch(); \U/\L/\u/\l/\E правят регистр, \r вставляет перевод строки.
- Повтор: голая :s/:&/:s {flags} флаги не сохраняют; унаследовать их можно только ведущим & - то есть :&& (или :s/.../.../&); g& - на весь файл с текущим паттерном. Пустой паттерн = последний поисковый.
- Через границы строк ищут \n (в паттерне), \_., \_s; :s всегда построчна (visual-block даёт строковый диапазон, не колонки); 'gdefault' инвертирует g - не включать.
- В Neovim включите inccommand для живого предпросмотра - это меняет ощущение от замены целиком.
- Для замены по многим файлам - глава 14 (:grep, quickfix, :cfdo); :global как движок пакетных правок - глава 12.