Предисловие

Ruby очень многие вещи позаимствовал из Perl. В том числе ключ интерпретатора, позволяющий выполнить кусок кода “сразу”. Например: ruby -e 'puts "Hello, World"'. Думаю, понятно, что за такое поведение отвечает ключ -e (execute, стоит полагать).

Это очень удобно. Как я помню, Ларри Уолл (создатель Perl) говорил, что одним из предназначений Perl была замена awk и sed. Действительно, если почитать его кэмел-бук, а именно справочный раздел, то можно обнаружить, что многие функции ссылаются на аналогичные в awk и sed.

Несколько лет назад я работал в одной компании, и мне поручили распарсить кучу логов, чтобы извлечь какую-то информацию. Парсить я планировал на питоне. Первым делом нужно было обратиться к сисадмину и попросить его дать мне эти логи. Собственно, на следующий же день он мне их отдал. Вместе с одной непонятной командой для консоли. “Она их распарсит” - сказал он. Действительно, этот маленький кусок кода сделал именно то, что мне нужно. Для меня это было открытием. Я был поражен тем, что такую задачу можно сделать, комбинируя 3 консольные утилиты. Через какое-то время узнал, что это называется Unix-way - программа должна выполнять одну мелкую задачу и выполнять ее хорошо. Как строительный блок. Кирпич.

Но это все лирика…

Для чего?

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

С помощью пайпов (pipes) вы объединяете несколько команд, передавя вывод одной на ввод другой. Например, вы можете прогрепать какой-то каталог ради поиска файлов по регулярке: ls some_dir | grep '\d*$' (поиск файлов, чье имя заканчивается цифрами).

Но что, если вам нужна какая-то предварительная обработка текста, но никакой команды для этого вы не знаете? Тут на помощь приходят интерпретаторы, которым вы четко сможете описать задачу.

Мой пример

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

Проводил рефакторинг одного модуля и решил переименовать один метод со сложным названием. Сказано - сделано, переименовал его в определении. Но что делать с остальными вхождениями этого метода? В проекте куча файлов. git grep METHOD_NAME выдал кучу всего.

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

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

Как я понимаю, обычно в таких случаях пользуются связкой awk + sed. Я не знаю ни того, ни другого, но зато я знаю Ruby!

git grep <...> | ruby -e '$<.each { |line| puts line.split(":")[0] }'

Теперь немного пояснений. Помимо некоторых синтаксических конструкций Ruby перенял из Perl еще и некоторые дефолтные переменные. Нас будут интересовать две из них: $_ и $<.

Первая в перле называется точно так же и устанавливается неявно в некоторых случаях, например, при чтении из файла. Вторая же в Perl не является переменной, но конструкцией <> - оператор ввода, в котором не указан поток. Она работает достаточно сложно. Если кратко, то это она ведет себя как IO и представляет собой либо STDIN, либо, если команда выполнена с аргументами, она считает свои аргументы списком файлов и представляет собой контактенацию их содержимого.

Пример:

ruby -e 'puts $_ while $<.gets' Gemfile
source 'https://rubygems.org'

gem 'github-pages', group: :jekyll_plugins
gem 'jekyll-feed'

В принципе, чтобы не париться, достаточно знать только про $< и считать, что работаешь с открытым файлом.

Продолжаем решать нашу задачку (гит, файлы, замена). Получив с помощью команды git grep <...> | ruby -e 'puts $_.split(":")[0] while $<.gets' список файлов, мы должны только произвести замену.

Лично я не знаю утилиты в *nix, которая бы это сделала. Но зато я знаю Ruby!

!! | ruby -e 'File.write $_, File.read($_.chop).gsub("OLD", "NEW") while $<.gets'

!! в баше содержит предыдущую команду. Используем ее просто чтобы было компактнее. В самом руби мы просто берем содержимое файла, меняем в нем нужный текст (#gsub) и переписываем этот же файл.

Давайте теперь посмотрим на то, что мы в итоге написали:

git grep <...> | ruby -e 'puts $_.split(":")[0] while $<.gets'
# Здесь выводится список файлов, смотрим, что все ок.

!! | ruby -e 'File.write $_, File.read($_.chop).gsub("OLD", "NEW") while $<.gets'

Здорово? Мне кажется, да. Также для создания подобных однострочников в интерпретатор Ruby добавили парочку полезных опций:

  • -n - оборачивает код в цикл while $<.gets
  • -l - немного шаманит с переводами строк, но самое главное - автоматически делает #chop!

С использованием этих опций улучшаем наши команды:

git grep OLD | ruby -n -e 'puts $_.split(":")[0]'
# Здесь выводится список файлов, смотрим, что все ок.

!! | ruby -l -n -e 'File.write $_, File.read($_).gsub("OLD", "NEW")'

Теперь все еще компактнее. Более подробно эта тема раскрывается в книге Мацумото в “10.3. Practical Extraction and Reporting Shortcuts”. Кстати, язык Perl расшифровывается как: Practical exctraction and reporting language -> Pearl -> Perl