company_banner

Что не так с вашей консольной программой?

    Мы еще в школе научились вызывать функцию 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.

    Конференции Олега Бунина (Онтико)
    Конференции Олега Бунина

    Комментарии 18

      +3
      скриншоте Manual page от git — супер-популярная, хорошая, и между прочим, очень продуманная программа. Manual page позволяет скроллить вывод вверх-вниз


      Нет такой программы «Manual page», а есть программы, которые рендерят файл в формате mdoc: groff и его современная реализация mandoc.

      P.S. История развития документации UNIX — manpages.bsd.lv/history.html
        +3
        автор открыл для себя man git, а скоро догадается набрать man man и его ждёт много интересного :)
        +3
        Всё это благолепие с разноцветным выводом нравится далеко не всем пользователям, поэтому имеет смысл предусмотреть отключение раскрашивания вывода.

        no-color.org
          0
          Выглядит одинаково хорошо
          Нет. Как минимум акценты (надписи за которые взгляд цепляется в первую очередь) на приведённых скриншотых из-за разных цветовых схем различны.
          и к такому выводу нужно стремиться
          Нет. Вывод консоли не только для просмотра людьми, часто приходится настраивать утилиты анализа логов, тот же fail2ban, и там вся эта красота может сломать многое.
          Вместо:
          Hello World!
          Будет что-то такое:
          \033[0;32mHello\033[0m \033[0;31mWorld!\033[0m
          вроде мелочь, а какую-нибудь регулярку, написанную «побыстрому» сломает на раз.
          здесь наш специалист по UX создал дизайнерский спиннер — у нас он должен крутиться треугольниками, а не палочками, как у всех остальных. Почему бы и нет? Но на Windows он крутится одинаковыми квадратиками, то есть не работает.
          Сами спросили, сами ответили, у всех — потому что так работает, у вас — не работает…
          И, опять же, все эти интерактивные красивости в логах потом выглядят так (вырезал, так сказать несколько строк для краткости):
          Total 144238 (delta 3), reused 144233 (delta 3)
          Checking out files: 7% (12197/157075)
          Checking out files: 8% (12566/157075)
          Checking out files: 9% (14137/157075)
          Checking out files: 10% (15708/157075)
          Checking out files: 11% (17279/157075)

          Checking out files: 97% (152363/157075)
          Checking out files: 98% (153934/157075)
          Checking out files: 99% (155505/157075)
          Checking out files: 100% (157075/157075)
          Checking out files: 100% (157075/157075), done.
          потому что для текстового лога нет понятия «передвинуть каретку без перевод строки».
          к счастью, сейчас наступила эпоха Windows 10

          При этом в коде GetStdHandle и SetConsoleMode и у обеих в разделе поддержки:
          Requirements

          Minimum supported client Windows 2000 Professional [desktop apps only]
          Minimum supported server Windows 2000 Server [desktop apps only]

          для параметров
          ENABLE_PROCESSED_INPUT и ENABLE_VIRTUAL_TERMINAL_PROCESSING ограничений по версии тоже не установлено. Спрашивается, как связаны Win10 и этот кусок кода, который прекрасно сработате на любой версии начиная от Windows 2000 и старше?
          Если вам нужен функционал CreatePseudoConsole, как я предполагаю, то приведённый кусок кода не поможет ни как. Или скриншот не тот приложили к статье?

          И ещё полно таких моментов… все описывать — статья не меньше текущей будет.

          Если честно, статья какая-то технически… безграмотная что ли.
            +6
            Нет. Вывод консоли не только для просмотра людьми, часто приходится настраивать утилиты анализа логов, тот же fail2ban, и там вся эта красота может сломать многое.

            В статье про это вообще-то написано:


            И когда мы, например, перенаправляем вывод через py в grep, чтобы что-нибудь там поискать, или записываем в файл, или запускаем из-под cron, HTTP-сервера или еще чего-нибудь — функция os.isatty() будет возвращать false:

            В таких случаях нельзя выводить ни цвет, ни стили, ни размер экрана.
              –1
              спустя треть статьи, вскользь и не потому, что это создаст мусор в логах, а
              Потому что размера экрана нет, а при попытке его спросить вы получите исключение
              то есть исключительно из-за «оно ж может упасть».
            +8
            Вот вывод команды (...) на темном фоне:
            И он же со светлой схемой:
            Выглядит одинаково хорошо, и к такому выводу нужно стремиться.
            Я же не один не могу без чудовищных усилий прочитать этот тёмно-красный на тёмно-сером?
              +2
              Там еще скриншот в JPEG, а он уродует красный текст.
              Вот что действительно нечитаемо — это темно-синий текст на черном фоне.
              рандомный скриншот из интернета
              image
              +2
              Например, у всем известного Docker’a вывод — это простыня ровного, скучного и не выделяемого текста. В нем просто трудно ориентироваться

              Да в общем-то нормально, как распечатка конфига по уровню читаемости. Можно бы отступ побольше, но это уже придирки.


              Один беглый взгляд на экран, и вы сразу можете выделить, что важно, а что — не очень

              А как в случае с Docker определить, что важно мне сейчас, а что — не очень? По-моему, там просто и выделять нечего особо — всё и так ясно.


              Нет, я согласен, что цвета в выводе могут сделать его гораздо приятнее для чтения, но пихать их куда попало — идея так себе. Нельзя забывать и про проверку на редирект вывода куда-то и выключать всякие свистелки форматирования в таком случае.

                +4
                Ужасный пример с тем, что автор видит разницы между терминалом и командами, сравнивая вывод «gh pr status» и «docker ps» и считая что это именно докер плохой.

                Сделай названия бренчей или коммит мессаджей подлиннее, и они тоже не влезут. И уже будет кто плохой?
                Это tty переносит на следующую строку часть информации, а не докер. А при парсинге вывода будет понятно, что это одна строка.
                Во-вторых никто не мешает вам выводить docker ps только те столбцы, которые нужны, а не все подряд.
                docker ps -a --format "table {{.ID}}\t{{.Image}}\t{{.Command}}"

                И gt вот так может?
                docker ps -a --format '{{json .}}'

                В статье есть несколько интересных инструментов для причесывания внешнего вида, но вот первый же пример меня сразу настроил против автора.
                  0
                  Ужасный пример с тем, что автор видит разницы между терминалом и командами, сравнивая вывод «gh pr status» и «docker ps»

                  Если честно эту часть вообще не понял. Не могли бы пояснить?

                    0
                    Могу предположить, что основной посыл — нельзя сравнивать «в лоб» вывод абсолютно разных программ, особенно когда этот вывод можно настраивать под свои нужды.
                      0
                      В том, что
                      1) перенос на вторую строку делает не docker и не gh, а терминал. То есть если в gh в вывод попадет длинная строка (например длинный коммент или длинное имя бренча), то также само будет переноситься на вторую строку. Таким образом это проблема не команды, а конкретного набора данных.

                      2) вывод docker ps можно настроить как угодно. Любые нужные тебе столбцы. То, что дефолтный вывод выводит чуть больше, чем влазит в 80 строк, не делает хуже, наоборот…
                    +2

                    про бесполезность красивостей уже сказали, особо раздражает потом в дженкинсе это цвета читать.


                    ну а курсор вернуть можно набрав аслепую
                    reset[enter]

                      +1

                      К статье не помешала бы ссылка на репозиторий шаблонного консольного приложения с уже реализованными примерами всего перечисленного и с авто-тестами на всякие нюансы с кодировками и прочим.
                      Бывают еще кукикаттеры, можно сделать шаблончик для оного из них.

                        +1
                        Раскраска — это очень плохой подход.
                        Намного правильней отмечать строки, например, [I] — info, [W] — warning, [E] — error.
                        Подобный подход применяется в логах, например, X-сервера.
                        Зачем?
                        Потому что можно сделать так:
                        ./dosomething | grep '[E]'
                        И получить только то что нужно. А попробуй сделать то же самое по цветам. Ага…

                          +2

                          тут еще надо различать логи и вывод на консоль. логи обязаны быть грепабительными. вывод в консоль должен быть максимально оптимизирован под пайпы и прочие автлматические вырезания столбцов и строк.
                          а статья применима только к небольшому классу конечных интерактивных приложений типа top

                          0

                          А ещё есть очень продвинутая библиотека под названием wasabi. Для простоты я использую docopt + wasabi. Click через чур монструозный, все зависит от задачи.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое