Обработка файлов
Этот раздел посвящён автоматной обработке текстовых файлов - по линиям, с простыми заменами шаблонов в виде регулярных выражений. На ум сразу приходят иероглифы на shell или итераторы на python, но почему бы не делать такие вещи на CL? Посмотрим что получится, и ещё - я тут не пытаюсь быть кратким :) на sed всё это будет гора*здо короче :))
Итерация по строкам
Итак, для работы с файлами в CL существует макрос with-open-file о котором подробнее можно почитать в PCL, в нашем случае у нас будет два файла - исходный файл и видоизменённый. Поэтому сразу делаем макрос "с-двумя-файлами-делай-то-то":
(defmacro with-two-files ((stream-1 stream-2 file-1 file-2 &key (if-second-exists :overwrite)
(if-second-not-exist :create))
&body body)
`(with-open-file (,stream-1 ,file-1)
(with-open-file (,stream-2 ,file-2 :direction :output
:if-exists ,if-second-exists
:if-does-not-exist ,if-second-not-exist)
,@body)))
Тут всё так же как и с with-open-file - мы называем два файлы, и объявляем потоки которыми будем пользоваться в теле макроса:
(with-two-files (s1 s2 #p"my-file" #p"my-new-file")
;; работаем с потоками s1 и s2 - пишем/читаем туда/сюда
)
Можно было бы сделать названия потоков анафорическими (контекстными) переменными, это бы сократило код, но потребовывало бы от нас помнить какие именно переменные неявно (без всякого объявления) появляются связанными в теле with-two-files:
(defmacro with-two-files ((file-1 file-2 &key (if-second-exists :overwrite)
(if-second-not-exist :create))
&body body)
`(with-open-file (^stream-1^ ,file-1)
(with-open-file (^stream-2^ ,file-2 :direction :output
:if-exists ,if-second-exists
:if-does-not-exist ,if-second-not-exist)
,@body)))
Теперь with-two-files требует только имена файлов и автоматически создает связь переменных ^stream-1^ и ^stream-2^ с потоками:
(with-two-files (#p"my-file" #p"my-new-file")
;; работаем с потоками ^stream-1^ и ^stream-2^
)
Такой получается интерфейс, макрос-класс. Никаких style-warning нет, но в данном случае такой способ не оправдан. Тогда смотрим дальше.
Теперь, когда у нас есть общий макрос для работы с двумя файлами, сделаем макрос следующего уровня - конвертации одного файла в другой. Первый вариант:
(defmacro convert-file (file-in file-out &key (filter #'(lambda (line)
line)))
`(with-two-files (stream-in stream-out ,file-in ,file-out)
(loop for line = (read-line stream-in nil)
while line do (format stream-out
"~a~%"
(funcall ,filter line))))))
Используем with-two-files и итерируем поток первого файла по линиям используя стандартный макрос loop; для каждой линии вызываем функцию фильтр, которая передаётся в конвертер в качестве аргумента. По-умолчанию функция фильтр - прямое отображение, - ничего не меняет, поэтому
(convert-file #p"my-file" #p"my-new-file")
работает как простое копирование.
Задачка 1 - перевод простых строк в другую фому
Попробуем вызвать convert-file с различными функциями фильтрации.
Пока неохота :)
Задачка 2 - удалить комментарий в начале файла
Первая версия convert-file совершенно ничего не знает о состоянии итератора в данный момент - в начале он, или в конце, были уже замены или нет. Теперь-то нам и пригодятся анафорические переменные в макросе, с помощью них функции или макросы использующие convert-file будут иметь доступ к ряду предикатов - своего рода способ задавать вопросы вышележащему макросу о состоянии итератора. Вот второй вариант:
(defmacro convert-file (file-in file-out &key (at-begining-p t)
(print-line-p t)
(filter #'(lambda (line)
line)))
"Транслятор файл -> файл. Функция filter транслирует текущую строку и имеет доступ
к параметрам трансляции."
`(with-two-files (stream-in stream-out ,file-in ,file-out)
(let ((^at-begining-p^ ,at-begining-p)
(^print-line-p^ ,print-line-p))
;; и т.д.
(loop for line = (read-line stream-in nil)
while line do (let ((filtered (funcall ,filter line)))
(if ^print-line-p^
(format stream-out "~a~%" filtered)))))))
И пример использования, функция delete-disclaimer удаляющая начальный комментарий в файле:
(defmacro delete-disclaimer (file-in file-out)
`(flet ((is-comment-p (string)
(if (< 0 (length string))
(string= (subseq string 0 1) ";"))))
(convert-file ,file-in ,file-out
;; Определяем начальное состояние КА!
:at-begining-p t
:print-line-p nil
:filter (lambda (line)
;; Функция КА смотрит вокруг:
(if ^at-begining-p^
(unless (is-comment-p line)
;; Функция КА меняет состояние транслятора:
(setq ^print-line-p^ t
^at-begining-p^ nil)
line)
line)))))