less как фронтенд - делаем gopher browser

После долгого отсутствия я вернулся к вам, мои несуществующие читатели, с новой интересной идеей - использованием less, как фронта для скриптов. Вы же знаете less? Это то приложение, которое позволит вам посмотреть длинный файл в консоли, не мучая прокрутку терминала. В нём можно отобразить текст, что-то поискать, прыгнуть туда или даже сюда. Так почему бы не воспользоваться всем этим богатством функций в наших скриптах, особенно учитывая, что у less есть и чуть менее известная функциональность. А именно:

lesskey - добавляем интерактивность

У less есть концепция горячих клавиш, на которые мы назначаем какую-то функциональность из набора встроенных возможностей. Ознакомиться с ними можно в man lesskey, но из всего богатства нас будет интересовать только возможность запускать shell скрипты прямо из less. Биндинги мы будем описывать в обычном текстовом файле, который потом подадим программе на вход, через аргумент командной строки. Старые версии less (< 582) требовали чтобы файл с биндингами был преобразован в бинарный формат программой lesskey, но мы будем использовать только новые, причем желательно собранные вручную из исходников. Это будет полезно ещё и потому, что системная версия less обычно собирается с прицелом на безопасность, и в ней выключена, или ограничена, возможность использования lesskey. Например, в OpenBSD less собран без поддержки запуска shell скриптов, а версия из MacOS вообще лишена возможности использовать свои биндинги. Ни на одной из них наш скрипт работать не будет.

Протокол “суслик”

С помощью less (и небольшого shell скрипта), мы выйдем в интернет и будем посещать там сайты, работающие на протоколе gopher. Это старый протокол, который когда-то был альтернативой www и предназначался для организации иерархичного доступа к документам в сети. Протокол позволяет делать директории, содержащие ссылки на файлы в определенных форматах и другие (саб)директории. Синтаксис формата очень прост. Каждая запись в директории занимает одну строку, первый символ которой обозначает тип контента, на который она ссылается. Мы будем поддерживать три типа:

Стандарт определяет ещё несколько типов, но большая часть из них устарела и не используется сейчас сколько нибудь широко. Ознакомиться с ним можно в RFC 1436. Интерес представляет тип 7 - позволяющий сослаться на сервис полнотекстового поиска - аналог google, только для гофера. Из известных поисковых сервисов есть Veronica-2, к которой можно получить доступ через веб прокси вот здесь. После типа идет строка, которая говорит пользователю о том, что же он увидит по этой ссылке, а затем, разделенные знаком табуляции, идут хост и урл самой ссылки. У формата до сих пор есть активные поклонники, держащие на нем свои сайты и блоги. Обычно это люди, утомленные современным вебом, наслаждающиеся минимализмом, отсутствием js, трекинга и рекламы.

“Готовое” “решение” - skefir

Наш скрипт представляет собой 63 строки кода, написанного на ANSI Shell (отсутствие башизмов проверял с помощью shellcheck). Он будет принимать на вход сервер и url нужного нам ресурса, скачивать его с помощью nc и, если это gopher директория, на лету генерировать набор биндингов, позволяющих перейти к документам в ней. С полным текстом программы можно ознакомиться на gist.github.com, а здесь мы с вами разберем основные моменты, заслуживающие внимания.

LESSKEY_FILE=$(mktemp -t skefir.XXXXXX)
echo "#command" >> "$LESSKEY_FILE"
TEXT=$(printf "%s\r\n" "$2" | nc "$1" 70)

[ -z "${NO_MAP:-}" ] && TEXT="$(echo "$TEXT" | viewer)"
echo "$TEXT" | less --lesskey-src="$LESSKEY_FILE"

rm "$LESSKEY_FILE"

В первой строке мы, с помощью mktemp, делаем временный файл, в котором будем хранить набор сгенерированных биндингов. При запуске less мы передадим этот файл с помощью аргумента --lesskey-src, а после того как пользователь насладится контентом и закроет less, мы этот файл удалим. В переменных $1 и $2 у нас лежит хост и адрес контента, к которому мы хотим получить доступ. Протокол говорит, что мы должны открыть TCP соединение с сервером (обычно на 70-ом порту) и отправить туда нужный нам адрес, завершим его символами \r\n. Для этого мы используем утилиту nc, в которую передаем подготовленную нами строку-адрес и получаем в ответ весь контент, который нам вернул сервер. TCP соединение может закрыться до того, как весь контент будет передан и у протокола нет никакой возможности дать нам знать, что мы скачали всё что было нужно (что делает в http заголовок Content-Length, например). Из-за этого некоторые авторы ставят в конце своих постов точку на отдельной строке, чтобы пользователь смог визуально убедиться, что у него скачался весь текст. Переменная NO_MAP выставляется в true, если на вход программе был передан аргумент -n. Он говорит нам о том, что мы сейчас будем запрашивать текстовый файл, а не директорию с навигацией, и парсить его нам совсем не надо. В таком случае мы просто передадим весь вывод nc на вход less. А вот если парсить все-таки надо, то в ход пойдет функция viewer, которая переформатирует содержимое в человекочитабельный вид и вызовет link для генерации тех самых биндингов. Давайте на неё посмотрим.

viewer () {
    LINK=0
    while read -r LINE || [ -n "$LINE" ]; do
        case "$LINE" in
        i*)
            printf "      "
            echo "$LINE" | awk -F '\t' '{print substr($1, 2)}'
            ;;
        1*)
            link "$LINK" "$LINE" ""
            LINK=$((LINK + 1))
            ;;
        0*)
            link "$LINK" "$LINE" "-n"
            LINK=$((LINK + 1))
            ;;
        *)
            ;;
        esac
    done
}

Мы читаем контент гофер директории строка за строкой и, ориентируясь на первый символ в строке, принимаем решение о том, как нам с ней поступить. Если в начале символ i, то мы парсим строку с помощью awk, делим её по знаку табуляции и выводим пользователю только часть до \t, откусив от этой части первый символ - букву i. После табуляции идет хост и урл, которые не нужны информационному сообщению, так как оно не является ссылкой, но всё равно часто добавляются гофер серверами для обратной совместимости со старыми клиентами. Если в начале строки стоит 0 или 1, то мы имеем дело либо с ссылкой на текстовый файл, либо с ссылкой на директорию. И для того и для того нам нужно сгенерировать кейбиндинг, который будет запускать новый инстанс skefir в текущем терминале. Единственная разница заключается в том, что для текстового файла мы будем прокидывать аргумент -n, говорящий скрипту, что парсить контент как директорию не нужно.

link () {
    SHORTCUT=$(echo "$1" | base64 | tr -d '=')
    LINE=$2
    ARGS=$3

    printf "[%5s] " "$SHORTCUT"
    echo "$LINE" | awk -F '\t' '{print substr($1, 2)}'

    KEFIR_CALL="$(echo "$LINE" | awk -F '\t' '{printf "%s %s", $3, $2}')"
    COMMAND="$SHORTCUT shell skefir $ARGS $KEFIR_CALL"
    printf "%s\n" "$COMMAND" >> "$LESSKEY_FILE"
}

Изначально я помечал ссылки цифровыми шорткатами и, для первых десяти ссылок, это работало очень удобно. К сожалению, обработка горячих клавиш в less реализована таким образом, что при наличие в списке 1 и, например, 14, он при вводе всегда будет сразу вызывать первый шорткат, не дожидаясь окончания ввода числа. Для текстовых команд такого не происходит и я не придумал ничего лучше, чем преобразовывать цифры в буквы через base64. В функции link мы готовим биндинг в формате

MAo shell skefir  gopher.club /phlogs/

где MAo это кейбиндинг, shell означает, что мы хотим выполнить команду в терминале, ну а в конце идет эта самая команда, сформированная на основе просмторенной гофер директории. Теперь мы, просматривая гофер директорию в less, можем набрать MAo и открыть новый less с новым набором биндингов, который уже позволит нам передвигаться по новой директории. Чтобы путешествовать по истории открытых ресурсов назад мы будем просто закрывать новые инстансы less. Ну а путешествие вперед у нас не реализовано. Я выложил на asciinema запись работы skefir - посмотрите, если интересно. Можно заметить, как долго открывается вторая страница - это огромный документ со списком всех пользователей, которые когда-либо делали гофер блоги на sdf.org и мы должны получить и спарсить его целиком до начала рендеринга. В нормальном клиенте, мы бы начали выводить контент до полного парсинга, но мы нормальный тут и не делали.

Выводы

Получилось интересно, но не очень практично. Для комфортного путешествия по сусликам есть куда более классные клиенты с интересными идеями и качественным исполнением, взять хотя бы VF-1. Но свой потенциал у такого использования lessвсе-таки есть. Например, если вам нужно просмотреть кучу файлов и выполнить над ними одно из нескольких типовых действий - то less с кастомными кейбиндингами может упростить вам жизнь.