Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

14. Файлы и файловый ввод/вывод

Common Lisp предоставляет мощную библиотеку для работы с файлами. В этой главе я остановлюсь на нескольких элементарных задачах, относящихся к файловыми операциям: чтение, запись, а также отображение структуры файловой системы. Средства Common Lisp, предназначенные для ввода/вывода, аналогичны имеющимся в других языках программирования. Common Lisp предоставляет потоковую абстракцию операций чтения/записи, а для манипулирования файловыми объектами в независимом от операционной системы формате - абстракцию файловых путей. Кроме того, Common Lisp предоставляет некоторое количество уникальных возможностей, такие как чтение и запись s-выражений.

Чтение данных из файлов

Самая фундаментальная задача ввода/вывода - чтение содержимого файла. Для того, чтобы получить поток, из которого вы можете прочитать содержимое файла, используется функция OPEN. По умолчанию, OPEN возвращает посимвольный поток ввода данных, который можно передать множеству функций, считывающих один или несколько символов текста: READ-CHAR считывает одиночный символ; READ-LINE считывает строку текста, возвращая ее как строку без символа конца строки; функция READ считывает одиночное s-выражение, возвращая объект Lisp. Когда работа с потоком завершена, вы можете закрыть его с помощью функции CLOSE.

Функция OPEN требует имя файла как единственный обязательный аргумент. Как можно увидеть в секции "Имена файлов", Common Lisp предоставляет два пути для представления имени файла, но наиболее простой способ - использовать строку, содержащую имя в формате, используемом в файловой системе. Так, предполагая, что /some/file/name.txt это файл, возможно открыть его следующим образом:

(open "/some/file/name.txt")

Вы можете использовать объект, возвращаемый функцией, как первый аргумент любой функции, осуществляющей чтение. Например, для того, чтобы напечатать первую строку файла, вы можете комбинировать OPEN, READ-LINE, CLOSE следующим образом:

(let ((in (open "/some/file/name.txt")))
  (format t "~a~%" (read-line in))
  (close in)
)

Конечно, при открытии и чтении данных может произойти ряд ошибок. Файл может не существовать, или вы можете непредвиденно достигнуть конца файла в процессе его чтения. По умолчанию, OPEN и READ-* будут сигнализировать об ошибках в данных ситуациях. В главе 19, я рассмотрю как обработать эти ошибки. Сейчас же, однако, будем использовать более легковесное решение: каждая из этих функций принимает аргументы, которые изменяют ее реакцию на исключительные ситуации.

Если вы хотите открыть файл, который возможно не существует, без генерирования ошибки функцией OPEN, вы можете использовать аргумент :if-does-not-exists для того, чтобы указать другое поведение. Три различных значения допустимы для данного аргумента - :error, по умолчанию; :create, что указывает на необходимость создания файла и повторное его открытие как существующего и NIL, что означает возврат NIL (при неуспешном открытии) вместо потока. Итак, возможно изменить предыдущий пример таким образом, чтобы обработать несуществующий файл.

(let ((in (open "/some/file/name.txt" :if-does-not-exist nil)))
  (when in
    (format t "~a~%" (read-line in))
    (close in)
)
)

Все функции чтения - READ-CHAR, READ-LINE, READ - принимают опциональный аргумент, по умолчанию "истина", который указывает должны ли они сигнализировать об ошибке, если они достигли конца файла. Если этот аргумент установлен в NIL, то они возвращают значение их 3го аргумента, который по умолчанию NIL, вместо ошибки. Таким образом, вывести на печать все строки файла можно следующим способом:

(let ((in (open "/some/file/name.txt" :if-does-not-exist nil)))
  (when in
    (loop for line = (read-line in nil)
         while line do (format t "~a~%" line)
)

    (close in)
)
)

Среди трёх функций чтения, READ – уникальна. Это – та самая функция, которая представляет букву "R" в "REPL", и которая используется для того, чтобы читать исходный код Lisp. Во время вызова она читает одиночное s-выражение, пропуская пробельные символы и комментарии, и возвращает объект Lisp, представляемый s-выражением. Например, предположим, что файл /some/file/name.txt содержит следующие строки:

(1 2 3)
456
"строка" ; это комментарий
((a b)
 (c d)
)

Другими словами, он содержит 4 s-выражения: список чисел, число, строку, и список списков. Вы можете считать эти выражения следующим образом:

CL-USER> (defparameter *s* (open "/some/file/name.txt"))
*S*
CL-USER> (read *s*)
(1 2 3)
CL-USER> (read *s*)
456
CL-USER> (read *s*)
"строка"
CL-USER> (read *s*)
((A B) (C D))
CL-USER> (close *s*)
T

Как было показано в Главе 3, возможно использовать PRINT для того, чтобы выводить объекты Lisp на печать в удобочитаемой форме. Итак, когда необходимо хранить данные в файлах, PRINT и READ предоставляют простой способ делать это без создания специального формата данных и парсера для их прочтения. Вы даже можете использовать комментарии без ограничений. И, поскольку s-выражения создавались для того, чтобы быть редактируемыми людьми, то они так же хорошо подходят для использования в качестве формата конфигурационных файлов 1).

Чтение двоичных данных

По умолчанию OPEN возвращает символьные потоки, которые преобразуют байты в символы в соответствии с конкретной схемой кодирования символов 2).

Для чтения потока байтов необходимо передать функции OPEN ключевой параметр :element-type со значением '(unsigned-byte 8) 3) Полученный поток можно передать функции READ-BYTE, которая будет возвращать целое число от 0 до 255 во время каждого вызова. READ-BYTE, так же, как и функции, работающие с потоками символов, принимает опциональные аргументы, которые указывают, должна ли она сигнализировать об ошибке, если достигнут конец файла, и какое значение возвращать в противном случае. В главе 24 мы построим библиотеку, которая позволит удобно читать структурированные бинарные данные, используя READ-BYTE. 4)

Блочное чтение

Последняя функция для чтения, READ-SEQUENCE, работает с бинарными и символьными потоками. Ей передается последовательность (обычно вектор) и поток, и она пытается заполнить эту последовательность данными из потока. Функция возвращает индекс первого элемента последовательности, который не был заполнен, либо ее длину, если она была заполнена полностью. Так же возможно передать ключевые аргументы :start и :end, которые указывают на подпоследовательность, которая должна быть заполнена вместо последовательности. Аргумент, определяющий последовательность должен быть типом, который может хранить элементы, из которых состоит поток. Поскольку большинство операционных систем поддерживают только одну какую-либо форму блочных операций ввода/вывода, READ-SEQUENCE скорее всего более эффективна чем чтение последовательных данных несколькими вызовами READ-BYTE или READ-CHAR.

Файловый вывод

Для записи данных в файл необходим поток вывода, который можно получить вызовом функции OPEN с ключевым аргументом :direction :output. Когда файл открывается для записи, OPEN предполагает, что файл не должен существовать, и будет сообщать об ошибке в противном случае. Однако, возможно изменить это поведение с помощью ключевого аргумента :if-exists. Передавая значение :supersede можно вызвать замену существующего файла. Значение :append позволяет осуществлять запись таким образом, что новые данные будут помещены в конец файла, а значение :overwrite возвращает поток, который будет переписывать существующие данные с начала файла. Если же передать NIL, то OPEN вернет NIL вместо потока, если файл уже существует. Характерное использование OPEN для вывода данных выглядит следующим образом:

(open "/some/file/name.txt" :direction :output :if-exists :supersede)

Common Lisp также предоставляет некоторые функции для записи данных: WRITE-CHAR пишет одиночный символ в поток. WRITE-LINE пишет строку, за которой следует символ конца строки, с учетом реализации для конкретной платформы. Другая функция, WRITE-STRING пишет строку, не добавляя символ конца строки. Две разные функции могут использоваться для того чтобы вывести символ конца строки: TERPRI - сокращение для "TERminate PRInt" (закончить печать) безусловно печатает символ конца строки, а FRESH-LINE печатает символ конца строки только в том случае, если текущая позиция печати не совпадает с началом строки. FRESH-LINE удобна в том случае, когда желательно избежать паразитных пустых строк в текстовом выводе, генерируемом другими последовательно вызываемыми функциями. Допустим, например, что есть одна функция, которая генерирует вывод и после которой обязательно должен идти перенос строки и другая, которая должна начинаться с новой строки. Но, предположим, что если функции вызываются последовательно, то необходимо обеспечить отсутствие лишних пустых строк в их выводе. Если в начале второй функции используется FRESH-LINE, ее вывод будет постоянно начинать с новой строки, но если она вызывается непосредственно после первой функции, то не будет выводиться лишний перевод строки.

Некоторые функции позволяют вывести данные Lisp в форме s-выражений: PRINT печатает s-выражение, предваряя его символом начала строки, и пробельным символом после. PRIN1 печатает только s-выражение. А функция PPRINT печатает s-выражения аналогично PRINT и PRIN1, но использует "красивую печать", которая пытается печатать s-выражения в эстетически красивом виде.

Однако, не все объекты могут быть напечатаны в том формате, который понимает READ. Переменная *PRINT-READABLY* контролирует поведение при попытке напечатать подобный объект с помощью PRINT, PRIN1 или PPRINT. Когда она равна NIL, эти функции напечатают объект в таком формате, что READ при попытке чтения гарантировано сообщит об ошибке; в ином случае они просигнализируют об ошибке вместо того, чтобы напечатать объект.

Еще одна функция, PRINC, также печатает объекты Лиспа, но в виде, удобном для человеческого восприятия. Например, PRINC печатает строки без кавычек. Текстовый вывод может быть еще более замысловатым, если задействовать потрясающе гибкую, и в некоторой степени загадочную функцию FORMAT. В 18 главе я расскажу о некоторых важных тонкостях этой функции, которая, по сути, определяет мини-язык для форматированного вывода.

Для того, чтобы записать двоичные данные в файл, следует открыть файл функцией OPEN с тем же самым аргументом :element-type, который использовался при чтении данных: '(unsigned-byte 8). После этого можно записывать в поток отдельные байты функцией WRITE-BYTE.

Функция блочного вывода WRITE-SEQUENCE принимает как двоичные, так и символьные потоки до тех пор, пока элементы последовательности имеют подходящий тип: символы или байты. Так же как и READ-SEQUENCE, эта функция наверняка более эффективна, чем запись элементов последовательности поодиночке.

Закрытие файлов

Любой, кто писал программы, взаимодействующие с файлами, знает, что важно закрывать файлы, когда работа с ними закончена, так как дескрипторы норовят быть дефицитным ресурсом. Если открывают файлы и забывают их закрывать, вскоре обнаруживают, что больше нельзя открыть ни одного файла5). На первый взгляд может показаться, что достаточно каждый вызов OPEN сопоставить с вызовом CLOSE. Например, можно всегда обрамлять код, использующий файл, как показано ниже:

(let ((stream (open "/some/file/name.txt"))) 
  ;; работа с потоком
 (close stream)
)

Однако этом метод имеет две проблемы. Первая — он предрасположен к ошибкам: если забыть написать CLOSE, то будет происходить утечка дескрипторов при каждом вызове этого кода. Вторая – наиболее значительная – нет гарантии, что CLOSE будет достигнут. Например, если в коде, расположенном до CLOSE, есть RETURN или RETURN-FROM, возвращение из LET произойдет без закрытия потока. Или, как вы увидите в 19 главе, если какая-либо часть кода до CLOSE сигнализирует об ошибке, управление может перейти за пределы LET обработчику ошибки и никогда не вернется, чтобы закрыть поток.

Common Lisp предоставляет общее решение того, как удостовериться, что определенный код всегда исполняется: специальный оператор UNWIND-PROTECT, о котором я расскажу в 20 главе. Так как открытие файла, работа с ним и последующее закрытие очень часто употребляются, Common Lisp предлагает макрос, WITH-OPEN-FILE, основанный на UNWIND-PROTECT, для скрытия этих действий. Ниже — основная форма:

(with-open-file (stream-var open-argument*) 
  body-form*
)

Выражения в body-form* вычисляются с stream-var, связанной с файловым потоком, открытым вызовом OPEN с аргументами open-argument*. WITH-OPEN-FILE удостоверяется, что поток stream-var закрывается до того, как из WITH-OPEN-FILE вернется управление. Поэтому читать файл можно следующим образом:

(with-open-file (stream "/some/file/name.txt") 
  (format t "~a~%" (read-line stream))
)

Создать файл можно так:

(with-open-file (stream "/some/file/name.txt" :direction :output) 
  (format stream "Какой-то текст.")
)

Как правило, WITH-OPEN-FILE используется в 90-99 процентах файлового ввода/вывода. Вызовы OPEN и CLOSE понадобятся, если файл нужно открыть в какой-либо функции и оставить поток открытым при возврате из нее. В таком случае вы должны позаботиться о закрытии потока самостоятельно, иначе произойдет утечка файловых дескрипторов и, в конце концов, вы больше не сможете открыть ни одного файла.

Имена файлов

До сих пор мы использовали строки для представления имен файлов. Однако, использование строк как имен файлов привязывает код к конкретной операционной и файловой системам. Точно так же, если конструировать имена в соответствии правилам конкретной схемы именования (скажем, разделение директорий знаком "/"), то вы также привязаны к одной определенной файловой системе.

Для того, чтобы избежать подобной непереносимости программ, Common Lisp предоставляет другое представление файловых имен: объекты файловых путей. Файловые пути представляют собой файловые имена в структурированном виде, что делает их использование легким без привязки к определенному синтаксису файловых имен. А бремя по переводу между строками в локальном представлении – строками имен – и файловыми путями ложится на плечи реализаций Lisp.

К несчастью, как и со многими абстракциями, спроектированными для скрытия деталей различных базовых систем, абстракция файловых путей привносит свои трудности. В то время, когда разрабатывались файловые пути, разнообразие широко используемых файловых систем было чуть значительнее, чем сегодня. Но это мало проясняет ситуацию, если все, о чем вы заботитесь, – представление имен файлов в Unix или Windows. Однако однажды поняв какие части этой абстракции можно забыть, как артефакты истории развития файловых путей, вы сможете ловко управлять именами файлов6).

Как правило, когда возникает необходимость в файловом имени, вы можете использовать как строку имени (namestring), так и файловый путь. Выбор зависит от того, откуда произошло имя. Файловые имена, предоставленные пользователем – например, как аргументы или строки конфигурационного файла – как правило, будут строками имен, так как пользователь знает какая операционная система у него запущена, поэтому не следует ожидать, что он будет беспокоиться о представлении файловых имен в Lisp. Но следуя общепринятой практике, файловые имена будут представлены файловыми путями, так как они переносимы. Поток, который возвращает OPEN, также представляет файловое имя, а именно, файловое имя, которое было изначально использовано для открытия этого потока. Вместе эти три типа упоминаются как указатели файловых путей. Все встроенные функции, ожидающие файловое имя как аргумент, принимают все три типа указателя файловых путей. Например, во всех предыдущих случаях, когда вы использовали строку для представления файлового имени, вы также могли передавать в функцию объект файлового пути или поток.

Как мы до этого докатились

Историческое разнообразие файловых систем, существующих в период 70-80 годов, можно легко забыть. Кент Питман, один из ведущих технических редакторов стандарта Common Lisp, описал однажды ситуацию в comp.lang.lisp (Message-ID: sfwzo74np6w.fsf@world.std.com) так:

В момент завершения проектирования Common Lisp господствующими файловыми системами были TOPS-10, TENEX, TOPS-20, VAX VMS, AT&T Unix, MIT Multics, MIT ITS, и это не упоминаю группу систем для мэйнфрэймов. В некоторых системах имена файлов были только в верхнем регистре, в других – смешанные, в третьих – чувствительны к регистру, но с возможностью преобразования (как в CL). Какие-то имели групповые символы (wildcards), какие-то – нет. Одни имели :вверх (:up) в относительных файловых путях, другие – нет. Также существовали файловые системы без каталогов, файловые системы без иерархической структуры каталогов, файловые системы без типов файлов, файловые системы без версий, файловые системы без устройств и т.д.

Если сейчас посмотреть на абстракцию файловых путей с точки зрения какой-нибудь определенной файловой системы, она выглядит нелепо. Но если взять в рассмотрение даже такие две похожие файловые системы, как в Windows и Unix, то вы можете заметить отличия, от которых можно отвлечься с помощью системы файловых путей. Файловые имена в Windows содержат букву диска в то время, как в Unix нет. Другое преимущество владения абстракцией файловых путей, которая спроектирована, чтобы оперировать большим разнообразием файловых систем, которые существовали в прошлом, – ее вероятная способность управлять файловыми системами, которые будут существовать в будущем. Если, скажем, файловые системы с сохранением всех старых данных и истории операций войдут снова в моду, Common Lisp будет к этому готов.

Как имена путей представляют имена файлов

Файловый путь – это структурированный объект, который представляет файловое имя, используя шесть компонентов: хост, устройство, каталог, имя, тип и версия. Большинство из них принимают атомарные значения, как правило, строки; только директория – структурный компонент, содержащий список имен каталогов (как строки) с предшествующим ключевым словом: :absolute (абсолютный) или :relative (относительный). Но не все компоненты необходимы на все платформах – это одна из тех вещей, которая вселяет страх в начинающих лисперов, потому что необоснованно сложна. С другой стороны, вам не надо заботиться о том, какие компоненты могут или нет использоваться для представления имен на определенной файловой системе, если только вам не надо создать объект файлового пути с нуля, а это почти никогда и не надо. Взамен Вы обычно получите объект файлового пути либо позволив реализации преобразовать строку имени специфичной для какой-то файловой системы в объект файлового пути, либо создав файловый путь, который перенимает большинство компонент от какого-либо существующего.

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

На файловых системах Unix, обычно, используются компоненты: директория, имя и тип. В Windows на один компонент больше – обычно устройство или хост – содержит букву диска. На этих платформах строку имени делят на части, а разделителем служит косая черта в Unix и косая или обратная косая черты в Windows. Букву диска в Windows размещают либо в компонент устройства или компонент хост. Все, кроме последнего из оставшихся элементов имени, размещаются в списке, начинающемся с :absolute или :relative в зависимости от того, начинается ли имя с разделителя или нет (игнорирую букву диска, если таковая присутствует). Список становится компонентом каталог файлового пути. Последний элемент делится по самой крайней точке, если она есть, и полученные две части есть компоненты имя и тип, соответственно.7)

Можно проверить каждый компонент файлового пути с функциями PATHNAME-DIRECTORY, PATHNAME-NAME и PATHNAME-TYPE.

(pathname-directory (pathname "/foo/bar/baz.txt"))(:ABSOLUTE "foo" "bar") 
(pathname-name (pathname "/foo/bar/baz.txt"))      → "baz"
(pathname-type (pathname "/foo/bar/baz.txt"))      → "txt"

Другие три функции – PATHNAME-HOST, PATHNAME-DEVICE и PATHNAME-VERSION – позволяют получить остальные три составляющие файлового пути, хотя они и не представляют интереса в Unix. В Windows либо PATHNAME-HOST, либо PATHNAME-DEVICE возвратит букву диска.

Подобно другим встроенным объектам, файловые пути обладают своим синтаксисом для чтения: #p, за которым следует строка, заключенная в двойные кавычки. Это позволяет печатать и считывать s-выражения, содержащие объекты файлового пути, но так как синтаксис зависит от алгоритма анализа строки, эти данные могут быть непереносимыми между разными операционными системами.

(pathname "/foo/bar/baz.txt") → #p"/foo/bar/baz.txt" 

Для того, чтобы файловый путь преобразовать обратно в строку имени – например, чтобы представить его пользователю – следует воспользоваться функцией NAMESTRING, которая принимает указатель файлового пути и возвращает строку имени. Две других функции – DIRECTORY-NAMESTRING и FILE-NAMESTRING – возвращают часть строки имени. DIRECTORY-NAMESTRING соединяет элементы компонента каталог в локальное имя каталога. FILE-NAMESTRING – компоненты имя и тип.8)

(namestring #p"/foo/bar/baz.txt")           → "/foo/bar/baz.txt" 
(directory-namestring #p"/foo/bar/baz.txt") → "/foo/bar/"
(file-namestring #p"/foo/bar/baz.txt")      → "baz.txt"

Конструирование имен путей

Вы можете создать файловый путь, используя функцию MAKE-PATHNAME. Она принимает по одному аргументу-ключу на каждую компоненту файлового пути и возвращает файловый путь, заполненный всеми предоставленными компонентами.9)

(make-pathname
  :directory '(:absolute "foo" "bar")
  :name "baz"
  :type "txt"
)
→  #p"/foo/bar/baz.txt"

Однако, если вы желаете, чтобы ваши программы были переносимыми, то вряд ли вы пожелаете создавать файловые пути с нуля, даже если абстракция файловых путей предохраняет вас от непереносимого синтаксиса файловых имен, ведь файловые имена могут быть непереносимыми еще множеством способов. Например, файловое имя "/home/peter/foo.txt" не очень-то подходит для OS X, в которой /home/ представлено /Users/.

Другой причиной, по которой не следует создавать файловые пути с нуля, является тот факт, что различные реализации используют компоненты файлового пути с небольшими различиями. Например, как было упомянуто выше, некоторые Windows-реализации LISP хранят букву диска в компоненте устройство в то время, как другие – в компоненте хост. Если вы напишите:

(make-pathname :device "c" :directory '(:absolute "foo" "bar") :name "baz") 

то это может быть правильным при использовании одной реализации, но не другой.

Вместо того, чтобы создавать пути с нуля, проще создать новый файловый путь, используя существующий файловый путь, при помощи аргумента-ключа :defaults функции MAKE-PATHNAME. С этим параметром можно предоставить указатель файлового пути, из которого будут взяты компоненты, не указанные другими аргументами. Для примера, следующее выражение создает файловый путь с расширением .html и компонентами из файлового пути input-file:

(make-pathname :type "html" :defaults input-file) 

Предполагая, что значение input-file было предоставлено пользователем, этот код – надёжен вопреки различиям операционных систем и реализаций, таким как наличие либо отсутствие в файловом пути буквы диска или место её расположения.10)

Используя эту же технику, можно создать файловый путь с другой компонентой директория.

(make-pathname :directory '(:relative "backups") :defaults input-file) 

Однако этот код создаст файловый путь с компонентой директория, равной относительному пути "backups/", безотносительно к любым другим компонентам файла input-file. Например:

(make-pathname :directory '(:relative "backups") 
               :defaults #p"/foo/bar/baz.txt"
)
→ #p"backups/baz.txt"

Возможно, когда-нибудь вы захотите объединить два файловых пути, один из которых имеет относительный компонент директория, путем комбинирования их компонент директория. Например, предположим, что имеется относительный файловый путь #p"foo/bar.html", который вы хотите объединить с абсолютным файловым путем #p"/www/html/", чтобы получить #p"/www/html/foo/bar.html". В этом случае MAKE-PATHNAME не подойдет; то, что вам надо, – MERGE-PATHNAMES.

MERGE-PATHNAMES принимает два файловых пути и соединяет их, заполняя при этом компоненты, которые в первом файловом пути равны NIL, соответствующими значениями из второго файлового пути. Это очень похоже на MAKE-PATHNAME, которая заполняет все неопределенные компоненты значениями, предоставленными аргументом :defaults. Однако, MERGE-PATHNAMES особенно относится к компоненте директория: если директория первого файлового пути – относительна, то директорией окончательного файлового пути будет директория первого пути относительно директории второго. Так:

(merge-pathnames #p"foo/bar.html" #p"/www/html/") → #p"/www/html/foo/bar.html" 

Второй файловый путь также может быть относительным. В этом случае окончательный путь также будет относительным.

(merge-pathnames #p"foo/bar.html" #p"html/") → #p"html/foo/bar.html" 

Для того, чтобы обратить это процесс, то есть получить файловый путь, который относителен определенной корневой директории, используйте полезную функцию ENOUGH-NAMESTRING.

(enough-namestring #p"/www/html/foo/bar.html" #p"/www/") → "html/foo/bar.html" 

Вы можете соединить ENOUGH-NAMESTRING и MERGE-PATHNAMES для того, чтобы создать файловый путь (с одинаковой относительной частью) в другой корневой директории.

(merge-pathnames 
  (enough-namestring #p"/www/html/foo/bar/baz.html" #p"/www/")
  #p"/www-backups/"
)
→   #p"/www-backups/html/foo/bar/baz.html"

MERGE-PATHNAMES используется стандартными функциями для доступа к файлам, чтобы дополнять незавершенные файловые пути. Например, пусть есть файловый путь, имеющий только компоненты имя и тип.

(make-pathname :name "foo" :type "txt") → #p"foo.txt" 

Если вы попытаетесь передать этот файловый путь как аргумент функции OPEN, недостающие компоненты, как, например, директория, должны быть заполнены, чтобы Lisp смог преобразовать файловый путь в действительное файловое имя. Common Lisp добудет эти значения для недостающих компонент, объединяя данный файловый путь со значением переменной *DEFAULT-PATHNAME-DEFAULTS*. Начальное значение этой переменной определено реализацией, но, как правило, это файловый путь, компонент директория которого представляет ту директорию, в которой Lisp был запущен. Компоненты хост и устройство заполнены подходящими значениями, если в этом есть необходимость. Если MERGE-PATHNAMES вызвана только с одним аргументом, то она объединит аргумент со значением *DEFAULT-PATHNAME-DEFAULTS*. Например, если *DEFAULT-PATHNAME-DEFAULTS*#p"/home/peter/", то в результате:

(merge-pathnames #p"foo.txt") → #p"/home/peter/foo.txt" 

Два представления для имен директорий

Существует один неприятный момент при работе с файловым путем, который представляет директорию. Файловые объекты разделяют компоненты директория и имя файла, но Unix и Windows рассматривают директории как еще один тип файла. Поэтому, в этих системах, каждая директория может иметь два различных преставления.

Одно из них, которое я назову представлением файла, рассматривает директорию, как любой другой файл и размещает последний элемент строки имени в компоненты имя и тип. Другое представление – представление директории – помещает все элементы имени в компонент директория, оставляя компоненты имя и тип равными NIL. Если /foo/bar/ – директория, тогда любой из следующих двух файловых путей представляет ее.

(make-pathname :directory '(:absolute "foo") :name "bar") ; file form 
(make-pathname :directory '(:absolute "foo" "bar"))       ; directory form

Когда вы создаете файловые пути с помощью MAKE-PATHNAME, вы можете получить любое из двух представлений, но нужно быть осторожным, когда имеете дело со строками имен. Все современные реализации Lisp создают представление файла, если только строка имени не заканчивается разделителем пути. Но вы не можете полагаться на то, что строки имени, предоставленные пользователем, будут в том либо ином представлении. Например, предположим, что вы запросили у пользователя имя директории, в которой сохраните файл. Пользователь ввел "/home/peter". Если передать функции MAKE-PATHNAME эту строку как аргумент :defaults:

(make-pathname :name "foo" :type "txt" :defaults user-supplied-name) 

то в конце концов вы сохраните файл как /home/foo.txt, а не /home/peter/foo.text, как предполагалось, так как "peter" из строки имени будет помещен в компонент имя, когда user-supplied-name будет преобразовано в файловый путь. В переносимой библиотеке файловых путей, которую я обсужу в следующей главе, вы напишите функцию pathname-as-directory, которая преобразует файловый объект в представление директории. С этой функцией вы сможете сохранять наверняка файл в директории, указанной пользователем.

(make-pathname 
  :name "foo" :type "txt" :defaults (pathname-as-directory user-supplied-name)
)

Взаимодействие с файловой системой

Хоть открытие файлов для чтения и записи – наиболее частый вид взаимодействия с файловой системой, порой вам захочется проверить существует ли файл, прочитать содержимое директории, удалить или переименовать файлы, создать директории или получить такую информацию о файле, как: кто его владелец, когда он последний раз был изменен, его длину. Тут-то общность абстракции файловых путей немного некстати. Стандарт языка не определяет, как функции, взаимодействующие с файловой системой, отображаются на какую-то конкретную файловую систему, поэтому у создателей конкретных реализаций есть некоторая свобода действий.

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

Для того, чтобы проверить существует ли файл, соответствующий указателю файлового пути – будь-то файловый путь, строка имени или файловый поток, – можно использовать функцию PROBE-FILE. Если файл, соответствующий указателю, существует, PROBE-FILE вернет "настоящее" имя файла – файловый путь с любыми преобразованиями уровня файловой системой, как, например, следование по символическим ссылкам. В ином случае, она вернет NIL. Однако, не все реализации позволяют использовать ее для того, чтобы проверить существует ли директория. Также, Common Lisp не предоставляет переносимого способа определить, чем является существующий файл – обычным файлом или директорией. В следующей главе вы сделаете функцию-обертку для PROBE-FILEfile-exists-p, которая может проверить существует ли файл и ответить: данное имя является именем файла или директории.

Так же стандартная функция для чтения списка файлов – DIRECTORY – работает прекрасно в незамысловатых случаях, но из-за различий реализаций ее использование становится мудреным делом. В следующей главе вы определите функцию чтения содержимого директорий, которая сгладит некоторые из различий.

DELETE-FILE и RENAME-FILE делают то, что следует из их названий. DELETE-FILE принимает указатель файлового пути и удаляет указанный файл, возвращая истину в случае успеха. В ином случае она сигнализирует FILE-ERROR.11)

RENAME-FILE принимает два указателя файлового пути и изменяет имя файла, указанного первым параметром, на имя, указанное вторым параметром.

Вы можете создать директории с помощью функции ENSURE-DIRECTORIES-EXIST. Она принимает указатель файлового пути и удостоверяется, что все элементы компонента директория существуют и являются директориями, создавая их при необходимости. Так как она возвращает файловый путь, который ей был передан, то удобно ее вызывать на месте аргумента другой функции.

(with-open-file (out (ensure-directories-exist name) :direction :output) 
   ...
   
)

Обратите внимание, что если вы передаете ENSURE-DIRECTORIES-EXIST имя директории, то оно должно быть в представлении директории, или последняя директория не будет создана. Обе функции FILE-WRITE-DATE и FILE-AUTHOR принимают указатель файлового пути. FILE-WRITE-DATE возвращает количество секунд, которое прошло с полуночи 1-го января 1900 года, среднее время по Гринвичу (GMT), до времени последней записи в файл. FILE-AUTHOR возвращает – в Unix и Windows – владельца файла.12)

Для того, чтобы получить размер файла, используйте функцию FILE-LENGTH. По историческим причинам FILE-LENGTH принимает поток как аргумент, а не файловый путь. В теории это позволяет FILE-LENGTH вернуть размер в терминах типа элементов потока. Однако, так как на большинстве современных операционных системах единственная доступная информация о размере файла – исключая чтение всего файла, чтобы узнать размер – это размер в байтах, что возвращают большинство реализаций, даже если FILE-LENGTH передан символьный поток. Однако, стандарт не требует такого поведения, поэтому ради предсказуемых результатов лучший способ получить размер файла – использовать бинарный поток.13)

(with-open-file (in filename :element-type '(unsigned-byte 8)) 
  (file-length in)
)

Похожая функция, которая также принимает открытый файловый поток в качестве аргумента – FILE-POSITION. Когда ей передают только поток, она возвращает текущую позицию в файле – количество элементов, прочитанных из потока или записанных в него. Когда ее вызывают с двумя аргументами, потоком и указателем позиции, она устанавливает текущей позицией указанную. Указатель позиции должен быть ключевым словом :start, :end или неотрицательным целым числом. Два ключевых слова устанавливают позицию потока в начало или конец. Если же передать функции целое число, то позиция переместится в указанную позицию файла. В случае бинарного потока позиция – это просто смещение в байтах от начала файла. Однако символьные потоки немного сложнее из-за проблем с кодировками. Лучшее что вы можете сделать если нужно переместиться на другую позицию в текстовом файле – всегда передавать FILE-POSITION в качестве второго аргумента только то значение, которое вернула функция FILE-POSITION, вызванная с тем же потоком в качестве единственного аргумента.

Другие операции ввода/вывода

Вдобавок к файловым потокам Common Lisp поддерживает другие типы потоков, которые также можно использовать с разнообразными функциями ввода/вывода для чтения, записи и печати. Например, можно считывать данные из строки или записывать их в строку, используя STRING-STREAM, которые вы можете создать функциями MAKE-STRING-INPUT-STREAM и MAKE-STRING-OUTPUT-STREAM.

MAKE-STRING-INPUT-STREAM принимает строку и необязательные начальный и конечный индексы, указывающие часть строки, которую следует связать с потоком, и возвращает символьный поток, который можно передать как аргумент любой функции символьного ввода, как, например, READ-CHAR, READ-LINE или READ. Например, если у вас есть строка, содержащая число с плавающей точкой с синтаксисом Common Lisp, вы можете преобразовать ее в число с плавающей точкой:

(let ((s (make-string-input-stream "1.23"))) 
  (unwind-protect (read s)
    (close s)
)
)

MAKE-STRING-OUTPUT-STREAM создает поток, который можно использовать с FORMAT, PRINT, WRITE-CHAR, WRITE-LINE и т.д. Она не принимает аргументов. Что бы вы не записывали, строковый поток вывода будет накапливать это в строке, которую потом можно получить с помощью функции GET-OUTPUT-STREAM-STRING. Каждый раз при вызове GET-OUTPUT-STREAM-STRING внутренняя строка потока очищается, поэтому существующий строковый поток вывода можно снова использовать.

Однако, использовать эти функции напрямую вы будете редко, так как макросы WITH-INPUT-FROM-STRING и WITH-OUTPUT-TO-STRING предоставляют более удобный интерфейс. WITH-INPUT-FROM-STRING похожа на WITH-OPEN-FILE – она создает строковый поток ввода на основе переданной строки и выполняет код в своем теле с потоком, который присвоен переменной, вами предоставленной. Например, вместо формы LET с явным использованием UNWIND-PROTECT, вероятно, лучше написать:

(with-input-from-string (s "1.23") 
  (read s)
)

Макрос WITH-OUTPUT-TO-STRING также связывает вновь созданный строковый поток вывода с переменной, вами названной, и затем выполняет код в своем теле. После того, как код был выполнен, WITH-OUTPUT-TO-STRING вернет значение, которое было бы возвращено GET-OUTPUT-STREAM-STRING.

CL-USER> (with-output-to-string (out)
            (format out "hello, world ")
            (format out "~s" (list 1 2 3))
)

"hello, world (1 2 3)"

Другие типы потоков, определенные в стандарте языка, предоставляют различные способы "соединения" потоков, то есть позволяют подключать потоки друг к другу почти в любой конфигурации. BROADCAST-STREAM – поток вывода, который посылает записанные данные множеству потоков вывода, переданных как аргументы функции-конструктору MAKE-BROADCAST-STREAM.14) В противоположность этому, CONCATENATED-STREAM – поток ввода, который принимает ввод от множества потоков ввода, перемещаясь от потока к потоку, когда очередной поток достигает конца. Потоки CONCATENATED-STREAM создаются функцией MAKE-CONCATENATED-STREAM, которая принимает любое количество потоков ввода в качестве аргументов.

Еще существуют два вида двунаправленных потоков, которые могут подключать потоки друг к другу – TWO-WAY-STREAM и ECHO-STREAM. Их функции-конструкторы, MAKE-TWO-WAY-STREAM и MAKE-ECHO-STREAM, обе принимают два аргумента, поток ввода и поток вывода, и возвращают поток соответствующего типа, который можно использовать как с потоками ввода, так и с потоками вывода.

В случае TWO-WAY-STREAM потока, каждое чтение вернет данные из потока ввода, и каждая запись пошлет данные в поток вывода. ECHO-STREAM по существу работает точно так же кроме того, что все данные прочитанные из потока ввода также направляются в поток вывода. То есть поток вывода потока ECHO-STREAM будет содержать стенограмму "беседы" двух потоков.

Используя эти пять типов потоков, можно построить почти любую топологию сети потоков, какую бы вы ни пожелали.

В заключение, хотя стандарт Common Lisp не оговаривает какой-либо сетевой API, большинство реализаций поддерживают программирование сокетов и, как правило, реализуют сокеты как еще один тип потока. Следовательно, можно использовать с ними все обычные функции вводы/вывода.15)

Теперь вы готовы двигаться дальше для написания библиотеки, которая позволит сгладить некоторые различия поведения основных функций для работы с файловыми путями в различных реализациях Common Lisp.

1)Заметим, однако, что считыватель Lisp, зная как пропускать комментарии, полностью их пропускает. Поэтому, если вы считаете конфигурационный файл, содержащий комментарии, при помощи READ, а затем запишете изменения при помощи PRINT, то потеряете все комментарии.
2)По умолчанию OPEN использует кодировку, используемую в операционной системе, но возможно указать ключевой параметер :external-format, в котором передать используемую схему кодирования, отличную от используемой в операционной системе. Символьные потоки также преобразуют платформозависимые символы конца строки в символ #\Newline.
3)Тип (unsigned-byte 8) обозначает 8-битный беззнаковый байт; "Байты" в Common Lisp могут иметь различный размер поскольку Lisp может выполняться на различных платформах с размерами байтов от 6 до 9 бит, к примеру PDP-10, может адресовать битовые поля различной длины от 1 до 36 бит.
4)В общем, поток может быть либо символьным, либо бинарным, так что невозможно смешивать вызовы READ-BYTE с READ-CHAR и другими символьными функциями. Однако, в некоторых реализациях, таких как Allegro, поддерживаются так называемые бивалентные потоки, которые поддерживают как символьные так и байтовые операции ввода/вывода.
5)Некоторые могут полагать, что это не является проблемой в языках со сборщиком мусора, таких как Lisp. В большинстве реализаций Lisp все потоки, которые больше не нужны, автоматически закрываются. Но на это не надо полагаться, так как сборщик мусора запускается, как правило, когда остается мало памяти. Он ничего не знает о других дефицитных ресурсах таких, как файловые дескрипторы. Если доступно много памяти, то доступные файловые дескрипторы могут быстро закончиться до вызова сборщика мусора.
6)Еще одна причина, по которой система файловых путей выглядит причудливо, – введение логических файловых путей. Однако, вы можете плодотворно использовать систему файловых путей с единственной мыслью в голове о логических файловых путях: о них можно не вспоминать. В двух словах, логические файловые пути позволяют программам, написанных на Common Lisp, ссылаться на файловые пути без именования конкретного файла. Они могут быть отображены впоследствии на конкретную точку файловой системы, когда будет установлена ваша программа, при помощи "преобразования логических файловых путей", которое преобразует эти имена, соответствующие определенному образцу, в файловые пути, представляющие файлы в файловой системе, так называемые физические файловые пути. Они находят применение в определенных ситуациях, но вы может со спокойной душой пройти мимо них.
7)Многие реализации Common Lisp под Unix трактуют файловые имена, чей последний элемент начинается с точки и не содержит больше других точек, следующим образом: помещают весь элемент – вместе с точкой – в компонент имя и оставляют компонент тип равным NIL.
(pathname-name (pathname "/foo/.emacs")) -> ".emacs" 
(pathname-type (pathname "/foo/.emacs")) -> NIL
Однако, не все реализации следуют этому соглашению. Некоторые создают файловый путь с пустой строкой в качестве имени и emacs в качестве типа.
8)Имя, возвращенное функцией FILE-NAMESTRING, также включает компонент версия на файловой системе, которая использует это.
9)Хост не может быть равным NIL, но если все же это так, то он будет заполнен значением, определенным конкретной реализаций.
10)Для максимальной переносимости, следует писать:
(make-pathname :type "html" :version :newest :defaults input-file) 
Без аргумента :version на файловой системе с контролем версий, результирующий файловый путь унаследует номер версии от входного файла, который, вероятнее всего, будет неправильным, ведь если файл сохранялся не раз, он будет иметь больший номер, чем созданный HTML файл. В реализациях без поддержки контроля версий, аргумент :version должен игнорироваться. Забота о переносимости – на вашей совести.
11)См. главу 19 насчет обработки ошибок.
12)Для приложений, которым нужен доступ к другим атрибутам файла в определенной операционной системе или на файловой системе, некоторые библиотеки предоставляют обвязки (bindings) для системных вызовов. Библиотека Osicat, размещенная по адресу http://common-lisp.net/project/osicat/, предоставляет простой API, созданный на основе Universal Foreign Function Interface (UFFI). Она должна работать с большинством реализаций Common Lisp на POSIX-совместимых операционных системах.
13)Количество байтов и символов в файле может разниться, даже если не используется многобайтная кодировка. Потому что символьные потоки также преобразуют специфичные для платформы переносы строк в единственный символ #\Newline. В Windows, в которой используется CRLF в качестве переноса строки, количество символов, как правило, будет меньше чем количество байт. Если вам действительно требуется знать количество символов в файле, то вы должны набраться смелости и написать что-то похоже на:
(with-open-file (in filename) 
  (loop while (read-char in nil) count t)
)

или, возможно, что-нибудь более эффективное, вроде этого:
(with-open-file (in filename) 
  (let ((scratch (make-string 4096)))
    (loop for read = (read-sequence scratch in)
          while (plusp read) sum read
)
)
)

14)MAKE-BROADCAST-STREAM может создать "черную дыру" для данных, если ее вызвать без аргументов.
15)Наибольшим пробелом в стандартных средствах ввода/вывода Common Lisp является отсутствие способа определения пользователем новых типов потоков. Однако, для определения пользователем потоков существует два стандарта де-факто. Во время разработки стандарта Common Lisp, Дэвид Грэй (David Gray) из Texas Instruments предложил набросок API, который позволяет пользователям определять новые типы потоков. К сожалению, уже не оставалось времени для разбора всех трудностей, поднятых этим наброском, чтобы включить его в стандарт. Но все же много реализаций поддерживают в некоторой форме так называемые потоки Грэя. Они основывают свой API на наброске Грэя. Другой, более новый API – Simple Streams (простые потоки) – были разработаны компанией Franz и включены в Allegro Common Lisp. Они были разработаны, чтобы улучшить производительность определяемых пользователем потоков, схожих с потоками Грэя. Простые потоки позже переняли некоторые открытые реализации Common Lisp.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru