[Из песочницы] Нескучный однострочный калькулятор на sed
Sed, по мнению некоторых адептов, непредсказуемый, с мутным, ветхозаветным синтаксисом язык, с правками на полях собственноручно сделанными еще Кириллом и Мефодием. Я всегда уважаю мнения оппонентов, но совершенно не обязана их разделять, поэтому для себя решила, что sed это то что нужно, что бы по быстрому набить мозоли и не быть белой вороной на обсуждениях, когда в строчках кода sed появляются загадочные символы отличные от литералов регулярных выражений.
Какой имею в наличие инструментарий? Первое это bash (версия 4.3.42). Второе это sed (4.2.2). Судя по версиям, если немного пофантазировать, то можно предположить, что наши скакуны стартовали примерно в одинаковую эпоху и идут хоть и не ноздря в ноздрю, но с разницей не более в половину корпуса. Все это добро расположилось в операционке fedora 24 на моем компьютере.
Освежив в памяти и пробежав по вершкам учебник от emulek я обнаружила, что в sed имеется модификатор который позволяет встраивать команды шелла и заменять шаблон на значение возвращаемое этой командой. Моя первая строчка на этом поприще выглядела довольно обнадеживающе.
sed -r 's/([0-9]+)([-+])([0-9]+)/expr \1 \2 \3/e' <<<2+2
Все получилось и на выходе меня ждала твердая четверка! Команда »s» (substitution) ищет совпадение по шаблону /([0–9]+)([-+])([0–9]+)/ и подменяет его выводом команды /expr ([0–9]+) ([-+]) ([0–9]+)/e. Параметры в этой команде определены в шаблоне. Первый блок в круглых скобках соответствует \1 второй \2 и третий \3. Пойдем дальше и расширим функционал добавив арифметические знаки »*%/».
sed -r 's/([0-9]+)([-+*%/])([0-9]+)/expr \1 \2 \3/e' <<<2*2
И сразу получаем на выходе синтаксическую ошибку в команде »expr». Начинаем анализировать и понимаем, что в выражении \2 при подстановке звездочка имеет специальное значение и sed подменяет ее значение своим, то есть пустотой. Придется экранировать данный символ до того как башевская команда примет его в оборот. Пишем еще одну команду подмены и несколько изменяем шаблон.
sed -r 's/\*/\\\*/; s/([0-9]+)([-+*%/\]+)([0-9]+)/expr \1 \2 \3/e' <<<2*2
Опять четверка! Шедевр так и просится на картину «опять двойка»! Здесь мы ввели еще одну команду подмены. Предворяем в первом шаблоне звездочку значком обратного слеша, что бы лишить ее злодейку супер способностей, а в подменяемой строчке не забываем экранировать сам значок обратного слеша. В итоге эта часть разрастается до вот такого страшного вида /\\\*/.
Так же во втором шаблоне, а именно во вторых круглых скобочках добавлен обратный слеш и что бы не городить огород к определенному квадратными скобочками классу символов [-+*%/\] подставим квантификатор »+», что дает нам совпадение по шаблону на строку из двух символов »\*». Можно было конечно более точно определить шаблон, но по контексту этого вполне достаточно.
Баш хорош тем что практически любую доступную в нем вещь можно сделать несколькими разными способами. Кстати в этом его и кажущаяся запутанность. Для выполнения арифметических действий с целыми числами предусмотрена более современная форма записи и не в обиду бородатым админам я, как ровесница поколения пепси, вооружусь альтернативной конструкцией »echo $(())», а »expr» оставлю только в строчках для примера. Новая конструкция позволяет нам избавиться от части кода и основательно упростить программку. Отпадает необходимость в экранировании звездочки. Так как код мы упростим то можно ввести и дополнительный функционал, поддерживая уровень сложности на приемлемом для новичка уровне.
Я отсылала в редактор только одну строчку и на этом работа программы прекращалась. В sed предусмотрена возможность условных и безусловных переходов. Те кто знаком с ассемблером сразу обнаружат идеальное сходство переходов или прыжков по меткам. Принцип тот же. И это уже становится интересно потому, что работа в редакторе начинает походить на работу
с настоящей программой.
Есть устоявшееся мнение что html это язык разметки, я с этим согласна. Так вот думаю без переходов и некоторых встроенных функций язык редактора sed тоже можно было бы подвести под это определение. Но переходы и функции в редакторе есть, а значит мы полноценно можем заниматься программированием его работы с текстом.
Следующий релиз нашей программы с вводом в обращение переходов, приобретает очертания настоящего кода.
sed -r ':again s/([0-9]+)([-+%/*])([0-9]+)/echo $((\1 \2 \3))/e; t again'
Запустив в терминале такой код нам остается только набирать арифметические выражения с двумя числами и жать на »enter». По меткам »again» сразу видно начало и конец цикла. Команда условного перехода »t» по метке »again» вернет нас в начало очередного цикла определенного этой меткой после двоеточия и редактор будет ждать ввода следующей строки.
Имейте ввиду, выход из программы это хорошо всем известный сигнал прерывания запускаемый по нажатию комбинаций клавиш Ctrl+C.
Но давайте разберем работу условного перехода. Команда »t» применяется совместно с командой »s» (substitution) и по результату последней осуществляется или не осуществляется переход. Если первая (при наличие нескольких) команда »s» производит подмену в буфере то условный переход выполняется.
Имеется так же команда »T» которая выполняется если первая (при наличие нескольких) команда »s» закончилась не удачей.
Мы разобрали работу программы с переходом по условию. Стоп скажете вы. А зачем нам здесь переход по условию, когда вполне достаточно будет безусловного перехода. Давайте заменим команду »t» на команду безусловного перехода »b». Тестируем.
sed -r ':again s/([0-9]+)([-+%/*])([0-9]+)/echo $((\1 \2 \3))/e; b again'
Вводим как и положено данные, а на выходе нет ни какого результата! Где же мы ошиблись, ведь по логике все должно работать точно также. Вернемся и снова проанализируем работу программы. Как всегда все оказалось элементарно. Мы не учли один момент, команда условного перехода »t» срабатывает и переводит выполнение программы на метку в том случае если происходит подмена в команде »s».
По всей видимости конструкция с расширением »e» работает несколько иначе. Как я осмелюсь предположить в нашем случае нет ни какой подмены, Наша строка полностью соответствует шаблону и появляется в неизменном виде, в виде параметров утилиты баша. А вот здесь по всей видимости и происходит таинство подмены, но увы наш редактор полагает, что команда »s» к этому уже не имеет отношения, а причастна команда-расширение »e». А так как мы знаем, что если метка отсутствует или не выполняется условие перехода то выполнение программы перейдет в конец командной строки, а не по метке в ее начало. Ремонтируем код.
sed -rn ':again s/([0-9]+)([-+%/*])([0-9]+)/echo $((\1 \2 \3))/e;p;d; b again'
Ситуация требует объяснений. Вводим дополнительно еще две команды и одну опцию которые исправляют ситуацию. После записи результата вычисления командой »p» выведем принудительно на печать содержимое буфера, а перед возвратом к началу программы очистим его командой »d». Опция »-n» обычно работает в паре с командой »p» и подавляет автоматический вывод буфера на печать. Чувствую, что повзрослела после таких злоключений на несколько лет и если так пойдет дальше то быстро состарюсь и останусь старой девой. Даже не представляла, что будет на столько не скучно!
Наша программа снова работает, но она все же во многом избыточна. Например метки которые я ввела большей степенью для демонстрации возможностей языка sed и которые неожиданно добавили детективного перца, здесь лишние. Они имели бы смысл если в строке у нас присутствовало несколько команд разделенных точкой с запятой и метка позволяла бы обойти одну из команд или блоков команд. Придется откатить с таким трудом освоенные навороты. На самом деле редактор и так ожидает ввода очередной строки и начинает работу по ее вводу с самого начала где у нас и располагалась метка. Без какого либо ущерба я смогу переписать строчку так:
sed -r 's/([0-9]+)([-+%/*])([0-9]+)/echo $((\1 \2 \3))/e' -
Или даже так:
sed -r 's/([0-9]+)([-+%/*])([0-9]+)/echo $((\1 \2 \3))/e'
Этот главный цикл, что мы смоделировали ранее в ручном режиме уже встроен в программу редактора и мы этим в дальнейшем станем пользоваться. Давайте вернемся к функционалу. Если вспомнить работу настоящего калькулятора то там обнаружим дополнительный буфер для хранения промежуточных результатов. А как же дела обстоят в sed? Оказывается в сед тоже есть дополнительный буфер и несколько команд по работе с ним. Все что мы делали до этого, это работали с главным буфером в который загружается строка и производятся действия с ней. Задействуем дополнительный буфер для хранения результата вычислений, а так же добавим функционал, когда последующие операции с промежуточным результатом вычисления можно было бы проводить просто набрав в строке знак арифметического действия и второй операнд. А так же предусмотрим работу с отрицательными числами. Так же не станем урезать функционал самого баша и добавим немного энтропии в алгоритм работы «Не скучного» калькулятора, встроим возведения числа в степень. Напомню что в баше возведение в степень выглядит так ЧИСЛО**СТЕПЕНЬ. Знак понятен, двойная звездочка принята к сведению. Заодно сразу же проведем всю оптимизацию доступную моему пониманию.
Хоть я поначалу и тыкалась носом как слепой котенок ища развития для кода, в заключение он все же приобрел следующий вид.
sed -r '/^[-+/%*]\*?-?[0-9]+$/{x;G;s/\n//}; s/.*/echo $((&))/e;h'
Я посчитала излишней расточительностью определять полноценный шаблон, который по сути ни чем не занимается кроме как предоставляет с помощью такой конструкции возможность ввести команду оболочки и сократила шаблон до минимума /.*/, совпадающий по сути с любой строкой. Я посчитала это приемлемым и даже представила, что застрахована на миллион баксов от ошибок при вводе. Если вы в себе не так уверены как я то можете вставить вот такой шаблон s/^-?[0–9]+[-+%/*]\*?-?[0–9]+$/. Всем остальным, похожим на меня блондинкам я советую не заморачиваться, потому, что даже при ошибке ввода дополнительный буфер обновляется с потерей промежуточных результатов и называется подобный цикл очень просто — «начинай сначала». Перезапускать при этом программу совершенно не обязательно, достаточно начать вводить правильные данные.
Ну и давайте разберем что мы на ваяли в последней строчке кода и начнем с алгоритма работы программы. После запуска редактора он находится в режиме ожидания ввода. Первая вводимая
нами строчка состоит из двух операндов и арифметического знака в середине. В самом начале кода программы стоит шаблон который «не замечает» все операции состоящие из двух операндов, а значит нашу первую строчку этот фильтр пропустит и действие перейдет сразу на уже знакомую команду вызывающую утилиту оболочки для получения окончательного результата.
В этой части программы я не только упростила шаблон, но еще и упростила саму формулу записи операндов. Теперь мы вставляем операнды не по отдельности, а одной строкой, одним блоком данных обозначенным как &. Амперсанд, синонимом этого знака обозначающего всю строку целиком является \0. В этом блоке кода заканчивающегося точкой с запятой мы меняем значения главного буфера на вычисленное значение командой оболочки. После точки с запятой у нас стоит команда »h» которая копирует все, что находится в главном буфере в дополнительный буфер. После чего программа выводит на экран содержимое главного буфера и переходит в начало цикла с ожиданием ввода новой строки.
Теперь мы знаем что у нас есть в буфере первый операнд и вводим только знак арифметического действия и второй операнд. Первая команда »s» обнаруживает, что в главном буфере имеется строка совпадающая по шаблону /^[-+/%*]\*?-?[0–9]+$/ после чего действие передается блоку команд заключенных в фигурные скобки. Вторая команда в блоке — »G», добавляет в конец главного буфера знак переноса строки »\n» и после него копирует строку из дополнительного буфера. В итоге мы имеем сразу две строки в главном буфере разделенных символом переноса строки. Первая — это только, что введенные знак операции и второй операнд.
Сразу обращает на себя внимание не правильный порядок расположения операндов. Что бы исправить это небольшое недоразумение перед добавлением строки из дополнительного буфера в главный, мы применим колено в виде команды »x», которая поменяет местами главный буфер с дополнительным и тогда после выполнения команды »G» все станет в правильном порядке. В итоге после выполнения двух команд »x; G» мы будем иметь подобную строчку 1операнд\nЗНАК2операнд в главном буфере. Перевод строки в середине выражения у нас оказывается лишним. Удалим его следующей командой подмены s/\n//. Ну, а дальше по написанному, управление переходит к «счетной машине».
Те кто раньше был не знаком с потоковым редактором sed смогут самостоятельно полистать учебник от emulek и посмотреть как же в действительности называются буфера в sed, ну и смогут обнаружить еще кучу полезностей.
На десерт всем домоSEDам сообщу, существует в природе еще такая утилита Super-sed. В репозитории debian-testing имеет название пакета ssed. Это потоковый редактор способный понимать перловские регулярные выражения. В fedora 24 в репозитории rpmfusion эта утилита отсутствует. Но это уже совсем другая нескучная история.