Бывает в жизни такое настроение, что душа хочет праздника. А так как по долгу деятельности я 80% времени провожу в консоли, то праздника и красоты хочется именно там! И тут на помощь к нам приходят escape последовательности. Они наполняют серый терминал разнообразием и свежестью! Они бибикают, перечеркивают, и расскрашивают текст. Вот лишь небольшая демонстрация их мощи:
for i in 6 1 19 20 5 14 22; do printf "\033[1;2;3$((i%8))m\x$(printf %x $((i+64)))\033[0m"; done; echo |
Тема расскраски консоли — избита на просторах интернета. И эта статья совсем не об этом, но мы конечно обсудим, что тут произошло. А поговорим мы о форматировании и напишем свой аналог column. Для начала — пара слов о column. Справочное руководство программы подсказывает отличный пример её целевого назначения:
printf "a:b:c\n1::3\n" | column -t -s ':' a b c 1 3 |
всякий раз, когда вы используете column — вы хотите красиво форматировать текст. С целью улучшения читабельности или исскуства ради. Column — хороший инструмент, но как и всё в нашем мире, он не лишен недостатков. Что бы раскрыть эту мысль, поговорим опять про цвета.
В стартовом примере происходит следующее:
$(printf %x $((i+64))) |
раскрывается в hex код буквы, которую потом следует напечатать. На данном шаге bash запоминает некое число, которое потом подставит в printf.
$((i%8)) |
данный код будет модификатором цвета для escape последовательности.
printf "\033[1;2;3$((i%8))m # Это управляющая последовательность, bash честно печатает её как есть, # но ваш терминал реагирует на специальное сочетание байт # и подменяет их на цвет. Точнне, он переключает цвет терминала. # Всё, что будет напечатано далее, будет напечатано этим цветом. \x$(printf %x $((i+64))) # Мы уже выяснили, что в $(...) содержится hex код буквы. # Теперь мы просим printf подставить # вместо кода саму букву. \033[0m" # Наконец мы просим терминал переключить цветовой режим в исходный. |
Замечу, что в нашем примере нет необходимости после каждой буквы переключать цветовой режим в исходный. Код можно переписать так:
for i in 6 1 19 20 5 14 22; do printf "\033[1;2;3$((i%8))m\x$(printf %x $((i+64)))"; done; echo -e "\033[0m" |
Результат от этого не изменится т.к. терминал всякий раз честно переключает режим и конструкция «выключить; включить такой-то» выглядит слегка бессмысленно. Я мотивирую такое написание своим опытом и дальнейшими примерами.
Но вернемся к column и добавим немножко музыки красок. Для удобства введем следующие переменные:
COLOR_RESET="\033[0m" COLOR_RED="\033[1;1;31m" COLOR_GREEN="\033[1;1;32m" COLOR_YELLOW="\033[1;1;33m" COLOR_BLUE="\033[1;1;34m" COLOR_PINK="\033[1;1;35m" COLOR_WHITE="\033[1;1;37m" |
и попробуем расскрасить пример из man. Я хочу выделить первую «A» т.к. именно с неё начинается моё имя. Ну а зелёный — мой любимый цвет. Пробуем:
printf "${COLOR_GREEN}A${COLOR_RESET}:b:c\n1::3\n" | column -t -s ':' |
Аааа..! Перфекционистам рекомендую скорее читать дальше. Ну а с теми кто остался давайте разберёмся. Как я уже говорил, вся интерпретация escape последовательностей идёт на уровне вашего терминала и только. Когда вы передаете вывод в column он видит всё как один текст. К сожалению, он ничего не знает про цвета и специальные последовательности. И в соответсвии со своим кодом честно всё форматрирует. Вставляя нужное количество отступов(пробелов) между словами. Когда же текст «выходит» из column, терминал съедает 19 символов заменяя их на переключение цвета. Можно ли починить column? Увы, никак. Или патчить код или хачить ввод. Например так:
W="${COLOR_WHITE}" RST="${COLOR_RESET}" printf "${COLOR_GREEN}A${RST}:${W}b${RST}:${W}c${RST}\n${W}1${RST}:${W} ${RST}:${W}3${RST}\n" | column -t -s ':' |
хорошая попытка, но… нет. Мы добавили везде белого цвета, что бы терминал у каждого «слова» украл равное количество символов и всё выглядело невинно. Кроме того, пришлось добавить лишний пробел во второй строке.
Теперь, мой дорогой читатель, тебе ясна мотивация к написанию своей библиотеки форматирования вывода на bash. Приступим!
Определимся с задачей. Когда я что-то форматирую, у меня первая строка — это заголовок. И обычно, я хочу добавить следующую строку и заполнить какие-то из её столбцов. Беспокоиться о том, какой именно это столбец не слишком удобно. Куда приятнее писать по имени. Для примера:
echo "pid=1111 mem=40% cpu=6%" | todo.sh |
удобнее, чем
echo -e "pid mem cpu\n1111 40% 6%" | column -t |
Так же мне очень нравятся VCS, в частности git и их pull/push.
Всё, техническое задание к разработке:
Создаем дирректорию проекта, инициализируем гит.. и запишем следующее в column.sh:
#!/bin/bash COLOR_RESET="\033[0m" COLOR_RED="\033[1;1;31m" COLOR_GREEN="\033[1;1;32m" COLOR_YELLOW="\033[1;1;33m" COLOR_BLUE="\033[1;1;34m" COLOR_PINK="\033[1;1;35m" COLOR_WHITE="\033[1;1;37m" format_msg_maxline=1 format_msg_maxpos=0 function \ push_format_MSG() { local pos parm_name parm_value j local max_line=${format_msg_maxline} offset=0 while [ "$1" ] do parm_name="${1%%=*}" parm_value="${1#*=}" for ((j=1; j<=format_msg_maxpos; j++)) do if [ "${FORMAT_MSG[${j}]}" == "${parm_name}" ] then FORMAT_MSG[$((100*max_line+j))]="${parm_value}" break fi done if ((j>format_msg_maxpos)) then FORMAT_MSG[$j]="${parm_name}" FORMAT_MSG_LEN[$j]="${#parm_name}" FORMAT_MSG[$((100*max_line+j))]="${parm_value}" format_msg_maxpos=$j fi if [ "${FORMAT_MSG[$((100*max_line+j))]:1:4}" = '033[' ] then offset=19 else offset=0 fi if ((((${#FORMAT_MSG[$((100*max_line+j))]}-offset))>$((${FORMAT_MSG_LEN[${j}]}+0)))) then FORMAT_MSG_LEN[${j}]=$((${#FORMAT_MSG[$((100*max_line+j))]}-offset)) fi shift done ((format_msg_maxline++)) } |
Рассмотрим, что тут происходит:
while [ "$1" ]; do ... shift; done |
Смысл у неё простой, цикл выполняется до тех пор, пока в $1 есть значение. А $1 — это первый аргумент скрипта.. Если бы не shift.
При вызове shift сдвигает вверх указатель на первый элемент. В итоге у нас $1 пробегает последовательно все значения из [email protected]
"${1%%=*}" |
читается как удалить все символы с конца до знака равенства включительно.
"${1#*=}" |
читается как удалить все символы с начала до первого знака равенства включительно.
Половина дела позади! Мы написали функцию, которая добавляет массив. Теперь напишем, которая из массива забирает.
Допишем в column.sh следующий код:
function \ pull_format_MSG() { local slen offset=0 for ((line=0; line<=$format_msg_maxline; line++)) do slen=0 for ((pos=1; pos<=$format_msg_maxpos; pos++)) do if [ -z "${FORMAT_MSG[$((line*100+pos))]}" ] then FORMAT_MSG[$((line*100+pos))]=" " fi if [ "${FORMAT_MSG[$((line*100+pos))]:1:4}" = '033[' ] then msg_len=$((${#FORMAT_MSG[$((line*100+pos))]}-19)) else msg_len=${#FORMAT_MSG[$((line*100+pos))]} fi printf "%$((slen+msg_len))b " "${FORMAT_MSG[$((line*100+pos))]}" if [ "${FORMAT_MSG[$((line*100+pos+1))]:1:4}" = '033[' ] then offset=13 fi slen=$((${FORMAT_MSG_LEN[$pos]}+offset-$msg_len)) offset=0 done echo done format_msg_maxline=1 format_msg_maxpos=0 unset FORMAT_MSG FORMAT_MSG_LEN } |
Что тут происходит?
Мы пробегаемся по подготовленному массиву и печатаем через printf по нужному смещению. Отдельно рассмотрим printf:
printf "%$((slen+msg_len))b " "${FORMAT_MSG[$((line*100+pos))]}" |
Модификатор b — интерпретировать escape последовательности. Число перед ним — выравнивание справа. В коде мы учитываем, сколько символов уже напечатано и насколько должен быть сдвинут следующий столбец. Домашнее задание. Как так, мы ранее сказали, что интерпретирует их терминал, а тут находится какой-то модификатор b, который на это влияет. А при s — интерпретации не происходит. Почему?
Ну вот и всё!
Код библиотеки доступен на github.
Пример использования:
Создадим fastenv.sh следующего содержания:
#!/bin/bash source "$(dirname $(readlink -f $0))/column.sh" push_format_MSG Company="${COLOR_BLUE}Fastenv${COLOR_RESET}" Comment="${COLOR_GREEN}Is the best!${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Artem${COLOR_RESET}" Role="${COLOR_GREEN}Team Lead${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}LFS${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Ludmila${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Database,Oracle,Monitoring}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Ubuntu${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Dmitriy${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Mail,Virtualization}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Debian${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Dmitriy${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Security,Bash}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Archlinux${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Dmitriy${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Java,JVM}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Archlinux${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Konstantin${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Rabbitmq,Git}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Ubuntu${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Aleksandr${COLOR_RESET}" Role="${COLOR_WHITE}Linux Administrator/{Network,Haproxy}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}Mint${COLOR_RESET}" push_format_MSG Name="${COLOR_WHITE}Sergey${COLOR_RESET}" Role="${COLOR_WHITE}Unix Administrator/{*BSD,Database}${COLOR_RESET}" 'Favorite OS'="${COLOR_WHITE}FreeBSD${COLOR_RESET}" pull_format_MSG |