А вы знаете Ruby?

в 10:35, , рубрики: ruby, консоль, Оболочки, Скриптинг

Многие не знают о тех мощных параметрах командной строки, что понимает интерпретатор Ruby. Они показывают как сильное влияние оказал на язык Perl и что Ruby отличный интсрумент общего назначения для командной строки.

Пусть есть задача обновить некоторые текстовые файлы, которые используются у нас в проекте. Данные выглядят как CSV, но так же содержат комментарии. Нам нужно отфильтровать некоторые записи по стране. Вот пример файла:

% wc -l
1005 data.csv
% head data.csv
# Copyright 2014 Acme corp. All rights reserved.
#
# Please do not reproduce this file without including this notice.
# ===============================================================
Name,Partner,Email,Title,Price,Country
Nikolas Hamill,Emely Langosh Sr.,nash@moen.info,Awesome Wooden Computer,42261,Puerto Rico
Friedrich Zboncak MD,Ms. Trycia Sporer,nils@treutelrodriguez.name,Sleek Wooden Hat,35701,Suriname
Marcus Nicolas,Margot Hoppe,maeve@hilll.info,Rustic Steel Shoes,40258,Argentina
Toni Ernser I,Guillermo Kihn II,clara.marvin@west.net,Sleek Cotton Pants,68332,Turks and Caicos Islands
Mayra Kerluke DDS,Marvin Lynch,sydni.schuppe@schuster.com,Incredible Steel Gloves,47017,New Zealand

Предположим мы не можем использовать модуль CSV из стандартной библиотеки Ruby и сделаем все ручками, например, вот так:

!/usr/bin/env ruby -w
# Скрипт обрабатывает файлы, выглядящие как CSV
# Удаляет комментарии и все записи не о Суринам

# Определяем базовые переменные
input_record_separator  = "n"
field_separator         = ','
output_record_separator = "n"
output_field_separator  = ';'
filename = ARGV[0]

File.open(filename, 'r+') do |f|

  # Считываем весь файл в массив
  input = f.readlines(input_record_separator)
  output = ''

  input.each_with_index do |last_read_line, i|

    # Удаляем символ перевода строки
    last_read_line.chomp!(input_record_separator)

    # Разбиваем строку на поля
    fields = last_read_line.split(field_separator)

    # Обрабатываем не комментарии о Суринам
    if fields[5] == 'Suriname' && !(last_read_line =~ /^# /)

      # Объединем строки и поля нашими разделителями
      fields.unshift i
      output << fields.join(output_field_separator)
      output << output_record_separator
    end
  end

  # Возвращаемся к началу файла и перезаписываем его содержимым output
  f.rewind
  f.write output
  f.flush
  f.truncate(f.pos)
end

Это определенно не лучший код в моей жизни, но он делает свою работу.

Используем стандартные переменные

Для оптимизации скрипта мы можем перейти на стандартные глобальные переменные. Чтобы прояснить их назначение, мы подключим библиотеку english:

#!/usr/bin/env ruby -w
require 'english'
$INPUT_RECORD_SEPARATOR  = "n"
$FIELD_SEPARATOR         = ','
$OUTPUT_RECORD_SEPARATOR = "n"
$OUTPUT_FIELD_SEPARATOR  = ';'
filename = ARGV[0]

File.open(filename, 'r+') do |f|
  input = f.readlines(input_record_separator)
  output = ''
  input.each_with_index do |last_read_line, i|
    $LAST_READ_LINE = last_read_line
    $INPUT_LINE_NUMBER = i
    $LAST_READ_LINE.chomp!($INPUT_RECORD_SEPARATOR)
    $F = $LAST_READ_LINE.split($FIELD_SEPARATOR)
    if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
      $F.unshift $INPUT_LINE_NUMBER
      output << $F.join($OUTPUT_FIELD_SEPARATOR)
      output << $OUTPUT_RECORD_SEPARATOR
    end
  end
  f.rewind
  f.write output
  f.flush
  f.truncate(f.pos)
end

Используем значения по-умолчанию

Поскольку эти переменные используются самим Ruby, они в большинстве случаев уже имеют осмысленные значения. Поэтому можно привести код к такому виду:

#!/usr/bin/env ruby -w
require 'english'
$FIELD_SEPARATOR         = ','
$OUTPUT_RECORD_SEPARATOR = "n"
$OUTPUT_FIELD_SEPARATOR  = ';'
filename = ARGV[0]

File.open(filename, 'r+') do |f|
  input = f.readlines
  output = ''
  input.each_with_index do |last_read_line, i|
    $LAST_READ_LINE = last_read_line
    $INPUT_LINE_NUMBER = i
    $LAST_READ_LINE.chomp!
    $F = $LAST_READ_LINE.split
    if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
      $F.unshift $INPUT_LINE_NUMBER
      output << $F.join
      output << $OUTPUT_RECORD_SEPARATOR
    end
  end
  f.rewind
  f.write output
  f.flush
  f.truncate(f.pos)
end

Мы смогли избавиться от некоторых аргументов и объявления $INPUT_RECORD_SEPARATOR. Также мы можем использовать IO#print, который объединяет несколько аргументов с помощью $OUTPUT_FIELD_SEPARATOR. Он также использует $OUTPUT_RECORD_SEPARATOR, если переменная инициализирована.

#!/usr/bin/env ruby -w
require 'english'
$FIELD_SEPARATOR         = ','
$OUTPUT_RECORD_SEPARATOR = "n"
$OUTPUT_FIELD_SEPARATOR  = ';'
filename = ARGV[0]

File.open(filename, 'r+') do |f|
  input = f.readlines
  f.rewind
  input.each_with_index do |last_read_line, i|
    $LAST_READ_LINE = last_read_line
    $INPUT_LINE_NUMBER = i
    $LAST_READ_LINE.chomp!
    $F = $LAST_READ_LINE.split
    if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
      $F.unshift $INPUT_LINE_NUMBER
      f.print *$F
    end
  end
  f.flush
  f.truncate(f.pos)
end

Так мы избавились от переменной output. Теперь вместо того, чтобы читать весь файл в массив, мы можем обрабатывать его построчно:

#!/usr/bin/env ruby -w
require 'english'
$FIELD_SEPARATOR         = ','
$OUTPUT_RECORD_SEPARATOR = "n"
$OUTPUT_FIELD_SEPARATOR  = ';'
filename = ARGV[0]

File.open(filename, 'r+') do |f|
  while f.gets
    $LAST_READ_LINE.chomp!
    $F = $LAST_READ_LINE.split
    if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
      $F.unshift $INPUT_LINE_NUMBER
      f.print *$F
    end
  end
end

Сейчас мы используем IO#gets чтобы читать файл и автоматически присваивать значения переменным $LAST_READ_LINE и $INPUT_LINE_NUMBER. Так же мы потеряли возможность перемотать и перезаписать весь файл.

Чтение и редактирование файла in-place

Используя флаги -n -i мы можем сказать Ruby читать файл используя IO#gets и с помощью IO#print писать обратно в файл. Для -i можно передать расширение, с которым будет создан backup-файл.

#!/usr/bin/env ruby -w -n -i
require 'english'
BEGIN {
  $FIELD_SEPARATOR         = ','
  $OUTPUT_RECORD_SEPARATOR = "n"
  $OUTPUT_FIELD_SEPARATOR  = ';'
}

$LAST_READ_LINE.chomp!
$F = $LAST_READ_LINE.split
if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
  $F.unshift $INPUT_LINE_NUMBER
  print *$F
end

-n оборачивает скрипт в цикл while gets… end. Блок BEGIN {… } вызывается на старте программы в не зависимости от расположения в коде. Вызов IO#print по умолчанию направлен в единственный открытый файл, а -i управляет записью обратно в оригинальный файл.

На заметку: -p делает почти тоже самое, что -n, но добавляет pring $_ в конец цикла. Он читает а затем пишет каждую строку в файле, позволяя вам пропускать или модифицировать строки перед записью.

Установка переменных с помощью опций командной строки

#!/usr/bin/env ruby -w -n -i -F, -l
require 'english'
BEGIN {
  $OUTPUT_FIELD_SEPARATOR  = ';'
}

$F = $LAST_READ_LINE.split
if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
  $F.unshift $INPUT_LINE_NUMBER
  print *$F
end

-F устанавливает значение для $INPUT_FIELD_SEPARATOR. -l говорит Ruby присвоить значение $INPUT_RECORD_SEPARATOR переменной $OUTPUT_FIELD_SEPARATOR и удалить $INPUT_FIELD_SEPARATOR из $LAST_READ_LINE используя String#chomp!.. Что означает: разделитель входящих записей будет удален из прочитанных строк (что нам и нужно) и добавлен к строкам на запись (опять же как раз то, чего мы хотим).

Теперь используем авторазделение -a:

#!/usr/bin/env ruby -w -n -i -F, -l -a
require 'english'
BEGIN {
  $OUTPUT_FIELD_SEPARATOR  = ';'
}

if $F[5] == 'Suriname' && !($LAST_READ_LINE =~ /^# /)
  $F.unshift $INPUT_LINE_NUMBER
  print *$F
end

С -a Ruby автоматически разобьет текущую строку в массив $F на каждой итерации.

Сравнение с текущей строкой

В Ruby есть сокращение, которое мы можем использовать с опцией -n или -p. Условия, регулярные выражение по-умолчанию применяются к $LAST_READ_LINE, а численные диапазоны к $INPUT_LINE_NUMBER. Благодаря этому знанию, можно упростить условный оператор:

#!/usr/bin/env ruby -w -n -i -F, -l -a
require 'english'
BEGIN {
  $OUTPUT_FIELD_SEPARATOR  = ';'
}

unless $F[5] != 'Suriname' || /^# /
  $F.unshift $INPUT_LINE_NUMBER
  print *$F
end

Сокращаем код

Уберем библиотеку english, перейдем на сокращенные названия переменных и запишем условный оператор в одну строку.

#!/usr/bin/env ruby -w -n -i -F, -l -a
BEGIN { $, = ';' }
print $., *$F unless $F[5] != 'Suriname' || /^# /

Заключение

Да, мы построили недоимплементацию Awk на Ruby. Если вы знаете Awk, то можете использовать его. Если вы знаете Ruby лучше, то благодаря этим ключам он может стать для вас хорошим инструментом командной строки.

Предыдущий скрипт, может быть написан прямо в консоли примерно так:

ruby -wlani -F, -e "BEGIN { $, = ';' }" -e "print $., *$F unless $F[5] != 'Suriname' || /^# /"

Можно парсить YAML:

ruby -r yaml -e 'puts YAML.load(ARGF)["database"]' config/database.yml

Иногда, методы Awk или Sed являются наиболее уместным инструментом для вашей задачи. Но иногда требуется что-то большее или вам просто нет смысла смотреть как сделать какие-то общие операции, которые вы знаете как реализовать на Ruby, на каком-то другом языке. Всегда стоит использовать подходящий инструмент для задачи, и представленная гибкость Ruby может удивить как часто Ruby — это тот самый инструмент.

Ссылки

Автор: MyDigitalPRO

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js