Нескучный однострочный калькулятор на sed

в 8:09, , рубрики: linux sed bash, метки:

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

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]+$/. Всем остальным, похожим на меня блондинкам я советую не заморачиваться, потому, что даже при ошибке ввода дополнительный буфер обновляется с потерей промежуточных результатов и называется подобный цикл очень просто — «начинай сначала». Перезапускать при этом программу совершенно не обязательно, достаточно начать вводить правильные данные.

image

Ну и давайте разберем что мы на ваяли в последней строчке кода и начнем с алгоритма работы программы. После запуска редактора он находится в режиме ожидания ввода. Первая вводимая
нами строчка состоит из двух операндов и арифметического знака в середине. В самом начале кода программы стоит шаблон который «не замечает» все операции состоящие из двух операндов, а значит нашу первую строчку этот фильтр пропустит и действие перейдет сразу на уже знакомую команду вызывающую утилиту оболочки для получения окончательного результата.

В этой части программы я не только упростила шаблон но еще и упростила саму формулу записи операндов. Теперь мы вставляем операнды не по отдельности, а одной строкой, одним блоком данных обозначенным как &. Амперсанд, синонимом этого знака обозначающего всю строку целиком является . В этом блоке кода заканчивающегося точкой с запятой мы меняем значения главного буфера на вычисленное значение командой оболочки. После точки с запятой у нас стоит команда "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 эта утилита отсутствует. Но это уже совсем другая нескучная история.

Автор: ne_zabudka

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js