Многие не знают о тех мощных параметрах командной строки, что понимает интерпретатор 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 — это тот самый инструмент.
Ссылки
- http://arjanvandergaag.nl/blog/using-ruby-command-line-options.html
- https://speakerdeck.com/avdgaag/getting-started-with-ruby](https://speakerdeck.com/avdgaag/getting-started-with-ruby
- http://ruby-doc.org/stdlib-2.1.2/libdoc/optparse/rdoc/OptionParser.html
- http://ruby-doc.org/stdlib-2.0.0/libdoc/find/rdoc/Find.html
Автор: MyDigitalPRO