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

image

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

У цій частині програми я не тільки спростила шаблон але ще й спростила саму формулу запису операндів. Тепер ми вставляємо операнди не окремо, а одним рядком, одним блоком даних позначеним як &. Амперсанд, синонімом цього знака, що позначає весь рядок є \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 ця утиліта відсутня. Але це вже зовсім інша ненудна історія.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.