Раньше всё было лучше: компьютеры были большими, а буквы на экране — зелёными. Тогда они ещё назывались ЭВМ, а инженеры ходили по машинному залу в белых халатах. В те благословенные времена никто не заморачивался на тему 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, но только синтаксис мы сможем определять сами.

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

Аркадий Пчелинцев, архитектор проектов
Поделиться с друзьями
-->

Комментарии (11)


  1. staticlab
    10.05.2017 11:12
    +5

    А чем вам не угодил YAML?


    - ID: '(18:0)'
      DI: 0x03740038
      NAME: sw1102
      UMASK: 0666
      CAPS: [ MN ]
      PORTS:
        - { PORT: 7, PW: X1, LS: G25, ID: [ 0 ] }
        - { PORT: 9, PW: X4, LS: G3125, ID: [ 8 ] }
        - { PORT: 10, PW: X4, LS: G25, ID: [ 16 ] }
    
    - ID: '(0:1)'
      DI: 0x04000003
      NAME: host
      UMASK: 0444,
      CAPS: [ MN, MB0, DB, DIO ]
    
    - ID: '(8:1)'
      NAME: meter
      UMASK: 0444,
      CAPS: [ MN,MB0,MB1,MB2,MB3,DB,DIO ]
    
    - ID: '(16:1)'
      DI: 0x03780038
      NAME: sw1101
      UMASK: 0444,
      CAPS: [ MN ]
      PORTS:
        - { PORT: 12, PW: X1, LS: G25, ID: [ 0, 8, 18 ] }


    1. Alex_ME
      10.05.2017 11:34
      +3

      Или JSON. YAML чувствителен к лишним пробелам.


      1. staticlab
        10.05.2017 12:20

        Да, это недостаток, но в нём в данном случае чуть меньше "мусорного" синтаксиса по сравнению с JSON и есть возможность оставлять комментарии (но для последнего есть JSON5).


        1. Alex_ME
          10.05.2017 12:23

          Да, по поводу мусора соглашусь с Вами.


          Приведенный выше YAML и JSON в сравнении


          Картинка

          image


    1. technont64
      10.05.2017 12:29
      +1

      или TOML :)


  1. master65
    10.05.2017 11:17

    Философия конфигурационных файлов


  1. atd
    10.05.2017 13:41
    +2

    я не настоящий программист, но на ваш ц++ смотреть без боли просто нельзя (


  1. opaopa
    10.05.2017 14:58

    Жаль, что в статье не упомянут критически важный в больших системах плюс текстовых конфигов: легко строится протоколирование и анализ истории и причин изменений.

    diff же для GUI… так и не придуман. То, что я видел в 1C… это забавно.


  1. Amomum
    10.05.2017 15:58
    +3

    Сделать хороший текстовый конфиг — тоже не так-то просто, на самом деле.
    Помнится, пришлось мне пользоваться одной САПР, которая настройки в файле хранила…

    • Настроек было пару тысяч, изначально в файле присутствовала пара сотен.
    • Настройки назывались типа «allow_harn_mfg_assy_retrieval » или «style_grid_spacing». Разумеется, среди них были ну очень похожие по названию, но совсем разные по действию.
    • То, что в файле уже было, не было отсортировано по алфавиту или еще как-то сгруппировано, просто все подряд навалено.
    • Некоторые опции конфликтовали друг с другом.
    • Разумеется, существовали огромные талмуды, где перечислялись все возможные опции, только надо было тщательно смотреть на соответствие версии талмуда и версии САПРа, потому что старые опции переставали работать, зато появлялись новые.


    Причем я так и вижу, что давным-давно этих опций было штук 10 и кто-то подумал — «Да лааадно, и так вроде все понятно».

    Так и не удалось мне рамку по ГОСТу сделать.


  1. zabbius
    10.05.2017 20:46
    +1

    Имхо чем городить очередной кастомный синтаксис, проще взять что-нибудь стандартное (yaml, json, ini наконец).
    А для тех, кто ну очень хочет тыкать мышкой, можно добавить GUI редактор конфига.


  1. MacIn
    11.05.2017 15:55

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