Хранение пользовательских типов данных. Часть 3.2. ini-файлы.
Продолжаем разговор, начатый здесь и продолженный в части 3.1.
Хранение в файлах списков, которые потом читаются и (возможно) преобразовываются, было рассмотрено в части 3.1. При таком методе хранения под каждый тип данных необходимо создавать свой файл.
А если данные можно по какому-либо признаку "объединять"? Ну, например, те же самые системные переменные: можно хранить значения "по умолчанию" внутри кода, а можно во внешнем хранилище.
Хранение "внутри кода" потребует перекомпиляции при любом чихе. Хранение во внешнем хранилище свободно от этой проблемы.
Допустим, что нам надо в каком-то внешнем хранилище одновременно хранить значения системных переменных "по умолчанию", а также значения системных переменных, установленных пользователем. Ограничимся сначала этим, а потом посмотрим, что можно добавить еще.
ini-файлы имеют следующую структуру:
1 2 3 4 5 6 7 8 9 | [Группа1] <Ключ>=<Значение> <Ключ>=<Значение> ; Комментарий. <Ключ>=<Значение> [Группа2] <Ключ>=<Значение> <Ключ>=<Значение> <Ключ>=<Значение> |
Имена групп всегда выделяются квадратными скобками. Комментарии тоже достаточно легко определяются (начинаются с символа ";"). Пустые строки в файле могут присутствовать, это не должно мешать работе кода. Дополнительно выдвинем требование: строковые значения ключей должны быть заключены в двойные кавычки.
Теоретически требований к уникальности имен групп или ключей внутри них нет, но для вящего спокойствия примем, что имена групп не повторяются, а имена ключей уникальны внутри одной группы. Опять же, для собственного удобства примем, что регистр не играет роли, то есть слова "Группа", "ГРУППА" и "гРуПпА" будут означать одно и то же.
Сейчас уже не будем рассматривать вариант создания ini-файла - предположим, что "внешнее хранилище" располагается где-то на сервере и не изменяется (например, \\server\autocad.adds\datas). Поэтому надо сделать функции чтения и записи данных в ini файл. Операции понадобятся как для отдельной группы и ключа, так и для полного файла. Поехали?
Сначала сделаем тестовый файл c:\test.ini:
1 2 3 4 5 6 | [group1st] key1=1 key2=3.14 [group2nd] key1="qwert" key21=pi |
Теперь сделаем функцию, которая "прочитает" весь ini-файл, объединив данные по группам:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | (defun _lispru-ini-datas-read1 (file / handle str group res) ;| * Чтение данных из ini-файла. * Параметры вызова: file полное имя файла, включая расширение. |; (if (findfile file) (progn (setq handle (open file "r")) (while (setq str (read-line handle)) (setq str (vl-string-trim "\t " str)) (cond ((or (< (strlen str) 2) (wcmatch str ";*"))) ((wcmatch str "<code>[*") (setq group (strcase (vl-string-trim "[]" str) t) res (append res (list (list group))) ) ;_ end of setq ) (group (setq res (subst (cons group (vl-remove 'nil (append (cdr (assoc group res)) (list (cons (strcase (substr str 1 (vl-string-search "=" str)) t ) ;_ end of strcase (substr str (+ 2 (vl-string-search "=" str)) ) ;_ end of substr ) ;_ end of cons ) ;_ end of list ) ;_ end of append ) ;_ end of vl-remove ) ;_ end of cons (assoc group res) res ) ;_ end of subst ) ;_ end of setq ) ) ;_ end of cond ) ;_ end of while (close handle) ) ;_ end of progn ) ;_ end of if (vl-remove-if-not 'cdr res) ) ;_ end of defun |
И "натравим" эту функцию на наш файл:
1 2 | _$ (setq lst (_lispru-ini-datas-read1 "c:\\test.ini")) (("group1st" ("key1" . "1") ("key2" . "3.14")) ("group2nd" ("key1" . ""qwert"") ("key21" . "pi"))) |
Ну, или преобразовывая результат в более понятный вид:
1 2 3 4 5 6 7 8 9 | '(("group1st" ("key1" . "1") ("key2" . "3.14") ) ("group2nd" ("key1" . ""qwert"") ("key21" . "pi") ) ) |
Как видно, значения независимо ни от чего представлены как строки. Изменим немного код, добавив read:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | (defun _lispru-ini-datas-read (file / handle str group res) ;| * Чтение данных из ini-файла. * Параметры вызова: file полное имя файла, включая расширение. |; (if (findfile file) (progn (setq handle (open file "r")) (while (setq str (read-line handle)) (setq str (vl-string-trim "\t " str)) (cond ((or (< (strlen str) 2) (wcmatch str ";*"))) ((wcmatch str "</code>[*") (setq group (strcase (vl-string-trim "[]" str) t) res (append res (list (list group))) ) ;_ end of setq ) (group (setq res (subst (cons group (vl-remove 'nil (append (cdr (assoc group res)) (list (cons (strcase (substr str 1 (vl-string-search "=" str)) t ) ;_ end of strcase (read (substr str (+ 2 (vl-string-search "=" str)) ) ;_ end of substr ) ;_ end of read ) ;_ end of cons ) ;_ end of list ) ;_ end of append ) ;_ end of vl-remove ) ;_ end of cons (assoc group res) res ) ;_ end of subst ) ;_ end of setq ) ) ;_ end of cond ) ;_ end of while (close handle) ) ;_ end of progn ) ;_ end of if (vl-remove-if-not 'cdr res) ) ;_ end of defun |
Теперь результат уже таков, что с ним можно работать Пожалуй, этот код и оставим. Забрать его можно здесь.
Осталось сделать функцию чтения значения определенного ключа из указанной группы. Тут совсем все просто: читаем весь файл, потом через assoc выбираем группу и оттуда уже через assoc забираем значение ключа.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | (defun _lispru-ini-value-read (file group key) ;| * Выполняет чтение указанного ключа секции ini-файла * Параметры вызова: file имя файла group имя секция key имя ключа |; (cdr (assoc (strcase key t) (cdr (assoc (strcase group t) (_lispru-ini-datas-read file) ) ;_ end of assoc ) ;_ end of cdr ) ;_ end of assoc ) ;_ end of cdr ) ;_ end of defun |
Код лежит здесь.
Проверим работу:
1 2 3 4 5 6 7 8 | _$ (_lispru-ini-value-read "c:\\test.ini" "group1st" "key1") 1 _$ (_lispru-ini-value-read "c:\\test.ini" "group1" "key1") nil _$ (_lispru-ini-value-read "c:\\test.ini" "group1st" "key11324") nil _$ (_lispru-ini-value-read "c:\\test1.ini" "group1st" "key11324") nil |
С чтением разобрались? Почти.
Проблема в том, что assoc воспринимает строки регистрозависимо. То есть если сделать
1 | (setq lst '(("key1" . 1) ("Key2" . 2))) |
и потом попытаться выполнить (assoc lst "KEY1"), то получим nil. То есть регистр надо учитывать. Или сделать так, чтобы регистр уже не играл такой критичной роли.
Сделаем "замену" стандартному assoc. Работать оно, конечно, будет медленнее, но зато безошибочно
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | (defun _lispru-list-assoc (key lst) ;| * Замена стандартному assoc * Параметры вызова: key ключ lst обрабатываеымй список |; (if (= (type key) 'str) (setq key (strcase key)) ) ;_ end of if (car (vl-remove-if-not (function (lambda (a / b) (and (setq b (car a)) (or (and (= (type b) 'str) (= (strcase b) key)) (equal b key) ) ;_ end of or ) ;_ end of and ) ;_ end of lambda ) ;_ end of function lst ) ;_ end of vl-remove-if-not ) ;_ end of car ) ;_ end of defun |
Ну и до кучи - assoc с поддержкой масок
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | (defun _lispru-list-assoc-with-mask (key lst) ;| * Замена стандартному assoc * Параметры вызова: key ключ (возможно, строка. Допускается применение масок). lst обрабатываеымй список |; (if (= (type key) 'str) (setq key (strcase key)) ) ;_ end of if (car (vl-remove-if-not (function (lambda (a / b) (and (setq b (car a)) (or (and (= (type b) 'str) (wcmatch (strcase b) key)) (equal b key) ) ;_ end of or ) ;_ end of and ) ;_ end of lambda ) ;_ end of function lst ) ;_ end of vl-remove-if-not ) ;_ end of car ) ;_ end of defun |
Эти функции можно (да, наверное, и нужно) будет использовать при работе с ini-файлами. А, возможно, и не только с ними
Теперь надо повоевать с записью данных.
При записи надо будет:
а) создавать файл, если он не существует;
б) создавать группу, если она не существует;
в) создавать ключ, если его нет.
г) если ключ есть, для него надо будет менять значение. В таком случае придется полностью читать файл, менять данные и снова файл записывать.
Именно тот факт, что нельзя выполнить запись части файла, и ограничивает применение этого метода: файловые операции одни из самых медленных.
Ну да ладно, приступим. Как говорится, глаза боятся, руки лезут
Сначала определим, что обязательно в качестве параметров передается имя файла и уже заранее структурированные данные:
1 2 3 | (defun _lispru-ini-datas-write (file datas / handle exist) ;<...> );_end of defun |
Договоримся, что имя файла всегда будет передаваться с каталогом. Каталоги мы уже умеем создавать: _lispru-dir-create.lsp специально для этого и создавался.
Прежде всего сделаем запись уже структурированных данных в ini-файл. Памятуя о том, что (повторение - мать учения):
- Передаем полное имя файла, включая каталог.
- Каталог может и не существовать. Значит, его надо создавать
- ini-файл может и не существовать. Значит, его надо тоже создавать
- В ini-файле может не быть нужной группы.
- Точно так же группа может быть, а вот ключ - нет
- Все есть, но и у ключа есть некое значение. Надо заменить.
Есть еще одно: в datas в качестве значений ключей может оказаться не строка. Поэтому сделаем дополнительную функцию преобразования значения в строку. Точнее, не сделаем, а возьмем готовый комплекс, который, в свою очередь, использует некоторые дополнительные функции. Единственное, что сделаем - переименуем, чтобы из общего потока не выбиваться.*
Ну вот, теперь и приступить можно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | (defun _lispru-ini-datas-write (file datas / handle exist) ;| * Запись списка в ini-файл * Параметры вызова: * file имя файла. Если файла не существует, то он создается * datas список вида ((group (key . value) (key . value)) <...>) * Примеры вызова: (_lispru-ini-datas-write "c:\\temp.ini" '(("gr1" ("Key1" . "Val1") ("Key2" . "Val2")) ("gr2" ("key1" . "Val1") ("Key2" . "Val2")))) |; ;; Сначала создаем каталог (_lispru-dir-create (vl-filename-directory file)) ;; Теперь проверяем наличие файла (if (not (findfile file)) (progn (setq handle (open file "w")) (close handle) ) ;_ end of progn ) ;_ end of if ;; Сначала преобразовываем новые данные (setq datas (mapcar (function (lambda (x) (cons (strcase (car x) t) (mapcar (function (lambda (y) (cons (strcase (car y) t) (cdr y)) ) ;_ end of lambda ) ;_ end of function (cdr x) ) ;_ end of mapcar ) ;_ end of cons ) ;_ end of lambda ) ;_ end of function datas ) ;_ end of mapcar ;; Теперь читаем имеющиеся данные exist (_lispru-ini-datas-read file) ;; И выполняем объединение с новыми datas ((lambda (/ gr) (foreach item datas (if (setq gr (cdr (assoc (car item) exist))) (progn (foreach key (cdr item) (if (cdr (assoc (car key) gr)) (setq gr (subst key (assoc (car key) gr) gr ) ;_ end of subst ) ;_ end of setq (setq gr (append gr (list key))) ) ;_ end of if ) ;_ end of foreach (setq exist (subst (cons (car item) gr) (assoc (car item) exist) exist ) ;_ end of subst ) ;_ end of setq ) ;_ end of progn (setq exist (append exist (list item))) ) ;_ end of if ) ;_ end of foreach exist ) ;_ end of lambda ) handle (open file "w") ) ;_ end of setq (foreach item datas (write-line (strcat "[" (_lispru-conv-value-to-string (car item)) "]") handle ) ;_ end of write-line (foreach val (cdr item) (write-line (strcat (vl-string-trim " " (_lispru-conv-value-to-string (car val)) ) ;_ end of vl-string-trim "=" (vl-string-trim " " (_lispru-conv-value-to-string (cdr val)) ) ;_ end of vl-string-trim ) ;_ end of strcat handle ) ;_ end of write-line ) ;_ end of foreach ) ;_ end of foreach (close handle) (princ) ) ;_ end of defun |
Ффух, вроде ничего не забыл (ну, кроме собственно файлов с исходниками).
---
* Как только будет решены некоторые технические трудности с предоставлением файлов, приложу все упоминавшиеся функции и дам на них ссылки.
---
Упоминавшиеся функции
Имя функции | Ссылка на загрузку |
_lispru-ini-datas-read | _lispru-ini-datas-read.lsp |
_lispru-ini-value-read | _lispru-ini-value-read.lsp |
_lispru-list-assoc | _lispru-list-assoc.lsp |
_lispru-list-assoc-with-mask | _lispru-list-assoc-with-mask.lsp |
_lispru-ini-datas-write | _lispru-ini-datas-write |
_lispru-dir-create | _lispru-dir-create.lsp |
Служебные функции | lispru-string-to-list lispru-string-right-part.lsp lispru-string-replace.lsp lispru-string-left-part.lsp lispru-string-align-to-right lispru-string-align-to-left.lsp lispru-string-some-part.lsp lispru-string-align.lsp |