Так получилось что в рамках моей основной деятельности пришла пора сделать сервис для манипуляции с ресурсами СХД для виртуальных машин (ВМ). Они подаются в SAN в виде «LUN» («Logical Unit Number»). Пока речь шла о десятках .. первых сотнях LUN, хватало моего старого решения (оно изначально про телефонию и блок-схемы, но на самом деле всё равно подо что делать очередной модуль). А потом он рос, рос, и…

В общем, взял я в руки python.
(Как удобный для прототипирования/»склейки» инструмент. И при этом в моём отделе примерно все могут, как минимум, прочитать написанное).
И наваял обёртку «lun» и пачку модулей в соседней папочке «lun.d». (Догадываюсь что на питоне принято раскладывать в другое место, но здесь — сервис местного значения.)
И вот теперь — главное. Эргономика рабочего места.
Структура вызова запроектирована в CLI таким образом (откидывая ненужные подробности):
# lun edit dc 60:0a:09:80:00:01:02:03:00:00:02:7d:5a:3a:e3:b9 [--extra-keys] # lun attach dc machine-name --lun=lun_name
Ровно один «фактор», и он не должен выламывать пальцы инженеру ТП!
WWN ("60:0a:09:80:00:01:02:03:00:00:02:7d:5a:3a:e3:b9") копипастится с консольки СХД или из задачи. Остальное должно заезжать «магически», примерно всё «интересное».
К делу
Чтобы не плеваться от UI, нужно автодополнять:
-
Имя модуля (список модулей покажет ls в подкаталоге).
-
Аббревиатуру датацентра («dc» в примере выше, спросить можно «
lun get list-dc«). -
Третий позиционный аргумент (больше низзя!), или «
--ключ«. -
Прочие «
--ключи«, по необходимости.
И в этом месте начался реальный трэш. По итогам которого я дал себе обещание написать «хавтушку» для других желающих «как лучше».
-
Хабро-перевод я просмотрел одним из первых. Но, «фактуры маловато».
-
Что можно найти в «man bash», читатель, я полагаю, в курсе. Я не настолько фанат bash (но в этот раз таки немного пришлось и там поползать).
-
Подход к подобному классу задач обычно состоит в том чтобы взять чей-нибудь код для затравки. И порыться в интернетах на предмет приличной статьи на тему. И тут — нате вам. Гугл принялся выдавать репосты одного и того же
chatGPTбреда про автодополнение из bash_history. А подручные «дополнялки» (модули bash-completion от используемого софта) демонстрируют откровенно слабое владение мат.частью. -
В какой-то момент повезло, наконец, наткнуться на devmanual.gentoo.org. Сразу дам ссылки: Ключевые понятия, Подробности про «compgen», Подробности про «complete». Все 3 статьи достаточно компактные, но позволяют начать разбираться в вопросе.
Дальше показываю «как», на своём примере.
«Рыба»:
_lun() { local cur="$2" local prev="$3" local obj cmd base keys key val local LIST="" local WWID="" local LUN="" local cmd="${COMP_WORDS[1]}" local DC="${COMP_WORDS[2]}" … } && complete -F _lun lun
-
complete -Fвызывает функцию_lun(), когда дополняет командуlun. -
Для простых манипуляций достаточно трёх аргументов вызываемой функции (подробнее). Какую команду дополняем (
$1), что дополняем ($2) и что было перед этим ($3). -
Ключевое слово
localпризвано подстраховать переменные внутри функции от «протекания» наружу. Вообще, всё автодополнение в bash свалено в одну кучу, и безграмотными действиями легко сломать работу чужого кода. -
Для более сложных манипуляций доступны массив
COMP_WORDS[]и указатель на его последний элементCOMP_CWORD. Выше видно, как я достаю из него пару «позиционных» аргументов. -
Для «высшего пилотажа» оставлен доступ к
COMP_LINEиCOMP_POINT(вся дополняемая строка и текущее положение курсора).
Первый аргумент:
if [ "${COMP_CWORD}" = "1" ] then # first level -> base objects base="/usr/local/sbin/lun.d" obj=$(cd ${base} && ls -1 *.py | cut -f 1 -d "." | sort -u) COMPREPLY=( $(compgen -W "help ${obj}" -- "${cur}") )
-
Башисты в этом месте должны стукнуть мне по рукам за
cd. Потому что есть параpushd/popd. -
Берём список всех модулей
*.py, добавляем ещё одно ключевое словоhelp(вывести хинт к обёрткеlun) и формируем из этого набор «слов» дляcompgen -W. ВCOMPREPLYвозвращается bash-массив (то что внутри круглых скобок «( … )»). -
В конце вызова
compgenнужно обязательно ставить дополняемое (${cur}). «--«подсказывают GNU’тым утилитам, что дальше-ключейне будет.
Второй аргумент:
elif [ "${COMP_CWORD}" = "2" ] then # second level -> commands DC=$(sudo lun get list-dc) if [ "${cmd}" = "get" ]; then COMPREPLY=( $(compgen -W "help ${DC} list-dc" -- "${cur}") ) else COMPREPLY=( $(compgen -W "help ${DC}" -- "${cur}") ) fi
-
Применение
sudoздесь реально важно. Пользователь имеет доступ к неким привилегированным командам, но неsudo -iдля всех подряд же! -
Вызов
sudo lunполомает работу автодополнения. Потому что дополнение будет дляsudo, а мнение писавшихsudoне всегда совпадает с моим. Чтобы у пользователя всё работало, нужен альяс (где-нибудь в~/.bashrc):alias lun="sudo lun"
Третий аргумент:
elif [ "${COMP_CWORD}" = "3" ] then # third level -> keys keys=`sudo lun ${cmd} args` if [[ "${cmd}" =~ get|add|edit ]]; then if [ "${DC}" = "list-dc" ]; then return fi WWID=$(sudo lun get ${DC} --fields=wwid --compact | cut -d ']' -f 1 | cut -d ' ' -f 2 | tail -n 5) COMPREPLY=( $(compgen -W "${keys} ${WWID}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') ) elif [[ "${cmd}" == vm || "${cmd}" == attach || "${cmd}" == resize ]]; then # compopt -o nospace LIST=`sudo lun vm ${DC} list "${cur}" --cached` COMPREPLY=( $(compgen -W "${LIST}" -- "${cur}" ) ) else COMPREPLY=( $(compgen -W "${keys}" -- "${cur}" | awk '{ if ($0 !~ "^-") {print "'\''"$0"'\''"} else print $0 }') ) fi
-
Про
nospaceбудет ниже. -
Каждый модуль обучен выводить список принимаемых аргументов по ключу
args. Его и показываем, в общем случае. -
list-dcвместо аббревиатуры датацентра выводит список известных ДЦ. Нечего дальше дополнять,return. -
Для команд
get|add|editдополнительно показываем список из пяти последних добавленных LUN. Не супер удачное решение, т.к. в процессе вытаскивает листинг всех LUN в ДЦ. Правильнее было бы не-юниксвейно протащить ограничение вlun get. -
Вот этот фокус с кавычками для WWN (кому не ясно что тут написано, гуглит «bash escape quotes»):
{print "'''"$0"'''"}. Он для того чтобы автодополнялось разделённое «:«. Т.к. «:» входит в список разделителей слов по умолчанию. Глубже копать в этом месте я поленился. -
Для команд
vm,attach,resizeдополняем имя ВМ из закэшированного в локальной БД списка. Выше сравнение было через «=~«, а здесь вот так. Просто потому что сначала команда была одна. -
Автодополнять ну очень желательно откуда-то из «быстрого» кэша. Не уподобляйтесь писателям
yum/dnfи иже с ним. Долгие запросы через ssh, так и вовсе отваливаются по таймауту. Я не нашёл этого места в bash-completion, но не сильно и старался. -
Для всех прочих команд, выводим только список ключей.
Остальные аргументы:
else # other level -> options if [ "${COMP_WORDS[COMP_CWORD]}" = "=" ]; then key=$((COMP_CWORD - 1)) elif [ "${COMP_WORDS[COMP_CWORD-1]}" = "=" ]; then key=$((COMP_CWORD - 2)) fi
Для не-булевых аргументов, ключи вида --key=value, без пробелов. С пробелами я ниасилил. Будем считать это «домашним заданием», для лучшего овладения материалом.
Поехали дополнять:
if [ ! -z "${key}" ]; then if [ "${COMP_WORDS[$key]}" = "--fields" ]; then val="alias host vm scsi blkdeviotune ," local list=$(echo "${cur}" | egrep -o '([a-z]+,)+') cur="${cur/[[:alpha:]]*,/}" COMPREPLY=( "${list}"$(compgen -W "${val}" -- "${cur}") ) compopt -o nospace elif [ "${COMP_WORDS[$key]}" = "--file" ]; then # [ "${cmd}" = "edit" ] compopt -o filenames COMPREPLY=( $(compgen -f -- "${cur}") ) elif [ "${COMP_WORDS[$key]}" = "--vm" ]; then # [ "${cmd}" = "get" ] LUN=`sudo lun vm ${DC} list --cached` COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) ) elif [ "${COMP_WORDS[$key]}" = "--lun" ]; then # [ "${cmd}" = "attach"|"resize" ] local vm="${COMP_WORDS[3]}" LUN=`sudo lun vm ${DC} ${vm} --cached --luns --json | jq -r ".luns[]"` COMPREPLY=( $(compgen -W "${LUN}" -- "${cur}" ) ) else val="<`echo ${COMP_WORDS[$key]} | tr -d '=-'`>" COMPREPLY=( $(compgen -W "${val}" -- "${cur}") ) compopt -o nospace fi else keys=`sudo lun "${COMP_WORDS[1]}" args` COMPREPLY=( $(compgen -W "${keys}" -- "${cur}") ) fi
-
Если ключ не начали вводить, показываем список ключей для модуля (нижний блок
else). Правильно было бы исключить уже́ задействованные ключи из списка. Внутри — питоновский «argparse», он тупо возьмёт последний попавшийся. -
Ключ
--fieldsпринимает на вход список полей через «,«. Такое решение опять-таки поперёк дефолтных настроек разделителей для libreadline, поэтому дальше — фокус:cur="${cur/[[:alpha:]]*,/}". Срезаем всё до последней «запятой». Я вообще в баш-портянках стараюсь ограничиваться средствами баша. Потому что самому потом грозит (в случае чего) разматывать логи аудита. -
В этом месте также следовало бы исключать все ранее перечисленные через «
,» поля из автодополнения. -
Для
--fileнужно автодополнять имена файлов из текущего каталога. Для этого заказываемcompopt -o filenames, иначе не даст «проваливаться» в подкаталоги. Где-то написано что это можно вставлять прямо в вызовcompgen -f. Но так (у меня, bash 4.4.20 из Oracle Linux 8) не работает. -
Для
--lunберём список LUN, относящихся к указанной ВМ. Запросjq -r ".luns[]"достаёт значения (имена LUN) из отданного в json словаря. JSON и «jq» — вообще довольно удобно при парсинге отдаваемого в CLI. Для тех утилит, которые умеют в JSON. -
Всё остальное (после
else) — не знаем как дополнять. По «табулятору» выводим название ключа в угловых скобках (--key=<key>).
Чтобы автодополнялось после «=«, просим не добавлять пробел:
if [[ "${COMPREPLY[@]}" =~ =$ ]]; then # Add space, if there is not a '=' in suggestions compopt -o nospace fi
Всё. Как смог, рассказал. Удачи в улучшении UI/UX для инструментов командной строки!
-
Это мой первый опыт написания (а не подачи в виде лекции) обучающего материала для взрослых тётей и дядей. Буду крайне признателен за конструктивную критику. И, по мере поступления таковой, постараюсь доработать эту статью для улучшения читаемости.
ссылка на оригинал статьи https://habr.com/ru/articles/808729/
Добавить комментарий