Что не так с вашей консольной программой?
Мы еще в школе научились вызывать функцию print. Что может пойти не так в консольной разработке? Да, и если бы не растущая сложность программ, проблем бы у нас не было до сих пор. А в реальности — то в тексте трудно найти нужную информацию, то он не влезает в экран по ширине и по длине, а от многочисленности цветов рябит в глазах.
Но как часто мы обсуждаем наши повседневные инструменты с точки зрения читабельности, хотя пишем под web и каждый день используем консольные утилиты? Сегодня Андрей Светлов расскажет, что со всем этим делать, и чем он пользуется для консолей. Помимо того, что Андрей CPython Core developer и понемногу развивает Python, в свободное от работы время он эксперт по asyncio, со-автор aiohttp, yarl, multidict и прочим популярным библиотекам.
Помимо всяких мелочей, которые решаются легко, я расскажу и о том, как же нам упростить просмотр. Я не имею в виду режим текстового редактора, у нас интерактивность в основном односторонняя: запустили программу, выбрали команду, поработали. То есть нам должны хорошо, приятно, доходчиво показывать текст, а сами мы обычно мало что вводим во время работы программы.
Информативность
Это самая первая и очевидная проблема. Например, у всем известного Docker«a вывод — это простыня ровного, скучного и не выделяемого текста. В нем просто трудно ориентироваться:
Для контраста посмотрите на новомодную Github«овскую штучку — утилиту для работы с самим Github. Здесь есть стили, цвета, подсветка. Один беглый взгляд на экран, и вы сразу можете выделить, что важно, а что — не очень:
Размер шрифта и экрана
Снова тот же Docker. Если экран не очень широкий, если шрифт большой или вертикальный экран, то Docker перестает помещаться и выводится на две строки:
И это еще не худший случай, тут можно о чем-то догадаться. А если у вас широченная таблица, которая начинает схлопываться в 3, 4 и более строчек, то разобраться в ней в таком виде решительно невозможно.
А ведь на консоли уже можно делать то, что давным-давно изобрели в мобильных приложениях — responsive design — когда размер текста перестраивается в зависимости от размера экрана. Минус только один — никто за нас не написал удобные библиотеки-помогайки, всё приходится делать самим.
Scrolling & Pager
Вот хороший пример: на скриншоте Manual page от git — супер-популярная, хорошая, и между прочим, очень продуманная программа. Manual page позволяет скроллить вывод вверх-вниз, искать текст с помощью pager, и что-то еще выделять стилями. И если у вас на экране списки файлов, объектов, какие-то большие и длинные таблицы, то это хорошая помощь:
Светлый/темный цвет фона
Было бы здорово, если бы у всех была темная консоль (автомобиль может быть любого цвета, если этот цвет — черный). Но жизнь не такая: светлая и тёмная тема — это как остроконечники и тупоконечники. Победителя нет, есть бесконечный спор. Но программа должна работать с обоими режимами одинаково хорошо.
Что из этого следует? Возвращаемся к примеру от Github. Вот вывод команды (неважно какой, сейчас это статус моих pull requests) на темном фоне:
И он же со светлой схемой:
Выглядит одинаково хорошо, и к такому выводу нужно стремиться. Но чтобы этого достичь, придется себя ограничивать.
ЦВЕТА И СТИЛИ
Все, что мы можем использовать, чтобы не рябило в глазах — пяток цветов и еще несколько стилей:
На этом выбор цветов для нас закончен! Конечно, такая — очень узкая — палитра не позволяет использовать полноцветные терминалы и точно перенести всё, что ваш UX дизайнер нарисовал в Фотошопе. Но зато у нее есть одно чудесное свойство: если приглядеться, то видно, что, например, желтый — это не классический желтый, а немного золотистый, чтобы выглядело хорошо. И все цвета подобраны программой терминала так же. Если меняется тема, то программа снова подскажет, как правильно, хорошо, контрастно и красиво нарисовать этот цвет для терминала.
И пока мы остаемся в этом цветовом диапазоне, с выводом у нас всё будет нормально. Но как только начинаем изобретать что-то свое, начинаются проблемы — то, что выглядело хорошо в вашем окружении, у соседа начинает смотреться отвратительно.
Обычно мы, конечно, выбираем понятные всем цвета:
Зеленый — все хорошо;
Красный — плохо;
Желтый — warning.
Но если хотим большего, у нас есть подсказка — для команды LS есть соглашение, каким цветом мы будем выводить файлы и папки. Это обеспечивается двумя переменными среды. Первая исторически появилась в виде LSCOLORS и рассказывает, как рисовать файлы и папки: по две буквы на одну позицию. Позиция — это папка (нормальный файл, сокет, что-то еще). В документации или в интернете это всё есть. Первая буква отвечает за цвет шрифта (букв), вторая — за цвет фона. Я попытался раскрасить так, как у меня закодировано на моей рабочей станции
Второй вариант немножко похитрее: помимо обозначений для папок и символических ссылок, можно еще рассказывать, какие окончания файлов рендерить:
И если у вас вывод, похожий на файлы, и там можно применить эти цвета, стоит написать простенький парсер и самому отформатировать вывод. Достоинство всё то же: эти цвета для LS настроены темой вашего терминала. В терминале они выглядят хорошо, естественно, сбалансировано — и ваша программа при их использовании будет выглядеть так же.
Смайлики
Смайлики нельзя недооценивать, это очень хорошая вещь — простенький значок, но позволяет быстро сориентироваться на экране:
Но нужно помнить, что смайликов очень и очень много, таблица Unicode также очень большая — в результате не все символы отображаются одинаково хорошо. Например, смайлик «улыбающийся человечек» на Windows-консоли, как правило, не отображается: ? → □
Поэтому выбирайте простые символы, смотрите, как они рендерятся в разных режимах и проверяйте это на всех платформах (Windows, MAC, Ubuntu). И математические символы в том числе. Везде есть хитрые смайлики, с которыми могут быть проблемы.
Shell
Еще нужно помнить, что терминал существует не сам по себе. Консольные программы, в нем запускаются под разными shell: sh, bash, zsh, fish, cmd.exe, powershell или еще какие-то. Программа должна работать с выбранным shell без проблем, в том числе на Windows. Но на практике мы видим разницу в том, как shell авто-дополняет ввод и как (и в каком терминале) выводятся символы. Поэтому проверяйте и на своем shell, и на тех, которые будут у пользователей.
TTY навсегда?
Помимо shell и интерактивного режима, на который в консолях тратится большая часть усилий по пользовательскому дизайну, программы у нас могут запускаться и без терминала. И когда мы, например, перенаправляем вывод через py в grep, чтобы что-нибудь там поискать, или записываем в файл, или запускаем из-под cron, HTTP-сервера или еще чего-нибудь — функция os.isatty () будет возвращать false:
В таких случаях нельзя выводить ни цвет, ни стили, ни размер экрана. Потому что размера экрана нет, а при попытке его спросить вы получите исключение. Но вывод должен работать и без терминала. Поэтому лучше дописать еще один тест и убедиться, что всё хотя бы базово работает даже без терминала.
Windows, любовь моя
К сожалению, мир консольных программ делится не только на темный и светлый фон, а еще на Windows и всех остальных. Если на posix системах (тех же MAC и Linux) всё весьма похоже, то на Windows есть много отличий, например:
less → more. Стандартная прокрутка less отсутствует, вместо нее есть куда более гадкое и неудобное more.
\n → \r\n. Возврат каретки другой.
dim / gray. Серого цвета нет (но на MAC, кстати, тоже бардак по поводу цвета, поэтому и надо всё проверять).
ANSI escape символы, которые как раз делают расцветку и прочие полезные вещи, по умолчанию выключены, но это легко поправить.
◢◣◤◥ → -\|/. Многие символы, как я говорил, не работают. Например, здесь наш специалист по UX создал дизайнерский спиннер — у нас он должен крутиться треугольниками, а не палочками, как у всех остальных. Почему бы и нет? Но на Windows он крутится одинаковыми квадратиками, то есть не работает.
Инструменты
Расскажу теперь, какими чудо-инструментами можно (и нужно) пользоваться при создании консольных программ и их интерфейсов. Некоторые инструменты действительно чудо — там и молоток есть, и напильник, иногда и кувалда встречается. Единственный нюанс. Так как консольная утилита — вещь маргинальная и нишевая, ее разработчики создают сами для себя, то инструментарий может быть не таким классным, каким бы он мог быть, и не таким доведенным до ума, как для web-программ, например.
CLICK
Я очень рекомендую Click от славного парня Армена Ронахена. Это инструмент со своими особенностями, но он гораздо лучше и мощнее, чем встроенный в Python argparse. Если вы сомневаетесь, используйте Click.
В нем есть набор утилит (функций), чтобы выводить тексты со стилями — можно печатать или накладывать стиль, чтобы получилась строка с анти-последовательностями. Можно снимать стили, использовать pager:
Кроме того, у Click есть маленькая, но очень приятная и удобная фича — он автоматически убирает стили для не-терминала (non-TTY). Click сам понимает, когда вывод идет не на полноценный терминал, а, например, куда-нибудь в файл — он автоматически снимает все стили и делает click.unstyle. Конечно, вы можете сделать unstyling сами, вместо использования click. Но в любом случае избегайте перенаправления в файл покореженного текста с кучей непонятных значков.
PYTHON PROMPT TOOLKIT
Второй инструмент — чудесная штука, которая используется, например, в IPython, BPython, в других shell — это инструмент для создания полноценных приложений. Но нас сейчас интересуют вопросы ввода-вывода — и здесь Prompt Toolkit решает все вопросы.
Сначала мне показалось, что Prompt Toolkit избыточен — потому что для работы хватает и Click. Например, если нужен progressbar, есть всем известный tqdm. Великолепная библиотека, которая решает ровно одну задачу, но делает это хорошо. А еще есть и click.progressbar ().
Prompt Toolkit же позволяет легко и просто создавать различные варианты ввода-вывода, используя стили, шапки и прочие штуки. Например, есть обновляемый виджет для progressbar. Вроде бы ничего особого, но у Prompt Toolkit это не один виджет для progressbar.
Из Prompt Toolkit можно собирать очень сложные вещи, используя layout, виджеты, компоновку. А если чего-то нет из коробки, это можно написать.
Благодаря слоям, в Python Prompt Toolkit можно легко отрисовать несколько progressbar-ов — по одному на слой загружаемого образа — таких же, как например, делает Docker pool:
ВСЁ ПРОПАЛО, ШЕФ! ИЛИ «ГДЕ МОЙ КУРСОР?»
Мелочь, которая в свое время попортила мне немало крови.
Распространенная тема: есть консольная программа, которая рисует чудесные виджеты, рассказывает, как Docker Image тянется на много потоков, даже не моргает и отрисовывает всё гладко. Но если ее внезапно закрыть, может, например, пропасть курсор — потому что в последнем режиме курсор спрятали, а обратно не вернули. Бывает, что вы в терминале печатаете, а курсор не мигает на экране. Есть и более сложные способы испортить консоль, загнав ее в какой-нибудь режим, который не предназначен для интерактивного вывода.
Чтобы этого не было, основная программа при выходе должна напечатать магическую скрипт-последовательность на экран:
Это так называемый Soft Reset, который сбрасывает режимы. Например, тот же less умеет переключаться для полноэкранного скроллинга в альтернативный режим. Но если ваша программа пытается реализовать функциональность как old less, то без магического скрипта при выходе надо будет переключаться обратно вручную.
ASYNCIO + CLICK
Я не могу не рассказать про asyncio!
Но Click из коробки не работает с asyncio — от слова совсем! Он это не умеет, он написан немного раньше, и они совсем не дружат. Поэтому самым простым решением будет написать AsyncioRunner (), который будет не функцией, а классом, а run можно вызывать несколько раз. Это бывает удобно, например, когда запускаем асинхронный код для проверки типов входных параметров и вдобавок что-то еще — run запускаются друг за другом в одном и том же контексте:
И что важно — AsyncioRunner () работает при этом как асинхронный контекстный менеджер, то есть по завершению работы чистит за собой.
Мы у себя используем простое правило: неблокирующий код (тот, который выполняется мгновенно) может быть синхронным, пока Click не читает файлы, не лезет в интернет или еще что-нибудь такое не делает. Но как только нам нужно запускать асинхронный код, мы пользуемся AsyncioRunner (). Легко создается какой-нибудь декоратор, который внутри async-команд сделает все, что нам надо:
ASYNCIO + PROMPT_TOOLKIT
А вот Asyncio + Prompt_Toolkit работают вместе великолепно даже из коробки. Prompt_Toolkit знает об asyncio, а Prompt_async — это стандартная штука Prompt_Toolkit, которая и запускает основную программу. Детали читайте в документации:
WINDOWS не отпускает
Windows из коробки не умеет пользоваться escape-последовательностью — у нее свой набор функций, чтобы поменять цвет, сделать жирным, стереть экран и т.д. Это дико неудобно.
Давно известный проект Colorama работает почти со всеми escape-последовательностями, подменяя собой stdout и stderr. Он парсит то, что печатается, находит там escape-последовательности и убирает их. Вместо этого вызываются разные Windows-функции для того, чтобы поменять тот же самый цвет букв или цвет фона. Но Colorama работает только с подмножеством ANSI-символов.
Полного набора escape-последовательностей, между нами говоря, не существует. Есть много разных терминалов. Начиная от древних и заканчивая актуальными (те же MAC- и Linux-терминалы), у которых хоть и есть некоторые разные escape-последовательности, но в целом они хорошо пересекаются.
Но, к счастью, сейчас наступила эпоха Windows 10. К счастью, потому что в ней можно перевести экран в режим, который обрабатывает escape-последовательности (по умолчанию он не включен). Этот режим позволяют включить две простые функции, вызвать их из Python при помощи ctypes — это упражнение на пару минут:
АВТОЗАПОЛНЕНИЕ
В Click оно есть из коробки, но не для Windows. Так уж получилось. Может быть, в следующих версиях будет по-другому. Для Unix-мира оно есть, и это уже хорошо.
Здесь декоратор принимает click.argument от autocompletion — то есть функция вызывается тогда, когда в процессе вывода мы нажимаем табуляцию, как обычный Bash, а еще лучше Zsh — как это делают shell для большого количества команд.
ВАЛИДАЦИЯ
В Click, разумеется, есть стандартные типы чисел, дат и файлов. Но если хочется сделать нестандартный тип, например, URL, то мы можем написать класс для параметра (будем его указывать в аргументе или опции), и Click автоматически вызовет метод convert, давая возможность нашему коду проверить, преобразовать тип и т.д.:
Я показал основы, которые мы используем для работы, в том числе и у себя. Совершенству нет предела — вы можете делать ваши консольные программы удобными и читабельными. Не бойтесь экспериментировать, но всегда проверяйте, как выглядит результат на разных операционных системах и в различных цветовых схемах.
Конференция для Python-разработчиков Python Conf++ 2021 пройдет 26 и 27 сентября в Москве. Сейчас уже открыт прием докладов. Если вы хотите поделиться тем важным и интересным, что вы нашли, разработали и открыли во время пандемии — велком! Программный комитет рассмотрит вашу заявку, и, если ваш доклад будет принят, поможет с его подготовкой на каждом его шагу.
Подписывайтесь на наши новости о конференции, чтобы быть в курсе всех изменений, новинок и интересностей!
Телеграм здесь и здесь, FB, VK, Twitter.