sttemp - пишем простой менеджер шаблонов

Как-то раз, мне захотелось собрать свои заготовки мейкфайлов, скелеты для скриптов и man страниц, тексты лицензий и прочую ерунду в одной директории и научиться удобно их использовать. Я не люблю привязанные к редактору схемы, так что мне нужен был максимально простой консольный менеджер шаблонов. Ну а люблю я костыли и велосипеды, поэтому такой менеджер я решил написать себе сам.

Первая версия была написана на C, использовала максимально простую реализацию и максимально уродский синтаксис для подстановки значений в шаблоны. Вот такой вот: {|variable|}.

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

Почему на stderr? Чтобы stdout можно было перенаправить в файл и всё равно увидеть, что прога чего-то от тебя хочет.

Так она и работал, причем вполне неплохо, чуть больше двух лет, если верить истории коммитов, пока я не решил переписать её на языке Shell. Я хотел упростить синтаксис подстановок, передать почти всю работу envsubst из состава GNU gettext utilities, и реализовать ещё несколько идей, которые позволили мне вокруг простой программы сделать такую же простую и маленькую экосистему использования.

Основная идея не поменялась. Всё так же есть директория с шаблонами, правда она переехала в $XDG_DATA_HOME/.local/share/sttemp, в знак уважения к free desktop, в них есть подстановки, но уже более привычного вида, в духе $VARIABLE и мы либо находим их в переменных окружения, либо спрашиваем у пользователя на stderr.

Shell и envsubst позволили мне сильно сократить размер программы, я удалил из репозитория 408 строк, вместе с .h и Makefile, и добавил 62, получив аналогичную программу. Она настолько проста, что не буду останавливаться на ней подробнее, приведу только одну функцию, которая будет иметь значение для дальнейшего повествования. А именно - ask. Всё что она делает, это принимает на вход имя подстановки из шаблона, запрашивает её значение у пользователя и возвращает текстом из себя.

ask () {
	echo "Enter $1:" >&2
	read -r value
	echo "$value"
}

Покончив с переписыванием, я двинулся к следующей идее. Я хотел иметь возможность использовать те же самые шаблоны, по точно такому сценарию, но уже в GUI приложениях. В редакторе, в браузере, где мне будет угодно. Так родился скрипт dsttemp.

Главной проблемой было то, как же мне спросить у пользователя, что он хочет подставить в шаблон. На помощь пришла утилита dmenu, от ребят из suckless. Утилита невероятно простая и настолько же невероятно полезная. Всё что она делает, это читает со стандартного ввода список вариантов, разделенных переносом строки, а потом позволяет пользователю, с помощью ввода какой-то части варианта, выбрать нужный, который и возвращает. Если вы когда-либо использовали fzf, то должны понять как оно работает. При этом, если пользователь вводит что-то не из набора изначальных вариантов, то возвращен будет этот самый ввод. В итоге, мы можем переписать нашу функцию ask как-то так:

ask () {
	echo $(dmenu -p "Enter $1" </dev/null)
}

Через -p мы задаем промпт для пользователя, а перенаправление /dev/null на вход нам нужно чтобы никаких вариантов пользователю не высвечивалось. Осталась одна проблема. Эта функция написана в одном файле - dsttemp, а вызывается в совсем другом, в sttemp.

Для решения этой проблемы я засунул код функции в переменную окружения STTEMP_ASK, а в sttemp добавил следующий код:

[ -n "${STTEMP_ASK:-}" ] && eval "$STTEMP_ASK"

Я уже говорил, что люблю костыли? В итоге, dsttemp принял следующий вид:

#!/usr/bin/env sh

set -eu

export STTEMP_ASK=$(
	cat <<'EOF'
ask () {
	echo $(dmenu -p "Enter $1" </dev/null)
}
EOF
)

TEMPL_NAME=$(sttemp -l | dmenu)
TEMPL_TEXT=$(sttemp "$TEMPL_NAME")

echo "$TEMPL_TEXT" | xclip -selection clipboard
xdotool key --clearmodifiers "Shift+Insert"

Сначала мы засовываем в переменную окружения код, которым хотим заменить оригинальную, консольную, функцию ask. Потом, тоже через dmenu предлагаем пользователю выбрать шаблон из списка, который возвращает sttemp -l. Дождавшись, когда пользователь заполнит все нужные переменные и к нам попадет итоговый текст, мы передаем его в иксовый буфер обмена, из которого вставляем его туда, где стоит курсор пользователя. В иксах несколько буферов обмена, возможно конкретный буфер и конкретную комбинацию клавиш нужно будет настроить исходя из того, каким окружением вы пользуетесь. У меня в dwm всё работает как часы.

Как видите, мы можем заскриптовать своё поведение в иксах с помощью консольных утилит, таких как xclip и xdotool. Это круто и это играет важную роль в следующем, и финальном на сегодня, скрипте.

Я читаю достаточно много статей в браузере и иногда мне хочется скопировать часть текста и отправить в свои заметки, чтобы потом над этим текстом отдельно подумать. В общем, хотелось что-то вроде Pocket, но попроще и чтобы он работал на меня, а не на Mozilla Corporation. И конечно же я не хочу ни писать, ни искать браузерное расширение. Я предпочитаю расширять Unix, а не браузер. Браузер и так расширился дальше некуда.

Для начала я набросал вот такой шаблон, назвав его webnote.

# $TITLE

$BODY

Source: $URL

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

С телом нет никаких проблем, просто считаем выделение в переменную.

export BODY=$(xclip -o)

С источником заметки история чуть более хитрая. Мы опять заскриптуем своё поведение в иксах, а именно нажатие Ctrl + l, для выделения содержимого address bar (у меня Chromium btw), а потом скопируем его и заключим в очередную переменную.

xdotool key --clearmodifiers "Ctrl+l"
xdotool key --clearmodifiers "Ctrl+c"
export URL=$(xclip -o -selection primary)
xdotool key --clearmodifiers "Escape"

Escape мы нажмем просто чтобы вернуться к тому состоянию, в котором пользователь начал выполнение скрипта. Теперь у нас есть всё, кроме заголовка, который пользователь заполнит сам. Осталось просто вызвать sttemp и сохранить его вывод туда, куда мы хотим.

FILE_NAME=$(date '+%Y-%m-%d-%H%M%S')
sttemp webnote >> "/home/anton/public_gopher/web/$FILE_NAME.md"

Вот собственно и всё. Полный исходный код доступен как на Github, так и на self host git. Зачем я вам всё это рассказал? Меня очаровывает тот факт, что даже простые программы, если объединять их с другими простыми программами, могут давать крутые эффекты, и даже наши действия в GUI можно и нужно скриптовать. А очаровывает ли это вас?