В нашем контексте типы STDIN, STDOUT и STDERR фактически являются экземплярами IO::FileDescriptor.
Crystal предоставляет некоторые полезные типы IO, которые мы уже использовали. Помните, как мы также использовали IO::Memory как средство передачи преобразованных входных данных в jq? Или как мы использовали String.build для создания строки данных после того, как jq преобразовал ее? IO::Memory — это реализация IO, которая хранит записанные данные в памяти приложения, а не во внешнем хранилище, таком как файл. Метод String.build выдает IO, в который можно записать данные, а затем возвращает записанное содержимое в виде строки. Полученный IO можно рассматривать как оптимизированную версию IO::Memory. Пример этого в действии будет выглядеть так:
io = IO::Memory.new
io << "Hello"
io << " " << "World!"
puts io # => Hello World!
string = String.build do |io|
io << "Goodbye"
io << " " << "World"
end
puts string # => Goodbye World!
Стандартная библиотека Crystal также включает в себя несколько примесей, которые можно использовать для улучшения поведения IO. Например, модуль IO::Buffered можно включить в тип IO, чтобы повысить производительность за счет добавления буферизации ввода/вывода к типу IO Другими словами, вы можете сделать так, чтобы данные не записывались немедленно в базовый IO, если это тяжелый процесс. Файл является примером буферизованного IO.
Crystal также предоставляет некоторые дополнительные специализированные типы ввода-вывода, которые можно использовать в качестве строительных блоков для создания других типов IO. Некоторые из них, на которые стоит обратить внимание, включают следующее:
• Delimited — IO, который оборачивает другой IO, считывая только до начала указанный разделитель. Может быть полезно для экспорта только части потока клиенту.
• Hexdump — IO, который печатает шестнадцатеричный дамп всех переданных данных. Может быть полезно для отладки двоичных протоколов, чтобы лучше понять, когда и как данные отправляются/получаются.
• Sized — IO, который оборачивает другой ввод-вывод, устанавливая ограничение на количество байтов, которые можно прочитать.
Полный список см. в документации API: https://crystal-lang.org/api/IO.html.
Теперь, когда мы познакомились с IO, давайте вернемся к обновлению нашего CLI, чтобы лучше использовать ввод-вывод на основе терминала. Планируется обновить src/transform_cli.cr для чтения непосредственно из STDIN и вывода непосредственно в STDOUT. Это также позволит нам устранить необходимость в константе INPUT_DATA. Теперь файл выглядит так:
require "./transform"
STDOUT.puts Transform::Processor.new.process STDIN.gets_to_end
Главное, что изменилось, это то, что мы заменили константу INPUT_DATA на STDIN. get_to_end. При этом все данные из STDIN будут прочитаны в виде строки, передав их в качестве аргумента методу #process. Мы также заменили puts на STDOUT.puts, которые семантически эквивалентны, но это просто проясняет, куда направляются выходные данные.
Остальная логика внутри нашего типа процессора остается прежней, включая String.build, чтобы вернуть вывод jq в виде строки, чтобы мы могли преобразовать его обратно в YAML перед выводом на терминал. Однако в следующем разделе будут представлены некоторые рефакторинги, которые сделают это ненужным.
Мы можем убедиться, что наше изменение работает, запустив echo $'---\n- id: 1\n author:\n name: Jim\n- id: 2\n author:\n name: Bob\n' | crystal src/transform_cli.cr '[.[] | {"id": (.id + 1), "name": .author.name}]', который должен выводиться так же, как и раньше:
---
- id: 2
name: Jim
- id: 3
name: Bob
Хотя сейчас мы читаем входные данные из STDIN, было бы также хорошим улучшением, если бы мы разрешили передачу входного файла для чтения входных данных. Crystal определяет константу ARGF, которая позволяет считывать данные из файла и возвращаться к STDIN, если файлы не предоставлены. ARGF также является вводом-выводом, поэтому мы можем просто заменить STDIN на ARGF в src/transform_cli.cr. Мы можем проверить это изменение, записав выходные данные последнего вызова в файл, скажем, input.yaml. Затем запустите приложение, передав файл в качестве второго аргумента после фильтра. Полная команда будет выглядеть так: crystal src/transform_cli.cr. input.yaml. Однако при запуске вы заметите ошибки: Необработанное исключение: Ошибка чтения файла: Является каталогом (IO::Error). Вы можете задаться вопросом, почему это так, но ответ заключается в том, как работает ARGF.