Регистрация | Войти
Lisp — программируемый язык программирования

Обработка файлов

Этот раздел посвящён автоматной обработке текстовых файлов - по строчкам, с простыми заменами на основе шаблонов заданных в виде регулярных выражений. На ум сразу приходят иероглифы на shell или итераторы на python, но почему бы не делать такие вещи на CL? Посмотрим что получится, и ещё - я тут не пытаюсь быть кратким, на sed всё это будет гораздо короче :)

Итерация по строкам

Итак, для работы с файлами в CL существует макрос with-open-file о котором подробнее можно почитать в PCL, в нашем случае у нас будет два файла - исходный файл и генерируемый. Поэтому сразу делаем макрос "с-двумя-файлами-делай-то-то":

(defmacro with-two-files ((input input-file &optional keys-for-input)
                          (output output-file
                           &optional (keys-for-output '(:direction :output :if-exists :overwrite :if-does-not-exist :create))
)

                          &body body
)

  `(with-open-file (,input ,input-file ,@keys-for-input)
     (with-open-file (,output ,output-file ,@keys-for-output)
       ,@body
)
)
)

Тут всё так же как и с with-open-file - мы называем два файлы, и объявляем потоки которыми будем пользоваться в теле макроса:

(with-two-files (i #p"my-file") (o #p"my-new-file")
  ;; работаем с потоками i и o - читаем из одного, пишем в другой
)

Теперь, когда у нас есть общий макрос для работы с двумя файлами, можно написать функцию конвертации одного файла в другой:

(defun convert-file (input-file output-file &key (filter #'(lambda (line) line)))
  (with-two-files (input input-file) (output output-file)
    (loop for line = (read-line input nil)
          while line do (write-line (funcall filter line) output)
)
)
)

Тут мы используем макрос with-two-files и итерируем поток первого файла по линиям используя стандартный макрос loop; для каждой линии вызываем функцию фильтр, которая передаётся в конвертер в качестве аргумента. По-умолчанию функция фильтр - прямое отображение, она ничего не меняет, поэтому

(convert-file #p"my-file" #p"my-new-file")

работает как простое копирование.

Задачка 1 - перевод объявлений #define из .h файлов

Попробуем применить convert-file для трансляции .h файлов с простыми объявлениями #define в .lisp файлы (простейший groveling). Для этого нужно всего лишь отдать convert-file подходящую функцию filter, которая преобразует соответствующий шаблон - её проще всего сделать на основе регулярных выражений.

(asdf:oos 'asdf:load-op :cl-ppcre)
(use-package :cl-ppcre)

(defun h->lisp (input-file output-file)
  (flet ((convert-templates (line)
           (let ((define
                  (register-groups-bind
                      (name value description)
                      ("^#define\\s+(\\S+)\\s+(\\S+)\\s*(?:/\\*\\s*(.*?)\\s*\\*/)?" line)
                    (format nil "(defconstant +~A+ ~A \"~A\")~&" name value (or description name))
)
)
)

             (if define
                 define
                 (let ((constant
                        (register-groups-bind
                            (name value description)
                            ("^\\s*(\\S+)\\s*=\\s*(\\S+?(?:,)?)\\s*(?:/\\*\\s*(.*?)\\s*\\*/)?" line)
                          (format nil "(defconstant +~A+ ~A \"~A\")~&" name value (or description name))
)
)
)

                   (if constant
                       constant
                       (format nil ";;; ~A~&" line)
)
)
)
)
)
)

    (convert-file input-file output-file :filter #'convert-templates)
)
)

Мы определили три фильтра для трёх шаблонов строк - #define-cтрок, строк с константами и неопределённых строк. Теперь

(h->lisp #p"foo.h" #p"foo.lisp")

Сконвертирует файл foo.h:

#define A 1
C = 3
foo ();

В файл foo.lisp:

(defconstant +A+ 1 "A")
(defconstant +C+ 3 "C")
;;; foo ();

Задачка 2 - примешивание лисп-форм в какой-либо текст

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

(defun text+lisp->text (input-file output-file)
  (flet ((convert (line)
           (let ((converted
                  (register-groups-bind
                      (pre sexpr post)
                      ("^(.*)##lisp##(.+)##(.*)?" line)
                    (format nil "~A~A~A~&" pre (eval (read-from-string sexpr)) post)
)
)
)

             (if converted
                 converted
                 line
)
)
)
)

    (convert-file input-file output-file :filter #'convert)
)
)

После выполнения

(text+lisp->text #p"text" #p"text+")

Из текста text вида

Define ##lisp##(defvar *a* 2)##
Define ##lisp##(defvar *b* 3)##
Result - ##lisp##(+ *a* *b*)##

Будет сгенерирован текст text+:

Define *A*
Define *B*
Result - 5

Задачка 3 - удаление комментариев в начале файла

Первая версия convert-file совершенно ничего не знает о состоянии итератора в данный момент - в начале он, или в конце, были уже замены или нет. Тут удобно сделать макро-аналог для функции convert-file с анафорическими переменные в нём, с помощью них функции или макросы использующие macro-convert-file будут иметь доступ к ряду предикатов - своего рода способ задавать вопросы вышележащему макросу о состоянии итератора.

(defmacro macro-convert-file (input-file output-file &key (at-begining-p t)
                                                           (write-line-p t)
                                                           (filter #'(lambda (line) line))
)

  `(with-two-files (input ,input-file) (output ,output-file)
     (let ((^at-begining-p^ ,at-begining-p)
           (^write-line-p^  ,write-line-p)
)

           ;; etc.
      (loop for line = (read-line input nil)
                 while line do (let ((filtered (funcall ,filter line)))
                                 (if ^write-line-p^
                                     (write-line filtered output)
)
)
)
)
)
)

И пример использования - функция delete-disclaimer удаляющая начальный комментарий в файле:

(defun delete-disclaimer (input-file output-file &key (is-comment-p #'(lambda (string)
                                                                        (if (< 0 (length string))
                                                                            (string= (subseq string 0 1) ";")
)
)
)
)

  (macro-convert-file input-file
                      output-file
                      ;; Определяем начальное состояние КА
                     :at-begining-p t
                      :write-line-p  nil
                      :filter (lambda (line)
                                ;; Функция КА смотрит вокруг:
                               (if ^at-begining-p^
                                    (unless (funcall is-comment-p line)
                                      ;; Функция КА меняет состояние транслятора:
                                     (setq ^write-line-p^ t
                                            ^at-begining-p^ nil
)

                                      line
)

                                  line
)
)
)
)

@2009-2013 lisper.ru