Документация COREmanager

Редактор конфигурационных файлов

В COREmanager встроен модуль для разбора и редактирования конфигурацонных файлов. В отличии от распространенных средств типа lex или bison, он ориентирован не только на поиск, но и на изменение данных. Данный модуль можно разделить на два больших компонента:

  • Tokenizer — служит для разбиения файлов на отдельные лексемы;
  • Cache — служит для сохранения данных и информации об их размещении внутри файла.

Tokenizer

Служит для разбиения файла на лексемы — последовательности символов, которые удобно обрабатывать вместе для разбора данного типа файлов. Необязательно использовать лексемы целиком. Вы можете объединять несколько лексем в одну последовательность или наоборот использовать лишь часть лексемы. Для того, чтобы определить, как разделить поток на лексемы необходимо задать набор Params и состоящий из отдельных правил Rule. Правила применяются в порядке обратным порядку их добавления в набор. То есть правила добавленные позднее могут определять более сложные лексемы, которые состоят из более простых. Например, у нас есть следующая последовательность:

Field "some complex value"

Предположим, что у нас есть следующие правила:

  • все последовательности символов разделенные переводом строки или другими лексемами считать за одну лексему (это правило существует всегда);
  • все последовательности пробелов, переводов строки и табуляции считать за лексему-разделитель;
  • все последовательности заключенные в двойные кавычки считать за одну лексему.

Согласно этим правилам получим следующие лексемы:

  • Field — последовательность символов, прерванная пробелом согласно правилу 1 (пробел — отдельная лексема согласно правилу 2);;
  • пробел — отдельная лексема согласно правилу 2;
  • "some complex value" — одна лексема согласно правилу 3. Если бы правила 3 не существовало, то вместо одной лексемы мы бы получили 5 отдельных лексем (три строки и два разделителя). Но так как правило 3 было добавлено позднее второго оно будет обработано раньше;
  • перевод строки — отдельная лексема согласно правилу 2;
  • пустая строка — виртуальная лексема существующая в конце каждой строки и в конце файла.

Одно правило может распознавать несколько различных лексем за один вызов. Правило 3 из предыдущего примера могло бы формировать не одну лексему, а три: лексему-разделитель (двойная кавычка), some complex value, ещё одна лексема-разделитель (двойная кавычка).

Типы лексем

Для удобства обработки лексемы разделены на несколько типов:

  • лексемы-разделители. Как очевидно из названия, служат для разделения последовательности символов на лексемы. Сами по себе они, как правило, никакой смысловой нагрузки не несут и не возвращаются при обычном вызове GetToken;
  • обычные лексемы — все остальные последовательности;
  • специальные лексемы — отдельный подвид обычных лексем. Обычный вызов GetToken, если ему попадается такая лексема, возвращает исключение. Это сделано для того, чтобы случайно не пропустить служебную лексему при разборе файла. Например, вы англизируете строку получая её лексемы вызовом GetToken. При этом вы точно знаете, что интересующая вас последовательность должна быть описана одной строкой. Вместо того, чтобы после каждого вызова GetToken проверять не встретили ли вы перевод строки, вы можете добавить правило которое будет распознавать перевод строки, как отдельную специальную лексему. И, если перевод строки встретится, когда вы этого не ожидаете (например из-за ошибки в файле) будет возвращено исключение, которое прервет разбор последовательности. А разбор файла продолжится со следующей строки.

GetToken

Основной метод для получения лексем. Он может быть вызван в двух режимах.

  • без параметров ---- пропускает все лексемы разделители до тех пор, пока не встретит обычную лексему. Если за этой лексемой следует лексема-разделитель, он прочитает и её. Пропущенные разделители можно получить вызовом метода skipped(), прочитанную лексему методом token(), а следующий разделитель методом delimiter(). Все эти лексемы будут извлечены из очереди. Если прочитанная лексема окажется специальной, будет выброшено исключение ESpec.
  • С параметром true — этот метод вернёт первую лексему из очереди вне зависимости от того, является ли эта лексема разделителем, специальной или обычной лексемой. Исключение возвращено не будет.

Cache

Этот модуль отвечает за сохранение как данных, прочитанных из файла, так и информации о размещении этих данных в файле. Все данные рассматриваются, как наборы однотипных записей RecordSet. Один файл может содержать неограниченное число таких наборов. Отдельные записи внутри набора могут, в свою очередь, включать вложенные наборы. Для описания набора вам необходимо написать два основных метода — это NewRecordTemplate и Parse. Первый служит для построения новой записи при её добавлении, второй — для распознавания записи и разбора ее полей и вложенных наборов.

NewRecordTemplate

Этот метод не имеет параметров и должен вернуть строку-шаблон для формирования новой записи. Все под строки вида _имя_ будут заменены в нём на значения полей записи с соответствующими именами. После формирования записи для полученной строки будет вызван метод Parse(), чтобы определить положение полей и избежать разночтений при добавлении записей и разборе файла. Данные в новой записи будут приведены в соответствие с теми, которые удастся распознать методу Parse().

Parse

Этот метод должен определить, находится ли сейчас Tokenizer на начале интересующей нас записи. Указать какое смещение в файле считать за начало записи, вызвав метод CacheStart(), какое смещение считать за конец записи вызовом метода CacheDone(). И вызовом метода CacheField() указать позиции всех полей записи. Если нет необходимости удалять поле из записи, то при вызове CacheField можно указать лишь значение поля и смещение значения в файле. Например, вызов CacheField(this, Name) сообщает о том, что последняя прочитанная лексема содержит значение поля Name.

Вы можете использовать часть последней лексемы как значение поля. Например: CacheField(this, Name, token().substr(0, 5)) — в качестве значения поля Name будут использованы первые 5 символов текущей лексемы. Если необходимо использовать лексему не с первого символа, то необходимо дополнительно указать смещение. Например: CacheField(this, Name, token().substr(3, 5), pos() + 3) — в качестве значения поля Name будут использованы 5 символов текущей лексемы начиная с четвертого символа.

Обратите внимание!
Значение, переданное в CacheField, должно точно соответствовать тому, что записано в файле по указанному смещению. Значения полей не могут перекрывать друг друга и не могут быть разорваны.

Добавление полей

Довольно часто возникает ситуация, когда запись имеет не фиксированное число полей, а их набор может меняться. В этом случае появляется необходимость в добавлении новых полей к существующей записи и их удалении. Для того, чтобы иметь возможность добавлять поля необходимо переопределить методы FieldPrefix и FieldSuffix или метод NewField. Последний позволяет очень гибко определять позицию для вставки нового поля но значительно сложнее в реализации. Реализация же методов FieldPrefix и FieldSuffix, как правило, тривиальна и проблем не вызывает.

По умолчанию, новые поля добавляются после последнего распознаного поля записи. Иногда возникает ситуация, когда такая логика не срабатывает. Например:

<Directory /home>
</Directory>

Единственное поле которое тут может быть распознано — это /home (имя каталога). И, по логике все новые поля должны быть добавлены сразу после него:

<Directory /home Access deny>
</Directory>

Очевидно, что это не то, что нам надо. Решить эту проблему можно вызовом SetInsertPos() в момент, когда мы прочитает лексему . Данный метод как бы добавляет еще одно распознаное поле нулевой длины в текущую позицию, и все вставки будут происходить после этой позиции (если распознаются какие-нибудь поля после вызова SetInsertPos() вставка будет происходить после этих полей).

Удаление полей

Особенность удаления полей заключается в том, что кроме самого значения поля, как правило, необходимо так же удалить его имя и некоторое количество пробелов до и после этого поля. Например:

<Directory /home>
 Index index.html
</Directory>

Если мы при разборе, встав на лексему index.html, мы выполним вызов CacheField(this, Index), мы сможем без проблем менять это значение, но при удалении получим следующее:

<Directory /home>
 Index
</Directory>

Скорее всего, это не то, что нужно. Для полного удаления поля Index, при разборе файла необходимо было указать не только позицию значения поля, но и позиции начала и конца записи Index index.html . Необходимо позвать CacheField с дополнительными параметрами:

GetToken(); // тут мы получили имя поля — лексему Index
GetUntilToken("\n", false); // получаем лексему состоящую из всех символов до символа перевода строки
CacheField(this, Index, token(), line_offset(), pos() — line_offset(), pos() + token().size() + delimiter().size() — line_offset());
// line_offset() — считаем, что описание поля начинается от начала строки
// pos() — line_offset() — это смещение данных относительно начала описания поля
// pos() + token().size() + delimiter().size() — line_offset() — это длинна описания поля. Таким выражением мы вычисляем длинну строки

Теперь при удалении — описание поля будет удалено целиком, и поля добавляемые после него будут добавлять после перевода строки, а не сразу за значением поля, как это происходило бы в предыдущем примере.