+7 (812) 494-9090
Обратная связьEnglish
Главная → Статьи → Системное ПО → Философия, или когда буквы были зелеными
Версия для печати

Философия, или когда буквы были зелеными

31 мая 2017

Аркадий Пчелинцев, архитектор проектов, АстроСофтАркадий Пчелинцев, архитектор проектов в АстроСофт, рассказал о разборе конфигурационных файлов и любого формального синтаксиса, а также о построении парсера конфигурационного файла. Как известно, от построения парсера конфигурационного файла до языка программирования всего лишь один шаг. Можно написать интерпретатор, а можно и вполне полноценный компилятор, если добавить генерацию кода. Обо всем этом речь идет в статье ниже, а также статья размещена на Хабре.






Раньше всё было лучше: компьютеры были большими, а буквы на экране — зелёными. Тогда они ещё назывались ЭВМ, а инженеры ходили по машинному залу в белых халатах. В те благословенные времена никто не заморачивался на тему user friendly-интерфейсов, а просто требовали от пользователя подготовить колоду перфокарт в соответствии с определённым форматом. Подготовил неверно — сам виноват. Это кажется не очень удобным и вовсе не «интуитивно понятным», но данный спартанский подход позволял обсчитывать весьма серьёзные задачи вроде моделирования ядерных реакций или расчёт полётного задания для космических ракет. И всё это при ресурсах, на два-три порядка меньших, чем почти любой современный смартфон.

Шло время, и перфокарты с магнитными барабанами канули в лету, в угоду пользователю стали доминировать программы с развитым GUI. Это стало подаваться как технический прогресс и забота об удобстве пользователе. А всегда ли это хорошо? А всегда ли это удобнее обычного текстового конфигурационного файла? Что-нибудь такое удобно воспринимать?



system-configure.jpg


programmable-auto-response.jpg



Не особо… Вложенность настроечных окон более 2-х уровней, в каждом из которых десятка по два-три «пумпочек» — это удобно? А если вы — администратор, и вам надо растиражировать это чудо инженерной мысли на две сотни рабочих мест со всеми настройками? Ох… как же хорошо было раньше, когда просто можно было положить рядом конфигурационный файл.

Действительно, тестовый конфигурационный файл обладает массой достоинств, и он нисколько не устарел:

  • не нужно писать весьма трудоёмкое GUI для настройки ПО;
  • легко тиражируется вместе с ПО;
  • легко создавать программно, если он велик;
  • если конфигурационный файл использует принцип необязательных параметров, его можно сделать более читаемым, чем множество форм с десятками элементов управления.

У конфигурационного файла есть лишь одна серьёзная проблема: если делать синтаксис понятным и читаемым, то парсер представляет собой довольно трудоёмкое изделие. Отдельные индивиды могут возразить, что на свете есть XML, для которого всё давно готово, но я бы не назвал XML хорошо читаемым и удобным форматом. Поборникам этого подхода я бы пожелал поработать с большими таблицами XML в полевых условиях, имея под рукой лишь редактор vi. Уверен, что это отрезвит сторонников XML.

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

Из всего многообразия своей простотой и доступностью отличается генератор COCO/R, созданный в университете города Линц, что в Верхней Австрии. Я выделил его исходя из следующих соображений:

  • на нём можно создавать парсеры для C, C++, C#, F#, Java, Ada, всех видов Pascal, Modula, Ruby и нескольких других языков;
  • лексемы и действия с ними описываются в одном файле, который легко читать;
  • его можно собрать и использовать на любой платформе;
  • его можно интегрировать в Visual Studio (мелочь, а приятно).

Не буду вдаваться в особенности использования COCO/R, т. к. об этом прекрасно написано в его документации, а сразу перейду к практическому примеру.

Мы создаём парсер для построения таблицы маршрутизации абстрактной сети в составе нескольких маршрутизаторов и нескольких оконечных устройств. Общий вид конфигурационного файла удобно представить следующим образом (при этом мы никак не ограничены количеством маршрутизаторов, количеством портов на них и количеством оконечных устройств):

 
	// конфигурация при подключении к 1102 DEVICE ID=(18,0) DI=0x03740038 NAME="sw1102" UMASK=0666 CAPS={MN} { PORT = 7 PW=X1 LS=G25 ID={ 0 }, // хост PORT = 9 PW=X4 LS=G3125 ID={ 8 }, // измеритель PORT = 10 PW=X4 LS=G25 ID={ 16 } // второй блок }; DEVICE ID=(0,1) DI=0x04000003 NAME="host" UMASK=0444 CAPS={ MN,MB0, DB,DIO }; DEVICE ID=(8,1) NAME="meter" UMASK=0444 CAPS={ MN,MB0,MB1,MB2,MB3,DB,DIO }; DEVICE ID=(16,1) DI=0x03780038 NAME="sw1101" UMASK=0444 CAPS={ MN } { PORT = 12 PW=X1 LS=G25 ID={ 0, 8, 18 } // хост, измеритель, первый блок }; 

Теперь строим описатель синтаксиса для парсера (строим парсер для C++):
 
	// #include <необходимые_файлы> COMPILER SRIO_CONFIG // используемый набор символов CHARACTERS letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz". digit = "0123456789". cr = '\r'. lf = '\n'. tab = '\t'. stringch = ANY - '"' - '\\' - cr - lf - tab. hexDigit = "0123456789abcdefABCDEF". printable = '\u0020' .. '\u007e'. TOKENS ident = letter {"_"} { letter | "_" | digit }. number = digit { digit } | "0x" hexDigit {hexDigit}. string = '"' { stringch | "\"\"" } '"' . COMMENTS FROM "/*" TO "*/" NESTED COMMENTS FROM "//" TO lf IGNORE cr + lf + tab PRODUCTIONS // описание устройства в конфигурационном файле DEVICE (. ConfDevice dev; dev.iStepNo=iStepDefNo; .) = "DEVICE" (. dev.iId = 0xFF; dev.iHopCnt=0xFF; .) [ { DEVNAME
	| "ID" "=" "(" NUMBER "," NUMBER ")" | "DI" "=" NUMBER<(int &)dev.uDidCar> | DEVCAPS
	| "UMASK" "=" NUMBER<(int &)dev.uMask> } ] [ "{" { (. ConfPort cport; .) ASSIGPort (. dev.lstPorts.Add(cport); .) [","] } "}" ] [ ";" ] (. m_pDevices->Add(dev); .) . // описание порта и маршрута ASSIGPort (. int num=-1; .) = "PORT" "=" NUMBER
	{ "PW" "=" PW
	| "LS" "=" LS
	| "ID" "=" "{" NUMBER (. pPort.lstIds.Push(num); .) { ',' NUMBER (. pPort.lstIds.Push(num); .) } "}" } . // ширина порта в физических линиях PW (. int iTmp=0; .) = ( "X1" (. pw = PWX1; .) | "X2" (. pw = PWX2; .) | "X4" (. pw = PWX4; .) | "X1L0" (. pw = PWX1L0; .) | "X1L1" (. pw = PWX1L1; .) | "X1L2" (. pw = PWX1L2; .) |NUMBER (. pw=(ePW)iTmp; .) ) . // ширина порта в линиях LS (. int iTmp=0; .) = ( "G125" (. ls = LSG125; .) | "G25" (. ls = LSG25; .) | "G3125" (. ls = LSG3125; .) | "G5" (. ls = LSG5; .) | "G625" (. ls = LSG625; .) |NUMBER (. ls=(eLS)iTmp; .) ) . // получение строки, заключённой в кавычки STRING = string (. str=CStr::FromPosLen(t->val, 1, strlen(t->val)-2); .) . // получение 10-ного или 16-ричного числа NUMBER = number (. value=Utils::ToInt32(t->val); .) . // название устройства (строка, применяется в devfs как имя) DEVNAME (. CStr str; .) = "NAME" "=" STRING (. strcpy(dev.szName, (LPCSTR)str); .) . // возможности API устройства DEVCAPS (. dev.uCaps = 0; .) = "CAPS" "=" "{" { [','] ( "MN" (. dev.uCaps |= MN; .) |"MB0" (. dev.uCaps |= MB0; .) |"MB1" (. dev.uCaps |= MB1; .) |"MB2" (. dev.uCaps |= MB2; .) |"MB3" (. dev.uCaps |= MB3; .) |"DB" (. dev.uCaps |= DB; .) |"DIO" (. dev.uCaps |= DIO; .) |"ST" (. dev.uCaps |= ST; .) ) } "}" . ////////////////////////////////////////////////////////////// // конфигурационный файл SRIO_CONFIG = DEVICE { DEVICE } . END SRIO_CONFIG 


Получается весьма кратко и изящно. Теперь достаточно пропустить этот описатель через генератор парсеров. В данном случае использовался генератор, модифицированный мной много лет назад:

 
	D:\WRL>cocor cocor –namespace cfg sdrv_conf.atg Coco/R (Nov 17, 2010), changed by APRCPV checking sdrv_conf.atg(1): warning LL1: in DEVICE: contents of [...] or {...} must not be deletable sdrv_conf.atg(1): warning LL1: in BLK_STEPDEF: "DEVICE" is start & successor of deletable structure parser + scanner generated 0 errors detected D:\WRL > 


Результат работы генератора — файлы Parser.cpp Scanner.cpp Parser.h Scanner.h, которые мы немедленно включим в проект и можем сразу использовать:
 
	bool ReadConfigFile() { bool bSuc=false; // проверяем наличие конфигурационного файла if (access(m_szConfFileName, 0)!=0) { TRACE(eLL_CRIT, "Error: Configuration file '%s' not found or unaccessable!\n", m_szConfFileName); abort(); } cfg::Scanner &s = *new cfg::Scanner(m_szConfFileName); // создаём сканнер cfg::Parser &p = *new cfg::Parser(&s); // создаём парсер p.m_pConfigData = this; // объект конфигурации в памяти программы p.Parse(); // выполняем разбор конфигурационного файла // проверяем на наличие синтаксических ошибок в конфигурации // если нужно, то можно вывести информацию в формате: error(row:col) bSuc=(p.errors->count < 1); delete &p; delete &s; // возвращаем признак успеха разбора конфигурационного файла return bSuc; } 


Дело сделано, теперь моя программа способна воспринимать конфигурационный файл в том формате, в котором его удобно писать. Более того, всегда можно изменить синтаксис и добавить в него новые возможности.

С использованием данной технологии можно не только разбирать конфигурационные файлы, а вообще разбирать любой формальный синтаксис, что бывает весьма полезно. Например, можно создать специализированный редактор с подсветкой синтаксиса не хуже, чем в Eclipse или Visual Studio, но только синтаксис мы сможем определять сами.

А теперь немного пофантазируем… Наверняка многим не давали покоя лавры Николаса Вирта или Кернигана и Риччи: ну почему они смогли разработать новый язык программирования, а я не могу?! И вправду, а почему? Ведь от построения парсера конфигурационного файла до языка программирования всего лишь один шаг. Можно написать интерпретатор, а можно и вполне полноценный компилятор, если добавить генерацию кода, которую мы уже рассмотрели в одной из статей.



Теги: c++, .net, GUI