- PVSM.RU - https://www.pvsm.ru -
gogrep [1] — это одна из моих любимых утилит для работы с Go. Она позволяет находить код по синтаксическим шаблонам, фильтровать результаты по типам выражений, а также выполнять замену (тоже по шаблону).
В этой заметке я расскажу как использовать gogrep
, а также о VS Code расширении [2] для более удобной работы с gogrep
прямо из редактора.
Если в тезисах, то gogrep
может быть полезен при:
Рассмотрим пример, который демонстрирует изящность и эффективность структурного поиска.
Функции a()
и b()
выполняют одинаковые операции:
func a(xs []int) []int {
xs = append(xs, 1)
xs = append(xs, 2)
return xs
}
func b(xs []int) []int {
xs = append(xs, 1, 2)
return xs
}
Допустим, мы хотим переписать все места, где вызовы append
можно схлопнуть.
Попробуем gogrep
:
-x
шаблона $x=append($x,$a); $x=append($x,$b)
-s
шаблон $x=append($x,$a,$b)
получаем искомую замену-w
все затронутые файлы будут обновлены.gogrep -w -x '$x=append($x,$a);$x=append($x,$b)' -s '$x=append($x,$a,$b)' ./...
Если поставить расширение для VS Code [2], то становится ещё проще.
Вот пример замены +=1
на ++
:
Пример из реальной жизни: как-то захотел выполнить замену slice[:] -> slice
. Даже заводил issue в staticcheck [4]. Специфика в том, что нельзя просто искать [:]
, потому что брать такой слайс от массива имеет смысл, а вот от строки или слайса — нет.
Вот пример того, как можно найти лишние слайсы от []byte
в stdlib:
# Только поиск.
gogrep -x '$s[:]' -a 'type([]byte)' std
# Поиск+замена.
gogrep -x '$s[:]' -a 'type([]byte)' -s '$s' -w std
Показываю только первые 30 результатов (всего их 300+):
$GOROOT/src/archive/tar/format.go:163:59: b[:]
$GOROOT/src/archive/tar/reader.go:345:33: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:348:17: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:348:28: zeroBlock[:]
$GOROOT/src/archive/tar/reader.go:349:34: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:352:18: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:352:29: zeroBlock[:]
$GOROOT/src/archive/tar/reader.go:396:23: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:497:36: blk[:]
$GOROOT/src/archive/tar/reader.go:528:33: blk[:]
$GOROOT/src/archive/tar/reader.go:531:14: blk[:]
$GOROOT/src/archive/tar/writer.go:392:26: blk[:]
$GOROOT/src/archive/tar/writer.go:477:23: zeroBlock[:]
$GOROOT/src/archive/zip/reader.go:233:29: buf[:]
$GOROOT/src/archive/zip/reader.go:236:15: buf[:]
$GOROOT/src/archive/zip/reader.go:251:30: buf[:]
$GOROOT/src/archive/zip/reader.go:254:15: buf[:]
$GOROOT/src/archive/zip/writer.go:92:17: buf[:]
$GOROOT/src/archive/zip/writer.go:110:19: buf[:]
$GOROOT/src/archive/zip/writer.go:116:30: buf[:]
$GOROOT/src/archive/zip/writer.go:132:27: buf[:]
$GOROOT/src/archive/zip/writer.go:157:17: buf[:]
$GOROOT/src/archive/zip/writer.go:177:27: buf[:]
$GOROOT/src/archive/zip/writer.go:190:16: buf[:]
$GOROOT/src/archive/zip/writer.go:198:26: buf[:]
$GOROOT/src/archive/zip/writer.go:314:18: mbuf[:]
$GOROOT/src/archive/zip/writer.go:319:31: mbuf[:]
$GOROOT/src/archive/zip/writer.go:386:16: buf[:]
$GOROOT/src/archive/zip/writer.go:398:23: buf[:]
$GOROOT/src/bytes/bytes.go:172:24: b[:]
Поисковой шаблон — это небольшой фрагмент Go кода, который может включать в себя $-выражения (мы будем называть их "переменными шаблона"). Шаблон может быть выражением, statement (или их списком) или декларацией.
Переменные шаблона — это Go переменные с префиксом $
. Переменные шаблона с одинаковым именем всегда захватывают идентичные элементы AST. Исключением является переменная с именем $_
, их можно использовать для обозначения "что угодно".
Перед именем переменной шаблона можно поставить *
, тогда переменная будет захватывать произвольное количество элементов.
Поисковой шаблон | Интерпретация |
---|---|
$_ |
Что угодно. |
$x |
Идентично первому примеру, "что угодно". |
$x = $x |
Самоприсваивание. |
(($_)) |
Любое выражение в двойных скобках. |
if $init; $cond {$x} else {$x} |
if с дублирующимися then/else блоками. |
fmt.Fprintf(os.Stdout, $*_) |
Вызов Fprintf с аргументом os.Stdout . |
Как уже демонстрировалось в примере с append()
, шаблон может содержать несколько statement'ов. Нотация "$x; $y
" означает "найди $x, за которым следует $y".
gogrep
выполняет честный backtracking для шаблонов с *
. К примеру, шаблоном можно найти все map
литералы, где есть хотя бы один дублирующийся ключ:
map[$_]$_{$*_, $key: $val1, $*_, $key: $val2, $*_}
Ранее мы использовали параметры -x
и -s
, не разбирая что они из себя представляют.
gogrep
оперирует командами, которые составляют конвейер (pipeline). Порядок команд имеет значение. Полный синопсис выглядит следующим образом:
gogrep commands... [targets...]
target
может быть файлом, директорией или пакетом. Всё эквивалентно тому, как обрабатывает аргументы команда go build
.
Команда | Описание |
---|---|
-x pattern |
Найти все элементы AST, которые подходят под pattern . |
-g pattern |
Отбросить результаты, которые не подходят под pattern . |
-v pattern |
Отбросить результаты, которые подходят под pattern . |
-a attr |
Отбросить результаты, которые не имеют атрибута attr . |
-s pattern |
Переписать результат, используя pattern . |
-p n |
Для каждого результата, подняться на n уровней по AST. |
Как можно догадаться, -x
чаще всего является первой командой в конвейере. Затем могут следовать фильтрующие команды или модифицирующие команды.
Рассмотрим это всё на примерах.
// file foo.go
package foo
func bar() {
println(1)
println(2)
println(3)
}
# Находим все вызовы println()
$ gogrep -x 'println($*_)' foo.go
foo.go:4:2: println(1)
foo.go:5:2: println(2)
foo.go:6:2: println(3)
# Добавляем команды -v для отбрасывания всех результатов,
# где есть литерал 1, а затем литерал 2.
$ gogrep -x 'println($*_)' -v 1 -v 2 foo.go
foo.go:6:2: println(3)
# Дополнительно поднимаемся на 2 уровня выше
# и доходим до содержащего *ast.BlockStmt.
$ gogrep -x 'println($*_)' -v 1 -v 2 -p 2 foo.go
foo.go:3:12: { println(1); println(2); println(3); }
Атрибуты довольно много, большая часть из них очень ситуативная, а документации на них нет совсем. Остаётся смотреть в исходниках [5].
Одним из наиболее полезных атрибутов является type
:
# Матчит и сложение, и конкатенацию.
gogrep -x '$lhs + $rhs'
# Матчит только конкатенацию.
gogrep -x '$lhs + $rhs' -a 'type(string)'
По умолчанию gogrep
не выполняет поиск в тестовых файлах. Чтобы это исправить, стоит передавать аргумент -tests
.
Все предоставляемые функции сводятся к нескольким командам (Ctrl+Shift+P
или Cmd+Shift+P
):
Каждая команда запрашивает поисковой шаблон:
Результаты печатаются в канал (output channel) gogrep
:
Для search and replace нужно разделять части "Find" и "Replace" токеном ->
:
Если убрать из шаблона !
, то вместо изменений файлов inplace в канал будут распечатаны кандидаты для замены.
Пример поиска тех самых комбинируемых append
(но без replace):
По умолчанию за командами расширения не назначено никаких горячих клавиш. Если вам нужен более быстрый доступ к поиску, вы можете назначить их самостоятельно, следуя личным предпочтениям эргономики.
Пока что автоматическая установка бинарника gogrep
предусмотрена только для GOARCH=amd64
и GOOS=linux|windows|darwin
.
Расширение не предоставляет возможностей использовать атрибуты или произвольные конвейеры. Интегрированы только -x
и -s
.
Если вам не хватает какого-то функционала или вы нашли баг, не стесняйтесь и не ленитесь открывать issue на GitHub [6].
Надеюсь, эта заметка поможет этому замечательному инструменту стать хотя бы немного популярнее.
Если вы используете продукты JetBrains, то вам может быть знаком механизм structural search and replace [7] (SSR). Он решают ту же задачу, но, в отличие от SSR, gogrep
удобнее запускать в произвольном окружении, так как это обычная утилита командной строки.
Дополнительные материалы:
Автор: Искандер
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/open-source/353755
Ссылки в тексте:
[1] gogrep: https://github.com/mvdan/gogrep
[2] VS Code расширении: https://marketplace.visualstudio.com/items?itemName=quasilyte.gogrep
[3] ruleguard: https://habr.com/ru/post/481696/
[4] issue в staticcheck: https://github.com/dominikh/go-tools/issues/282
[5] исходниках: https://github.com/mvdan/gogrep/blob/24e8804e5b3cbe82de972195f127eb3c3592d94b/parse.go#L362
[6] открывать issue на GitHub: https://github.com/quasilyte/vscode-gogrep/issues/new
[7] structural search and replace: https://www.jetbrains.com/help/idea/structural-search-and-replace.html
[8] Daniel Martí рассказывает о gogrep: https://talks.godoc.org/github.com/mvdan/talks/2018/gogrep.slide
[9] Множество примеров gogrep шаблонов: https://github.com/quasilyte/go-ruleguard/blob/master/rules.go
[10] phpgrep: https://habr.com/ru/post/464893/
[11] VS Code расширение для phpgrep: https://marketplace.visualstudio.com/items?itemName=quasilyte.phpgrep
[12] Источник: https://habr.com/ru/post/505652/?utm_source=habrahabr&utm_medium=rss&utm_campaign=505652
Нажмите здесь для печати.