ARGF сначала проверит, пуст ли ARGV. Если да, то будет выполнено чтение из STDIN. Если ARGV не пуст, предполагается, что каждое значение в ARGV представляет файл для чтения. В нашем случае ARGV не пуст, поскольку содержит [.", "input.yaml"], поэтому он пытается прочитать первый файл, который в данном случае представляет собой точку, обозначающую текущую папку. Поскольку папку нельзя прочитать как файл, возникает исключение, которое мы видели. Чтобы обойти эту проблему, нам нужно убедиться, что ARGV содержит только тот файл, который мы хотим прочитать, прежде чем вызывать ARGF#gets_to_end. Самый простой способ справиться с этой проблемой — вызвать метод #shift для ARGV, который работает, поскольку это массив. Этот метод удаляет первый элемент массива и возвращает его, в результате чего в ARGV остается только файл.
Однако есть еще одна проблема, которую нам также необходимо решить. Поскольку мы используем ARGV напрямую для предоставления входных аргументов jq, нам нужно будет провести некоторый рефакторинг, чтобы иметь возможность получить доступ к фильтру перед вызовом #gets_to_end. Мы можем добиться этого, переместив часть логики из src/transform_cli.cr в src/processor.cr! Обновите src/processor.cr, чтобы он выглядел так:
class Transform::Processor
def process : Nil
filter = ARGV.shift
input = ARGF.gets_to_end
output_data = String.build do |str|
Process.run(
"jq",
[filter],
input: IO::Memory.new(
Transform::YAML.deserialize input
),
output: str
)
end
STDOUT.puts Transform::YAML.serialize output_data
end
end
Ключевым дополнением здесь является введение filter = ARGV.shift, который гарантирует, что остальная часть ARGV будет содержать только тот файл, который мы хотим использовать в качестве входных данных. Затем мы используем нашу переменную как единственный элемент в массиве, представляющий аргументы, которые мы передаем в jq, заменяя жестко закодированную ссылку ARGV.
Также обратите внимание, что мы удалили входной аргумент из метода #process. Причина этого в том, что все входные данные теперь получаются изнутри самого метода, и поэтому нет смысла принимать внешние входные данные. Еще одним примечательным изменением было изменение типа возвращаемого значения метода на Nil, поскольку мы выводим его непосредственно в STDOUT. Это немного снижает гибкость метода, но об этом также будет сказано в следующем разделе.
Есть еще одна вещь, которую нам нужно обработать, прежде чем мы сможем объявить рефакторинг завершенным: что произойдет, если в jq будет передан недопустимый фильтр (или данные)? В настоящее время это вызовет не очень дружелюбное исключение. Что нам действительно нужно сделать, так это проверить, успешно ли выполнен jq, и если нет, записать сообщение об ошибке в STDERR и выйти из приложения, внеся следующие изменения в src/processor.cr:
class Transform::Processor
def process : Nil
filter = ARGV.shift
input = ARGF.gets_to_end
output_data = String.build do |str|
run = Process.run(
"jq",
[filter],
input: IO::Memory.new(
Transform::YAML.deserialize input
),
output: str,
error: STDERR
)
exit 1 unless run.success?
end
STDOUT.puts Transform::YAML.serialize output_data
end
end
Два основных улучшения заключаются в том, что любой вывод ошибок, возникающий во время работы jq, должен выводиться в STDERR и что программа должна завершиться раньше, если jq не выполнился успешно.
Эти два улучшения позволяют пользователю понять, что пошло не так, и предотвращают дальнейшее выполнение приложения, которое в противном случае привело бы к попытке преобразовать сообщение об ошибке в YAML.
Поддержка других IO
В последнем разделе мы уже внесли немало улучшений: нам больше не нужно жестко кодировать входные данные, и мы лучше справляемся с ошибками, исходящими от jq. Но помните, как мы также хотели поддержать использование нашего приложения в контексте библиотеки? Как кто-то будет обрабатывать тело ответа HTTP и выводить его в файл, если наш процессор тесно связан с концепцией терминала?
В этом разделе мы собираемся устранить этот недостаток, снова проведя рефакторинг, чтобы разрешить любой тип IO, а не только типы IO на основе терминала.