Как, думаю, многим известно, Ruby создавался под влиянием Perl, поэтому нет ничего удивительного в том, что Ruby может подменить Perl в его нише "практического извлечения данных и составления отчетов". В данном небольшом посте речь пойдет об использовании ruby для мелкой обработки текста в консоли.


TL;DR


Запоминаем две конструкции:


  1. ... | ruby -lne <CODE>
  2. ruby -lne <CODE> file1, file2, ...

В первом случае входными данными будет вывод предыдущей команды, а во второй — конкатенация содержимого файлов.


CODE будет выполняться для каждой строки входных данных, которая будет записываться в переменную $_. Важно: строки будут без завершающего \n (за это отвечает флаг -l).


Примеры


Отраженный cat:


ruby -lne 'puts $_.reverse' file-to-show.txt

Вывести только первые 10 символов имени файлов в текущей директории:


ls | ruby -lne 'puts $_[0..9]'

Немного подробностей


Предопределенные переменные


В Ruby есть множество предопределенных переменных, о существовании многих из которых мы с вами можем даже не знать. Среди них есть и перекочевавшие из Perl, например, $_ и $< (на самом деле, в Perl нет такой переменной, но есть конструкция оператора ввода <>, читающая из файлов/stdin и устанавливающая $_).


С их списком можно ознакомиться здесь.


В рамках темы поста советую обратить внимание на вышеупомянутые $_ и $<, а помимо них на: $~, $1, $stdin, ARGV, $stdout, STDIN и STDOUT.


Страшный секрет gets


Возможно, многие использовали gets для того, чтобы запросить ввод с клавиатуры. Однако далеко не все знают, что он проверяет содержимое ARGV — аргументов командой строки.


Когда ARGV непустой, наш gets считает, что там указан список файлов, берет первый из них и пытается прочитать строку оттуда.


Возьмем скрипт:


# myscript.rb
puts gets

И запустим его с помощью ruby myscript.rb. Как и ожидалось, программа ждет нашего ввода.


А теперь запустим его вот так:


ruby myscript.rb no-such-file

Произойдет ошибка: файла "no-such-file" не существует. А теперь изменим скрипт:


# myscript.rb
ARGV.pop
puts gets

Теперь интерпретатор опять ожидает нашего ввода, т.к. с помощью pop мы выкинули наш "no-such-file".


Опции интерпретатора


Вот так выглядит мой шаблон однострочника:


ruby -lne <CODE> [file1, file2, ...]

(зачастую без -l)


Описание используемых и некоторых потенциально полезных опций:


  • -e <CODE> — вынуждает интерпретатор не пытаться искать скрипт среди аргументов и выполнить CODE.
  • -n — оборачивает скрипт (или код, переданный с помощью -e) в цикл while gets. Метод gets неявно устанавливает возвращаемое значение переменной $_ (привет, Perl!), которую мы и должны использовать. Важно: строка будет заканчиваться \n.
  • -l — для простоты можно считать, что она просто удаляет символ новой строки \n из $_.
  • -p — работает точно так же, как и -n, но после каждой итерации выводит содержимое $_. Т.е. подразумевается, что вы будете его менять. Например: ls | ruby -pe '$_.upcase!'
  • -C <DIR> — меняет рабочую директорию.
  • -a(от auto-split) — используется совместно с -n или -p и устанавливает переменную $F, равной $_.split.
  • -F — устанавливает стандартный разделитель для String#split ($;). Теоретически, может быть полезно совместно с -a. Например, так можно обрабатывать простенькие .csv

Заключение


Впервые использовать ruby -e меня натолкнула команда cut. Мне ее приходилось пару раз использовать, и каждый раз мне нужно было открывать мануал, который моя тугая голова не хотела быстро понимать.


В конечном итоге я подумал: "Да, я не умею пользоваться cut/awk/sed. Но зато я неплохо знаю Ruby, почему я не могу для всяких мелочей использовать его?".


Собственно, в конечном итоге вместо запоминания множества команд мне достачно было запомнить только конструкцию, полученную опытным путем:


ruby -lne CODE


И все. Все мои проблемы с построчной обработкой текста решены.


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


P.S. Напоследок вот пример использования:


Скачал сезон сериала, а там имена файлов некрасивые, вроде "s01e01 — super release by super-mega-macho.mkv", хочу их переименовать, чтобы в медиатеке все было красиво. Пожалуйста:


ls | ruby -lne 'File.rename($_, "#{$_[0..5]}.mkv")'

Комментарии (10)


  1. scizor
    07.10.2017 09:01

    Просто подумал, что, возможно, не всегда удобно использовать _$ и -n. С тем же xargs например ls | ruby -lne 'puts $_[0..9]' можно переписать как ls | xargs ruby -e 'ARGV.each { |file| puts file[0..9] }'. Хотя конкретно для этого кейса ваш код выглядит поприятнее


    1. Nondv Автор
      07.10.2017 09:04

      Ну, в посте разбирается построчная обработка. Я сомневаюсь, что использование ARGV когда-нибудь будет удобнее в этом плане.
      Хотя все возможно:)


      1. Nondv Автор
        07.10.2017 09:45

        Преимущество ARGV, как мне кажется, только в том, что в скрипте имеешь доступ ко всем аргументам сразу (в итерации можно, например, использовать предыдущий)


        1. scizor
          07.10.2017 09:49

          ага, именно) разве что так сразу пример не подобрать, когда может понадобиться.


          1. Nondv Автор
            07.10.2017 10:03

            В любом случае я бы использовал $< вместо ARGV:


            ls | ruby -e '$<.each_with_index { |s, i| puts "#{i+1}: #{s}" }'
            
            # vs
            
            ls | xargs ruby -e 'ARGV.each_with_index { |s, i| puts "#{i+1}: #{s}" }'


            1. scizor
              07.10.2017 10:43

              $< выглядит как прекрасная альтернатива, да, xargs + ARGV будет полезен когда нужны все аргументы сразу, типа для случая ARGV.join или ARGV[0] + ARGV[-1], скорее не для построчной обработки, что вы в общем и сказали, конечно зависит от количества входных данных и всегда можно заюзать ARGF если захотеть, я бы сказал, что по ситуации надо выбирать


          1. michaelkl
            08.10.2017 18:30
            +1

            Можно распараллелить обработку:

            ls | xargs ruby -e 'require "parallel"; Parallel.each(ARGV) { |s| puts s.upcase }'


            1. Nondv Автор
              09.10.2017 19:40
              +2

              Спасибо за идею!


              можно написать -r parallel, чтобы не перенасыщать строку кода:


              ls | xargs ruby -r parallel -e 'Parallel.each(ARGV) { |s| puts s.upcase }'


  1. g4xd
    09.10.2017 01:00
    -1

    или можно просто научиться awk и sed, и особо много времени на это не нужно


    1. Nondv Автор
      09.10.2017 19:39
      +1

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


      В случае с ruby/perl/etc Вы, запомнив по сути всего парочку фактов, получаете швейцарский нож, с помощью которого можете сделать, что угодно.


      Это хорошо, если человек знает awk+sed. Он решит задачу, скорее всего, красивее, чем с помощью руби/перл. Но это при условии, что он знает. Мне вот это пригождается не так часто, чтобы я постигал прелести awk (которым я раньше временами пользовался) и пытался держать их в голове.