Это частично руководство, частично статья о том, что вам следует учитывать перед созданием нового CLI-приложения в Rust.
Прежде чем переходить к `cargo new` и приступать к ее реализации, ознакомьтесь с ней и узнайте, как повысить удобство разработки, эргономичность интерфейса и ремонтопригодность проекта.
Структура
Когда у вас есть приложение CLI, у вас есть куча флагов и команд, и для них подходящий логический модуль.
Например, для подкоманды `git clone` у вас есть некоторая функциональность клонирования, но для подкоманды `git commit` у вас может быть совершенно другая функциональность, которая может находиться в совершенно другом модуле (и должна быть). Таким образом, это может быть другой модуль, а также другой файл.
Или у вас может быть простая плоская структура CLI-приложения, которое делает только одну вещь, но использует различные флаги в качестве настроек для этой единственной вещи:
Итак, когда мы говорим о макете команды, это должно означать разделение на файлы, и затем мы также хотим поговорить о файловой структуре и макете проекта.
Просмотрев большое количество популярных CLI-приложений в Rust, я обнаружил, что обычно существует три типа структур приложений:
- Ad-hoc, см. xh в качестве примера, с любой структурой папок
- Плоский, со структурой папок, такой как:
- Вложенные команды, где структура вложена, вот в качестве примера, со структурой папок, такой как
Вы можете использовать плоскую или вложенную структуру, используя стартовый проект: `rust-starter`, и использовать то, что вам нужно, а то, что вам не нужно, удалить.
Пока вы этим занимаетесь, всегда полезно разделить ядро вашего приложения и его интерфейс. Я нахожу, что хорошее эмпирическое правило заключается в том, чтобы думать о создании:
- Библиотеки
- Интерфейса командной строки, использующий эту библиотеку
- И кое-что, что помогает обобщить и укрепить API этой библиотеки: какой-то другой графический интерфейс (который никогда не будет существовать), который мог бы использовать эту библиотеку
Чаще всего, особенно в Rust, я нахожу, что это разделение было чрезвычайно полезным, и мне представляются варианты использования библиотеки как самостоятельной.
Обычно оно делится на три части:
- Библиотека
- Основной рабочий процесс/запуск, (например `workflow.rs` ), который управляет приложением CLI
- Части CLI (запрос, обработка выхода, флаги синтаксического анализа и команды), которые отображаются в рабочий процесс
Флаги, параметры, аргументы и команды
Даже если мы исключим приложения CLI, которые очень графичны, такие как Vim (используют TUI и т.д.), нам все равно придется решать проблемы пользовательского интерфейса/UX в нашем CLI в форме флагов, команд и аргументов, которые получает программа.
Существует несколько различных способов указания и анализа параметров командной строки, каждый со своим собственным набором соглашений и рекомендаций.
Согласно стандарту POSIX, параметры задаются с помощью одного тире, за которым следует одна буква, и могут быть объединены в один аргумент (например, -abc). Длинные параметры, которые обычно более элегантны и их легче читать, указываются с помощью двух тире, за которыми следует слово (например, --option). Параметры также могут принимать значения, которые задаются с помощью знака равенства (например, -o=значение или --option=значение).
Позиционные аргументы - это аргументы, которые приложение командной строки ожидает указать в определенном порядке. Они не имеют перед собой тире и часто используются для указания требуемых данных, необходимых приложению для правильной работы.
Стандарт POSIX также определяет ряд специальных опций, таких как -h или --help для отображения справочного сообщения и -v или --verbose для вывода подробностей. Эти параметры широко известны и используются многими приложениями командной строки, что облегчает пользователям поиск и использование различных функций.
В целом, стандарт POSIX предоставляет набор соглашений для указания и синтаксического анализа параметров командной строки, которые широко признаны и которым следуют многие приложения командной строки, облегчая пользователям понимание и использование различных инструментов командной строки.
Или, другими словами, при разработке интерфейса для приложения командной строки нам нужно подумать о:
- Подкоманды: Некоторые приложения командной строки позволяют пользователям указывать подкоманды, которые по сути являются дополнительными вложенными приложениями, которые могут быть запущены внутри основного приложения. Например, команда git позволяет пользователям указывать такие подкоманды, как `commit`, `push` и `pull`. Подкоманды часто используются для группировки связанных функций в рамках одного приложения и упрощения поиска пользователями различных функций и их использования.
- Позиционные аргументы: Позиционные аргументы - это аргументы, которые приложение командной строки ожидает указать в определенном порядке. Например, команда cp ожидает два позиционных аргумента: исходный файл и файл назначения. Позиционные аргументы часто используются для указания требуемых данных, которые необходимы приложению для правильного функционирования.
- Флаги/параметры: Флаги - это параметры командной строки, которые не ожидают указания значения. Они часто используются для переключения определенного поведения или настройки в приложении. Флаги обычно задаются с помощью одинарного тире, за которым следует одна буква (например, -v для подробной информации), или двойного тире, за которым следует слово (например, --verbose для подробной информации). Флаги также могут принимать необязательные значения, которые задаются с помощью знака равенства (например, --output=file.txt ).
Чтобы сделать отличную библиотеку, на которую можно положиться, которая поможет вам перейти от простого приложения к сложному без необходимости перехода в другую библиотеку, вы можете использовать `clap crate`. `Clap` поможет вам пройти долгий путь, рассмотрите возможность использования альтернативы только в том случае, если у вас есть особые требования, такие как более быстрое время компиляции, меньший размер двоичного файла или что-то подобное.
Минималистичная альтернатива `clap` - `argh`. Зачем выбирать что-то другое?
- размер `clap` может быть слишком большим для вашего двоичного файла, и вас действительно волнует размер двоичного файла (здесь мы говорим о таких числах, как 300 кб против 50 кб).
- компиляция `clap` может занять больше времени (но для большинства людей она достаточно быстрая)
- Возможно, у вас очень простой интерфейс CLI и вы цените код, который занимает половину экрана для всего, что вам нужно сделать.
Вот пример использования argh:
А потом просто:
Конфигурация
В большинстве операционных систем существуют стандартные места для хранения файлов конфигурации и других пользовательских данных. Эти местоположения часто называются “домашними папками” или “каталогами профилей” и используются для хранения файлов конфигурации, данных приложений и других пользовательских данных.
Unix-подобные системы (Linux / macOS)
Домашняя папка обычно находится по адресу `/home/username` или `/Users/username` и используется для хранения файлов конфигурации и других данных, специфичных для пользователя. Домашнюю папку часто называют каталогом `$HOME`, и к ней можно получить доступ с помощью символа `~` (например, `~/.bashrc`).
В домашнем каталоге часто находится папка `.config` (также известная как "каталог конфигурации"), которая используется для хранения файлов конфигурации и других данных, специфичных для пользователя. Папка `.config` - это каталог с “точкой”, поэтому, если вы ее не видите, используйте терминал. Приложение может хранить свои конфигурационные файлы в подкаталоге папки `.config`, например `~/.config/myapp`.
В Windows домашняя папка обычно находится по адресу `C:\Users\username` , и используется для хранения файлов конфигурации и других данных, специфичных для пользователя. Домашнюю папку часто называют каталогом "профиль пользователя", и к ней можно получить доступ с помощью переменной окружения `%USERPROFILE%` (например,` %USERPROFILE%\AppData\Roaming`).
Конфигурационные файлы
С serde легко читать содержимое конфигурации, потому что это трехэтапный процесс:
- “Сформируйте” свою конфигурацию в виде структуры
- Выберите формат и включите необходимые функции `serde` (например, `yaml`)
- Десериализуйте свою конфигурацию (`serde::from_str`)
В большинстве случаев этого более чем достаточно, и получается простой, поддерживаемый код, который также можно доработать, изменив форму вашей структуры.
Некоторые варианты использования требуют “обновления” загрузки конфигурации двумя возможными способами:
- Локальная и глобальная конфигурации и их взаимосвязи. То есть, прочитайте файл конфигурации текущей папки и “подымитесь” по иерархии папок, заполняя остальную недостающую конфигурацию с каждым новым найденным файлом конфигурации, пока не доберетесь до файла глобальной конфигурации пользователя, расположенного где-то вроде `~/.your-app/config.yaml`.
- Входные данные многоуровневой конфигурации. То есть чтение из локального `config.yaml`, но, если определенное значение было предоставлено с помощью флага среды или флага CLI, переопределите то, что было найдено в `config.yaml`, этим значением. Для этого требуется библиотека, которая может обеспечить выравнивание ключей конфигурации для различных форматов: `YAML`, флагов CLI, переменных окружения и так далее.
Хотя я настоятельно призываю вас сохранять простоту и развиваться (просто используйте загрузку `serde`), я обнаружил, что обе эти библиотеки действительно надежны при чтении многоуровневой конфигурации и делают практически одно и то же.:
Цвета, стиль и терминал
В современном мире стало совершенно нормально выражать себя с помощью стиля в терминале. Это означает иногда цвета RGB, эмодзи, анимацию и многое другое. Использование цветов, unicode, может привести к потере совместимости с “традиционным” терминалом Unix, большинство библиотек могут понижать качество работы по мере необходимости.
Цвета
Если вы не используете какую-либо другую библиотеку пользовательского интерфейса терминала, `owo-colors` великолепен, минимален, удобен в использовании ресурсов и интересен:
Если вы используете что-то вроде `dialoguer` для подсказок, стоит изучить, что он использует для цветов. В этом случае он использует консоль для манипулирования терминалом. С помощью консоли вы можете оформить его таким образом:
Немного отличается, но не слишком.
Эмодзи
Размышляя об эмодзи: это форма самовыражения. Итак, 🙂, :-) и `[smiling]` - это одно и то же выражение, но разные средства. Вам нужны эмодзи с хорошей поддержкой юникода, текстовый смайлик на текстовых терминалах без юникода и многословная улыбка, когда вам нужен текст с возможностью поиска или для более доступного и читабельного вывода для слабовидящих.
Еще один совет, который следует помнить, заключается в том, что эмодзи могут выглядеть по-разному в разных терминалах. В Windows у вас есть старый терминал cmd.exe и Powershell, и они радикально отличаются тем, как они отображают эмодзи в терминалах Linux и macOS (в то время как рендеринг эмодзи в Linux и macOS довольно близок).
Если уж на то пошло, лучше всего абстрагировать ваши буквальные эмодзи от переменных. Это может быть просто набор литералов с вашей собственной логикой переключения или что-то более причудливое с реализацией `fmt::Display`.
Возможно, вы захотите переключиться на основе матрицы требований:
- Операционная система
- Поддержка функций терминала (unicode, istty)
- Запрошенное пользователем значение (они специально не просили никаких смайликов?)
В консоли есть отличная реализация этой идеи (хотя и не такая обширная)
Таблицы
Одной из наиболее гибких библиотек для печати и форматирования таблиц является `tabled`. Что делает библиотеку табличной печати гибкой?
- Поддержка “данных свободной формы” — просто набор имен строк и столбцов
- Поддержка типизированных записей через `serde`, поэтому вы просто предоставляете ему набор типизированных элементов в `Vec`
- Форматирование и придание формы: выравнивание, интервалы, охват и многое другое
- Поддержка цветов — это не так просто, поскольку при расчете макета таблицы вам нужно учитывать коды ANSI, которые делают строку побайтно длиннее и ее трудно предсказать
- И многое другое
`tabled` делает все это, и это здорово. Если вы ищете результаты печати таблиц или просто результаты компоновки таблиц (например, результаты форматирования страницы), не ищите ничего другого, это все.
Prompts
`dialoguer` - наиболее широко используемая библиотека подсказок на данный момент, и она надежна как скала. В нем есть почти все различные подсказки, которые можно было бы ожидать от универсального приложения CLI, такие как `checkbox`, `option selects` и `fuzzy selects`.
У неё есть одна серьезная проблема — отсутствие тестируемости. То есть, если вы хотите протестировать свой код и он зависит от неё, у вас жесткая зависимость от терминала (ваши тесты будут зависать).
Чего вы могли бы ожидать, так это наличия какого-то средства абстракции ввода-вывода, которое вы сможете внедрять в тесты, программно передавать ему нажатия клавиш и проверять, что они были прочитаны и что были предприняты соответствующие действия.
Другая библиотека - `inquire` , но она тоже страдает от отсутствия тестирования, и вы можете увидеть, насколько сложной может быть такая вещь при проблеме, которую я отслеживаю.
Хорошей новостью является то, что у вас есть довольно хорошая работающая с тестированием с гораздо менее популярной библиотекой `requestty`, хотя, в любом случае, при внедрении этого уровня абстракции ввода-вывода вы также должны думать о изменяемости и владении:
Зрелище не из приятных, но оно обеспечивает хорошо протестированный поток взаимодействия с CLI.
Какие еще варианты у вас есть для проведения тестирования?
- Доверяйте стабильному состоянию `dialoguer` и просто не тестируйте части взаимодействия вашего приложения
- Протестируйте взаимодействие с помощью тестирования "черного ящика" (подробнее о тестировании позже). При таком подходе вы можете довольно далеко продвинуться с точки зрения рентабельности инвестиций в тестирование
- Создайте свою собственную тестовую установку с переключаемым уровнем взаимодействия с пользовательским интерфейсом, где вы полностью заменяете его во время тестирования чем-то, что воспроизводит действие (опять же, ваш собственный пользовательский код). Это означает, что реальный код, работающий с подсказками и выборками, никогда не будет протестирован, каким бы маленьким он ни был
Статус и прогресс
`indicatif` - это библиотека золотого стандарта Rust для индикаторов состояния и выполнения. Есть только одна отличная библиотека, и это хорошо, потому что мы не застряли в парадоксе вариантов, так что используйте эту!
Работоспособность
В Rust в основном существует два варианта ведения лога для обеспечения работоспособности, и вы можете использовать оба или один из них:
- Логирование — что стало стандартом: https://lib.rs/crates/log где люди в основном комбинируют его с `env_logger`, который прост и действительно удобен в использовании.
- Трассировка — для этого вам нужно использовать один крейт для трассировки и экосистему, которая есть в Rust (к счастью, есть только одна!). Вы можете начать с tracing-tree, но позже вы также можете решить подключить телеметрию и сторонние SDK, а также печатать графики пламени, как и следовало ожидать от инфраструктуры трассировки
Трассировка позволяет вам очень легко настраивать ваш код, украшая функции:
И у вас может быть опция автоматического захвата аргументов, возвращаемых значений и ошибок из функции.
Обработка ошибок
Это большая проблема в Rust. Просто потому, что ошибки прошли большой этап эволюции. Было несколько библиотек, которые появились, а затем вымерли, а затем еще несколько, которые появились и вымерли.
В целом, это был фантастический процесс. В промежутках между каждым циклом экосистема Rust получала реальные уроки и вносились улучшения, и сегодня у нас есть несколько действительно замечательных библиотек.
Недостатком является то, что в зависимости от кода, который вы будете читать, примеры здесь и там и проекты с открытым исходным кодом — вам нужно будет запомнить, какие библиотеки он использует и к какой эпохе библиотек ошибок он принадлежит.
Итак, на момент написания этой статьи, это библиотеки, которые, как я обнаружил, идеально подходят для CLI-приложения. Здесь я также разделяю свое мышление на “ошибки приложений” и “ошибки библиотеки”, где для ошибок библиотеки вы хотите использовать типы ошибок, которые полагаются на стандартную библиотеку, а не заставлять ваших пользователей использовать специализированную библиотеку ошибок.
- Ошибки приложения: `eyre`, который является близким родственником `anyhow`, но имеет действительно отличную историю сообщений об ошибках с такими библиотеками, как `color-eyre`
- Ошибки библиотеки: Раньше я использовал `thiserror`, но затем перешел к `snafu`, который я использую для всего. `snafu` дает вам все преимущества `this-error`, но с эргономикой `anyhow` или `eyre`
И затем, я использую библиотеки, которые улучшают их отчеты об ошибках. В основном я использую `fs_err` вместо `std::fs`, который имеет тот же API, но более сложные и понятные для человека ошибки, например:
Вместо
Тестирование
Я нахожу, что балансировка типов тестов и стратегий в Rust может быть очень деликатной, но полезной задачей. То есть ржавчина безопасна. Это не означает, что ваш код хорошо работает с самого начала, но это означает, что помимо того, что это статически типизированный язык с типами, которые защищают от многих ошибок программирования, он также безопасен в том смысле, что устраняет широкий спектр ошибок программистов, связанных с совместным использованием данных и владением ими.
Я бы осторожно сказал, что мой код Rust содержит меньше тестов по сравнению с моим кодом Ruby или JavaScript и является более надежным.
Я нахожу, что это свойство Rust также в значительной степени возвращает к тестированию чёрного ящика. Потому что после того, как вы протестировали некоторые внутренние компоненты, объединение модулей и их интеграция довольно безопасны благодаря компилятору.
Итак, в целом моя стратегия тестирования приложений Rust CLI такова:
- Модульные тесты — тестирование логики в функциях и модулях
- Интеграционные тесты — по мере необходимости, между модулями и тестирование сложных рабочих процессов взаимодействия
- Тесты черного ящика — использование таких инструментов, как `try_cmd`, для запуска сеанса CLI, предоставления входных данных, получения выходных данных и моментального снимка результирующего состояния для утверждения и сохранения.
Я использую тестирование моментальных снимков там, где могу, потому что в тестах нет смысла кодировать слева направо:
- `insta` - https://docs.rs/insta/latest/insta, заботится о рабочем процессе разработки, моментальных снимках, просмотре и дополнительных функциях, таких как редактирование и различные форматы сериализации моментальных снимков
- `trycmd` - https://lib.rs/crates/trycmd, действительно надежен, хорошо работает и фантастически прост. Вы записываете свои тесты в виде файла markdown, и он выполняет синтаксический анализ, запускает встроенные команды и отслеживает, какие результаты должны быть сопоставлены с результатом в том же файле markdown, так что ваши тесты также являются живой документацией
Компиляция в двоичный файл
Со временем я сформировал свой рабочий процесс реализации в виде начальных проектов и инструментов, поэтому вместо того, чтобы описывать и демонстрировать, как создать рабочий процесс с нуля, просто используйте эти инструменты и проекты. И если вам интересно — прочтите их код.
Если вы хотите провести здесь время как можно проще, вы можете ознакомиться с этим:
- `rustwrap` — для компиляции встроенных двоичных файлов из Github в homebrew или npm
- `xtaskops` — для переноса некоторой логики из вашего CI в Rust в виде шаблона `xtask`
- `rust-starter` — для использования готовых рабочих процессов CI для упрощения тестирования, компоновки и компиляции
Если у вас есть правильный двоичный файл Rust, вам также следует рассмотреть возможность компиляции в cargo контейнеры, подробнее об этом смотрите в `cargo-binstall`.
Статья на list-site.