Pull to refresh

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

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

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

image

image

Не особо… Вложенность настроечных окон более 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<int &iStepDefNo>        (. ConfDevice dev; dev.iStepNo=iStepDefNo; .)
=                                                                                
       "DEVICE"        (. dev.iId = 0xFF; dev.iHopCnt=0xFF; .)
       [
             {
                    DEVNAME<dev>
                    | "ID" "=" "(" NUMBER<dev.iId> "," NUMBER<dev.iHopCnt> ")"
                    | "DI" "=" NUMBER<(int &)dev.uDidCar>
                    | DEVCAPS<dev>
                    | "UMASK" "="  NUMBER<(int &)dev.uMask>
             }
       ]

       [ 
             "{"             
                    {      (. ConfPort cport; .)        
                           ASSIGPort<cport>  (. dev.lstPorts.Add(cport); .)   
                           [","]
                    }
             "}"                                                                 
       ]
       [ ";" ]
                           (. m_pDevices->Add(dev); .)
.

// описание порта и маршрута
ASSIGPort<ConfPort &pPort>   (. int num=-1; .)
=                                                            
       "PORT" "="    NUMBER<pPort.nPort>
    {         
             "PW" "=" PW<pPort.Pw>
             | "LS" "=" LS<pPort.Ls>
             | "ID" "="
                       "{"
                                  NUMBER<num>   (. pPort.lstIds.Push(num); .)
                                  {
                                        ',' NUMBER<num>   (. pPort.lstIds.Push(num); .)
                                  }
                       "}"
    }
.

// ширина порта в физических линиях
PW<ePW &pw>                        (. int iTmp=0; .)
=      (             
             "X1"                 (. pw = PWX1; .)
             | "X2"               (. pw = PWX2; .)
             | "X4"               (. pw = PWX4; .)
             | "X1L0"             (. pw = PWX1L0; .)
             | "X1L1"             (. pw = PWX1L1; .)
             | "X1L2"             (. pw = PWX1L2; .)
             |NUMBER<iTmp>        (. pw=(ePW)iTmp; .)
       )
.

// ширина порта в линиях
LS<eLS &ls>                        (. int iTmp=0; .)
=      (             
             "G125"               (. ls = LSG125; .)
             | "G25"              (. ls = LSG25; .)
             | "G3125"            (. ls = LSG3125; .)
             | "G5"               (. ls = LSG5; .)
             | "G625"             (. ls = LSG625; .)
             |NUMBER<iTmp>        (. ls=(eLS)iTmp; .)
       )
.

// получение строки, заключённой в кавычки
STRING<CStr &str> =   string        (. str=CStr::FromPosLen(t->val, 1, strlen(t->val)-2); .)
.

// получение 10-ного или 16-ричного числа
NUMBER<int &value> = number             (.  value=Utils::ToInt32(t->val); .)
.

// название устройства (строка, применяется в devfs как имя)
DEVNAME<ConfDevice &dev>            (. CStr str; .)
=      "NAME" "="    STRING<str>   (. strcpy(dev.szName, (LPCSTR)str); .)
.

// возможности API устройства
DEVCAPS<ConfDevice &dev>            (. 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<iStepDefNo> { DEVICE<iStepDefNo> }
.

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, но только синтаксис мы сможем определять сами.

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

Аркадий Пчелинцев, архитектор проектов
Tags:
Hubs:
+2
Comments11

Articles