Глава 13. Работа с файлами
В этой главе мы рассмотрим некоторые общие функции для манипулирования файлами. В стандартном релизе Эрланга есть множество функций для работы с файлами. Мы сосредоточимся на той малой их части, которую я использую в большинстве моих программ. Мы также рассмотрим некоторые примеры техники, которую я использую для написания эффективного кода обработки файлов. В дополнение к этому, я кратко упомяну некоторые реже используемые файловые операции, чтобы вы знали, что они есть. А если вы захотите узнать больше подробностей – обращайтесь к мануалам.
Мы сосредоточимся на следующих областях:
- организация библиотек;
- разные способы чтения файлов;
- разные способы записи файлов;
- работа с директориями;
- поиск информации о файлах.
13.1 Организация библиотек
Функции для манипулирования файлами организованы в четыре модуля:
file
– функции для открытия, закрытия, чтения и записи файлов; просмотра директорий и т. д. Краткая сводка часто используемых функций приведена на Таблице 13.1. За полными подробностями обращайтесь к руководству по модулю file
.
filename
– этот модуль содержит функции, которые манипулируют именами файлов платформонезависимым образом, так, что вы можете выполнять один и тот же код на разных операционных системах.
filelib
– этот модуль – дополнение к file, который содержит ряд вспомогательных функций для просмотра файлов, проверки типов файлов и т.п. Большинство из них написаны, используя функции из file
.
io
– этот модуль содержит функции, которые работают с открытыми файлами. Он содержит функции для парсинга (разбора) данных в файле и записи форматированных данных в файл.
Функция | Описание |
---|---|
change_group |
Сменить группу у файла |
change_owner |
Сменить владельца у файла |
change_time |
Сменить время модификации или последнего доступа у файла |
close |
Закрыть файл |
consult |
Прочитать термы Эрланга из файла |
copy |
Копировать содержимое файла |
del_dir |
Удалить директорию |
delete |
Удалить файл |
eval |
Выполнить выражения Эрланга из файла |
format_error |
Вернуть строку с описанием причины ошибки |
get_cwd |
Получить текущую директорию |
list_dir |
Вывести список файлов в директории |
make_dir |
Создать директорию |
make_link |
Создать hard ссылку на файл |
make_symlink |
Создать soft (символическую) ссылку на файл |
open |
Открыть файл |
position |
Установить позицию в файле |
pread |
Читать из файла с указанной позиции |
pwrite |
Записать в файл с указанной позиции |
read |
Читать из файла |
read_file |
Прочитать весь файл целиком |
read_file_info |
Получить информацию о файле |
read_link |
Посмотреть — куда указывает ссылка |
read_link_info |
Получить информацию о ссылке или файле |
rename |
Переименовать файл |
script |
Выполнить и вернуть значение выражений Эрланга из файла |
set_cwd |
Установить текущую директорию |
sync |
Синхронизировать состояние файла в памяти и на физическом носителе |
truncate |
Обрезать файл |
write |
Записать в файл |
write_file |
Записать весь файл целиком |
write_file_info |
Сменить информацию о файле |
Таблица 13.1: Сводка файловых операций (модуль file
)
Разные способы чтения файлов
Давайте глянем – что у нас есть, когда дело доходит до чтения файлов. Мы начнём с написания пяти маленьких программок, которые открывают файл иберут оттуда данные несколькими разными способами.
Содержимое файла — это просто последовательность байт. Что они значат — зависит от интерпретации этих байтов.
Чтобы продемонстрировать это мы будем использовать один и тот же файл для всех наших примеров. Вообще-то, он содержит последовательность термов Эрланга. В зависимости от того, как мы открываем и читаем файл, мы можем интерпретировать содержимое как последовательность термов Эрланга, как последовательность текстовых строк или как куски бессмысленных и беспощадных бинарных данных.
Загрузить data1.dat
{person, "joe" , "armstrong" ,
[{occupation, programmer},
{favoriteLanguage, erlang}]}.
{cat, {name, "zorro" },
{owner, "joe" }}.
А теперь мы прочитаем части этого файла разными способами.
Чтение всех термов из файла
data1.dat
содержит последовательность термов Эрланга. Мы можем прочитать их все, используя функцию file:consult следующим образом:
1> file:consult("data1.dat").
{ok,[{person,"joe",
"armstrong",
[{occupation,programmer},{favoriteLanguage,erlang}]},
{cat,{name,"zorro"},{owner,"joe"}}]}
file:consult(File)
полагает, что File
содержит последовательность термов Эрланга. Она возвращает {ok, [Term]}
, если может прочитать все термы из файла. В противном случае она возвращает {error, Reason}
.
Чтение термов по одному за раз
Если мы хотим прочитать термы из файла по одному за раз, то мы открываем файл функцией file:open
, а затем читаем отдельные термы функцией io:read
до тех пор, пока не дойдём до конца файла. Затем мы закрываем файл функцией file:close
.
Вот сеанс в оболочке Эрланга, который показывает что происходит, когда мы читаем термы из файла по одному за раз:
1> {ok, S} = file:open("data1.dat", read).
{ok,<0.36.0>}
2> io:read(S, '').
{ok,{person,"joe",
"armstrong",
[{occupation,programmer},{favoriteLanguage,erlang}]}}
3> io:read(S, '').
{ok,{cat,{name,"zorro"},{owner,"joe"}}}
4> io:read(S, '').
eof
5> file:close(S)
Функции, которые мы здесь использовали, следующие:
@spec file:open(File, read) => {ok, IoDevice} | {error, Why}
Пытается открыть файл File
для чтения. Возвращает {ok, IoDevice}
в случае успеха, либо {error, Reason}
в случае ошибки. IoDevice
— это некий дескриптор, который используется для доступа к файлу.
@spec io:read(IoDevice, Prompt) => {ok, Term} | {error,Why} | eof
Читает терм Эрланга из IoDevice
. Подсказка Prompt игнорируется если IoDevice
представляет собой открытый файл. Подсказка Prompt
используется для выдачи подсказки только если мы используем io:read
для чтения стандартного ввода.
@spec file:close(IoDevice) => ok | {error, Why}
Закрывает IoDevice
.
Используя эти функции мы можем реализовать file:consult
, который мы использовали в предыдущей части. Вот, как file:consult
может быть определён:
Загрузить lib_misc.erl
consult(File) ->
case file:open(File, read) of
{ok, S} ->
Val = consult1(S),
file:close(S),
{ok, Val};
{error, Why} ->
{error, Why}
end.
consult1(S) ->
case io:read(S, '') of
{ok, Term} -> [Term|consult1(S)];
eof -> [];
Error -> Error
end.
На самом деле file:consult
определён не так. Стандартная библиотека использует улучшенную обработку ошибок.
Ну а теперь пришло время посмотреть на версию из стандартной библиотеки. Если вы поняли, как работает предыдущая версия функции, то вы легко поймёте код из библиотеки. Вот только есть одна проблема. Как найти исходный код для file.erl
? Для нахождения кода мы используем функцию code:which
, которая обнаруживает объектный код для любого загруженного модуля.
1> code:which(file).
"/usr/local/lib/erlang/lib/kernel-2.11.2/ebin/file.beam"
В стандартном релизе у каждой библиотеки есть две поддиректории. Одна, называемая src
, содержит исходный код. Другая, называемая ebin
, содержит скомпилированный Эрланг код. Так что исходный код для файла file.erl
должен находиться в следующей директории:
/usr/local/lib/erlang/lib/kernel-2.11.2/src/file.erl
В случае, когда ничего уже не помогает, а документация не даёт ответов на ваши вопросы, быстрый взгляд в исходный код может помочь с ответом. Я знаю, что этого (поиска ответа в исходниках) не должно происходить, но все мы люди и иногда документация просто не помогает.
Чтение строк из файла по одной за раз
Если заменить io:read
на io:get_line
, то мы можем прочитать строки из файла по одной за раз. io:get_line
читает символы до тех пор, пока не встретит символ перевода строки или конец файла. Вот пример:
1> {ok, S} = file:open("data1.dat", read).
{ok,<0.43.0>}
2> io:get_line(S, '').
"{person, \\"joe\\", \\"armstrong\\",\\n"
3> io:get_line(S, '').
"\\t[{occupation, programmer},\\n"
4> io:get_line(S, '').
"\\t {favoriteLanguage, erlang}]}.\\n"
5> io:get_line(S, '').
"\\n"
6> io:get_line(S, '').
"{cat, {name, \\"zorro\\"},\\n"
7> io:get_line(S, '').
" {owner, \\"joe\\"}}.\\n"
8> io:get_line(S, '').
eof
9> file:close(S).
ok
Чтение всего файла целиком как бинарный объект
Вы можете использовать file:read_file(File)
, чтобы прочитать файл целиком в бинарный объект, используя следующую атомарную операцию:
1> file:read_file("data1.dat").
{ok,<<"{person, \\"joe\\", \\"armstrong\\""...>>}
file:read_file(File)
возвращает {ok, Bin}
в случае успеха и {error,Why}
в противном случае.
Это явно лучший способ чтения файлов и я использую этот способ наиболее часто. В большинстве случаев я читаю файл целиком в память одной операцией, работаю над содержимым файла и сохраняю файл тоже одной операцией (используя file:write_file)
. У нас будет пример для данного способа работы.
Чтение файла с произвольным доступом
Если файл, который мы хотим прочитать, очень большой или содержит бинарные данные в формате, который определён где-то вовне, то мы можем открыть файл в сыром (raw) режиме и читать порции файла операцией file:pread
.
Пример:
1> {ok, S} = file:open("data1.dat", [read,binary,raw]).
{ok,{file_descriptor,prim_file,{\#Port<0.106>,5}}}
2> file:pread(S, 22, 46).
{ok,<<"rong\\",\\n\\t[{occupation, progr...>>}
3> file:pread(S, 1, 10).
{ok,<<"person, \\"j">>}
4> file:pread(S, 2, 10).
{ok,<<"erson, \\"jo">>}
5> file:close(S).
file:pread(IoDevice, Start, Len)
читает точно Len
байт из IoDevice
, начиная с байта в позиции Start
(байты в файле нумеруются так, что первый байт находится в позиции 1) (прим. перев. - так в книге. В документации — всё по-другому). Она возвращает {ok, Bin}
или {error,Why}
.
В заключение, мы используем функции для произвольного доступа к файлу для написания утилиты, которая нам понадобится в следующей главе. В части 14.7 «Широковещательный сервер» мы разработаем простой широковещательный сервер (это сервер для так называемого потокового вещания. В данном случае — для вещания MP3). Часть этого сервера нуждается в поиске исполнителя и названий композиций, которые внедрены в файл MP3. Мы сделаем это в следующей части.
Чтение тегов MP3
MP3 — это бинарный формат, используемый для хранения сжатых звуковых данных. MP3 файлы сами по себе не содержат информацию о содержимом файла. К примеру в MP3 файле с музыкой не будет имени исполнителя. Эти данные (название композиции, исполнитель и прочее) хранятся внутри MP3 файла в специальном блочном формате ID3. Теги ID3 были придуманы программистом Eric Kemp для хранения метаданных, описывающих содержимое звукового файла. Вообще-то, есть несколько форматов ID3, но для наших целей мы будем использовать простейшие формы тегов — ID3v1 и ID3v1.1.
У тега ID3v1 простая структура — это последние 128 байт файла, содержащие тег фиксированной длины. Первые 3 байта содержат ASCII символы TAG, за которыми идут ряд полей фиксированной длины. Полностью эта 128 байтовая структура показана далее:
Длина | Содержимое |
---|---|
3 | Заголовок, содержащий символы TAG |
30 | название |
30 | исполнитель |
30 | альбом |
4 | год |
30 | комментарий |
1 | жанр |
В теге ID3v1 не было места, чтобы добавить номер композиции. Способ, реализующий это был предложен Michael Mutschler в формате ID3v1.1. Идея в том, чтобы заменить 30 байтовый комментарий следующим:
| Длина | Содержимое | | 28 | Комментарий | | 1 | 0 (ноль) | | 1 | Номер композиции |
Легко написать программу, которая будет читать теги ID3v1 из MP3 файла и сопоставлять поля, используя бинарное битовое сопоставление с образцом. Вот эта программа:
Загрузить id3_v1.erl
-module(id3_v1).
-import(lists, [filter/2, map/2, reverse/1]).
-export([test/0, dir/1, read_id3_tag/1]).
test() -> dir("/home/joe/music_keep" ).
dir(Dir) ->
Files = lib_find:files(Dir, "*.mp3" , true),
L1 = map(fun(I) ->
{I, (catch read_id3_tag(I))}
end, Files),
%% L1 = [{File, Parse}] where Parse = error | [{Tag,Val}]
%% we now have to remove all the entries from L where
%% Parse = error. We can do this with a filter operation
L2 = filter(fun({_,error}) -> false;
(_) -> true
end, L1),
lib_misc:dump("mp3data" , L2).
read_id3_tag(File) ->
case file:open(File, [read,binary,raw]) of
{ok, S} ->
Size = filelib:file_size(File),
{ok, B2} = file:pread(S, Size-128, 128),
Result = parse_v1_tag(B2),
file:close(S),
Result;
Error ->
{File, Error}
end.
parse_v1_tag(<<$T,$A,$G,
Title:30/binary, Artist:30/binary,
Album:30/binary, _Year:4/binary,
_Comment:28/binary, 0:8,Track:8,_Genre:8>>) ->
{"ID3v1.1" ,
[{track,Track}, {title,trim(Title)},
{artist,trim(Artist)}, {album, trim(Album)}]};
parse_v1_tag(<<$T,$A,$G,
Title:30/binary, Artist:30/binary,
Album:30/binary, _Year:4/binary,
_Comment:30/binary,_Genre:8>>) ->
{"ID3v1" ,
[{title,trim(Title)},
{artist,trim(Artist)}, {album, trim(Album)}]};
parse_v1_tag(_) ->
error.
trim(Bin) ->
list_to_binary(trim_blanks(binary_to_list(Bin))).
trim_blanks(X) -> reverse(skip_blanks_and_zero(reverse(X))).
skip_blanks_and_zero([$\\s|T]) -> skip_blanks_and_zero(T);
skip_blanks_and_zero([0|T]) -> skip_blanks_and_zero(T);
skip_blanks_and_zero(X) -> X.
Основная точка входа нашей программы — это id3_v1:dir(Dir)
. Первое, что мы делаем — это ищем все наши MP3 файлы, вызывая lib_find:find(Dir, "*.mp3", true)
(утилита поиска показана далее в части 13.8), которая рекурсивно сканирует директории ниже Dir
на предмет файлов MP3. Найдя файл, мы разбираем теги, вызывая read_id3_tag
. Разбор сильно упрощён, потому что мы используем простое битовое сопоставление с образцом. После этого мы подчищаем имена исполнителей и названия композиций, удаляя завершающие пробелы и нулевые символы, которые разделяют строки. В конце мы выводим результат в файл для дальнейшего использования (lib_misc:dump
описывается в части E.2, Техника отладки).
Большинство музыкальных файлов помечены тегами ID3v1, даже если они дополнительно содержат ещё и теги стандартов ID3v2, v3, v4, добавленные позже, отформатированные по-другому и находящиеся в начале файла (или, что более редко — в середине файла). Программы для тегирования часто добавляют как ID3v1, так и дополнительные (и более трудные для чтения) теги в начало файла. Для наших целей мы сосредоточимся только на файлах, содержащих корректные теги ID3v1 и ID3v1.1.
Теперь, когда мы знаем, как читать файл, мы можем перейти к различным способам записи файла.
13.3 Разные способы записи файлов
Запись в файл включает в себя достаточно много таких же операций, как и чтение файла. Рассмотрим их подробнее.
Запись списка термов в файл
Предположим, что мы хотим создать файл, который мы сможем прочитать функцией file:consult
. Стандартная библиотека вообще-то не содержит такой функции, так что мы напишем свою собственную. Назовём эту функцию unconsult
.
Загрузить lib_misc.erl
unconsult(File, L) ->
{ok, S} = file:open(File, write),
lists:foreach(fun(X) -> io:format(S, "\~p.\~n" ,[X]) end, L),
file:close(S).
Мы можем выполнить это из оболочки Эрланга, чтобы создать файл, называемый test1.dat
:
1> lib_misc:unconsult("test1.dat",
[{cats,["zorrow","daisy"]},
{weather,snowing}]).
ok
Удостоверимся, что это действительно OK:
2> file:consult("test1.dat").
{ok,[{cats,["zorrow","daisy"]},{weather,snowing}]}
Чтобы реализовать unconsult мы открываем файл на запись и затем используем io:format(S, "\~p.\~n", [X])
для записи термов в файл. io:format
— это рабочая лошадка для создания форматированного вывода. Для выполнения форматированного вывода мы вызываем функцию:
@spec io:format(IoDevice, Format, Args) -> ok
IoDevice
— это некое устройство ввода-вывода (которое было открыто в режиме записи), Format
— это строка, содержащая коды форматирования, а Args
— это список элементов для вывода.
Для каждого элемента из Args
в строке формата должна присутствовать команда форматирования. Команды форматирования начинаются с тильды ~
.
Вот некоторые наиболее часто используемые команды форматирования:
~n
- Перевод строки. ~n
достаточно умён, так что работает платформонезависимо — на Unix — выведет в поток вывода ASCII (10), а на Windows — ASCII (13, 10)
~p
- Структурная распечатка аргумента
~s
- Аргумент является строкой
~w
- Вывод данных со стандартным синтаксисом. Используется для вывода термов Эрланга
У форматной строки есть масса аргументов, которые никто не будет запоминать в здравом уме. Как говорил Эйнштейн — для констант есть справочники. А для полного списка параметров формата есть руководство по модулю io
. Я помню только ~p
, ~s
и ~n
. Если вы начнёте с них, у вас не возникнет лишних проблем.
Лирическое отступление
Я соврал. Вам наверняка понадобится больше, чем просто ~p
, ~s
, ~n
. Вот пара примеров:
Формат | Результат |
---|---|
io:format("~10s~n", ["abc"]) |
⎵⎵⎵⎵⎵⎵⎵⎵abc |
io:format("~-10s~n", ["abc"]) |
abc⎵⎵⎵⎵⎵⎵⎵⎵ |
io:format("~10.3.+s~n",["abc"]) |
+++++++abc |
io:format("~10.10.+s~n",["abc"]) |
abc+++++++ |
io:format("~10.7.+s~n",["abc"]) |
+++abc++++ |
Запись строк в файл
Это похоже на предыдущий пример — мы просто используем другие команды форматирования:
1> {ok, S} = file:open("test2.dat", write).
{ok,<0.62.0>}
2> io:format(S, "\~s\~n", ["Hello readers"]).
ok
3> io:format(S, "\~w\~n", [123]).
ok
4> io:format(S, "\~s\~n", ["that's it"]).
ok
5> file:close(S).
Это создаёт файл называемый test2.dat
со следующим содержимым:
Hello readers
123
that's it
Запись всего файла целиком одной операцией
Это наиболее эффективный способ записи в файл. Функция file:write_file(File, IO)
записывает данные IO (который является списком ввода-вывода, т. е. списком, элементами которого могут быть другие списки ввода-вывода, бинарные данные, целые числа от 0 до 255) в файл File
. При записи список автоматически плющится (делается плоским — flattened
, т. е. все квадратные скобки устраняются). Этот способ крайне эффективен и я этим частенько пользуюсь. Программа в следующей части демонстрирует это.
Вывод URL-ов из файла
Давайте напишем простенькую функцию, называемую urls2htmlFile(L, File)
, которая берет список ULR-ов L
и создаёт HTML файл, где URL-ы представлены в виде кликабельных ссылок. Это позволит нам отработать технику создания целого файла одной-единственной операцией ввода-вывода.
Мы поместим нашу программу в модуль scavenge_url
.
Загрузить scavenge_urls.erl
-module(scavenge_urls).
-export([urls2htmlFile/2, bin2urls/1]).
-import(lists, [reverse/1, reverse/2, map/2]).
urls2htmlFile(Urls, File) ->
file:write_file(File, urls2html(Urls)).
bin2urls(Bin) -> gather_urls(binary_to_list(Bin), []).
В программе две точки входа. urls2htmlFile(Urls, File)
берёт список URL-ов и создаёт HTML файл, содержащий кликабельные ссылки для каждого URL. bin2urls(Bin)
ищет по бинарным данным и возвращает список всех URL-ов, содержащихся в этих данных. Вот urls2htmlFile
:
Загрузить scavenge_urls.erl
urls2html(Urls) -> [h1("Urls" ),make_list(Urls)].
h1(Title) -> ["<h1>" , Title, "</h1>\\n" ].
make_list(L) ->
["<ul>\\n" ,
map(fun(I) -> ["<li>" ,I,"</li>\\n" ] end, L),
"</ul>\\n" ].
Этот код возвращает вложенный список символов. Заметьте, что мы не делали попыток сплющить список (что было бы довольно неэффективно). Мы создали вложенный список символов и просто отправили его в функцию вывода. Когда мы записываем вложенный список в файл функцией file:write_file
система ввода-вывода автоматически плющит список (т. е. записывает только символы из списка, но не скобки, создающие структуры списка). Ну и в конце — код, извлекающий URL-ы из бинарных данных:
Загрузить scavenge_urls.erl
gather_urls("<a href" ++ T, L) ->
{Url, T1} = collect_url_body(T, reverse("<a href" )),
gather_urls(T1, [Url|L]);
gather_urls([_|T], L) ->
gather_urls(T, L);
gather_urls([], L) ->
L.
collect_url_body("</a>" ++ T, L) -> {reverse(L, "</a>" ), T};
collect_url_body([H|T], L) -> collect_url_body(T, [H|L]);
collect_url_body([], _) -> {[],[]}.
Чтобы выполнить это, нам надо иметь данные для разбора. Входные данные (бинарные данные) — это содержимое HTML страницы, так что нам нужна HTML страница для очистки от мусора. Для этого мы используем socket_examples:nano_get_url
(см. главу 14.1, извлечение данных с сервера). Будем делать это по шагам в оболочке Эрланга:
1> B = socket_examples:nano_get_url("www.erlang.org"),
L = scavenge_urls:bin2urls(B),
scavenge_urls:urls2htmlFile(L, "gathered.html").
ok
Это создаст файл gathered.html
:
Загрузить gathered.html
<h1>Urls</h1>
<ul>
<li><a href="old_news.html" >Older news.....</a></li>
<li>
<a href="http://www.erlang-consulting.com/training_fs.html">
here</a>
</li>
<li><a href="project/megaco/" >Megaco home</a></li>
<li><a href="EPLICENSE" >Erlang Public License (EPL)</a></li>
<li><a href="user.html\#smtp_client-1.0">smtp_client-1.0</a></li>
<li><a href="download-stats/" >download statistics graphs</a></li>
<li><a href="project/test_server" >Erlang/OTP Test Server</a></li>
<li><a href="http://www.erlang.se/euc/06/" >proceedings</a></li>
<li><a href="/doc/doc-5.5.2/doc/highlights.html" >
Read more in the release highlights.
</a></li>
<li><a href="index.html" ><img src="images/erlang.gif"
border="0" alt="Home" ></a></li>
</ul>
Запись файлов с произвольным доступом
Запись в файл с произвольным доступом подобна чтению. Сначала мы должны открыть файл в режиме записи. Затем мы используем file:pwrite(Position, Bin)
для записи в файл. Вот пример:
1> {ok, S} = file:open("...", [raw,write,binary])
{ok, ...}
2> file:pwrite(S, 10, <<"new">>)
ok
3> file:close(S)
ok
Этот код записывает символы "new", начиная со смещения 10 в файле, перезаписывая имеющееся содержимое файла.
Операции над директориями
Для операций над директориями в модуле file
есть три функции. list_dir(Dir)
используется для получения списка файлов в Dir
, make_dir(Dir)
создаёт новую директорию и del_dir(Dir)
удаляет директорию.
Если мы выполним list_dir
в директории с кодом, которую я использую при написания этой книги, то мы увидим следующее:
1> cd("/home/joe/book/erlang/Book/code").
/home/joe/book/erlang/Book/code
ok
2> file:list_dir(".").
{ok,["id3_v1.erl\~",
"update_binary_file.beam",
"benchmark_assoc.beam",
"id3_v1.erl",
"scavenge_urls.beam",
"benchmark_mk_assoc.beam",
"benchmark_mk_assoc.erl",
"id3_v1.beam",
"assoc_bench.beam",
"lib_misc.beam",
"benchmark_assoc.erl",
"update_binary_file.erl",
"foo.dets",
"big.tmp",
..
Заметьте, что файлы в списке никак не упорядочены, никак не видно признаков, что данное имя является файлом или директорией, нет длин, вообще ничего нет.
Чтобы найти больше информации об индивидуальном файле в директории мы используем функцию file:read_file_info
, которая подробнее описывается в следующей части.
Поиск информации о файле
Для нахождения информации о файле F
мы вызываем функцию file:read_file_info(F)
. Она возвращает {ok, Info}
, если F
— это правильное имя файла или директории. Info
— это запись (record) типа #file_info
, которая определена так:
-record(file_info,
{size, % Размер файла в байтах
type, % Атом: device, directory, regular, other
access, % Атом: read, write, read_write, none
atime, % Локальное время последнего чтения файла
mtime, % Локальное время последней записи файла
ctime, % Интерпретация этого поля зависит от
% операционной
% системы. В Unix это время последнего
% изменения файла
% или inode. В Windows — это время создания
% файла
mode, % Целое число: права на файл. В Windows права
% владельца
% будут дублироваться для группы и пользователя
links, % Количество ссылок на файл (1, если файловая
% система не поддерживает ссылки)
major_device, % Целое число: показывает файловую систему
% (в Unix) или номер устройства
% (A: = 0, B: = 1) (Windows)
Замечание: поля прав mode
и доступа access
перекрываются. Вы можете использовать права, чтобы установить несколько файловых атрибутов одной операцией. Впрочем, вы можете использовать access для простых операций.
Чтобы найти длину и тип файла мы вызываем функцию read_file_info
(заметьте, что нам приходится подключать file.hrl
, который содержит определение записи #file_info
):
Загрузить lib_misc.erl
-include_lib("kernel/include/file.hrl" ).
file_size_and_type(File) ->
case file:read_file_info(File) of
{ok, Facts} ->
{Facts\#file_info.type, Facts\#file_info.size};
_ ->
error
end.
Теперь можно слегка улучшить вид списка, выведенного функцией list_file
, добавив информацию о файлах в функции ls()
:
Загрузить lib_misc.erl
ls(Dir) ->
{ok, L} = file:list_dir(Dir),
map(fun(I) -> {I, file_size_and_type(I)} end, sort(L)).
Теперь список отсортирован и вдобавок содержит полезную информацию:
1> lib_misc:ls(".").
[{"Makefile",{regular,1244}},
{"README",{regular,1583}},
{"abc.erl",{regular,105}},
{"alloc_test.erl",{regular,303}},
...
{"socket_dist",{directory,4096}},
...
Дополнительное удобство в том, что модуль filelib
экспортирует несколько маленьких функций, таких как file_size(File)
и is_dir(X)
. Это просто интерфейсы к file:read_file_info
. Если нам надо всего лишь размер файла, то проще вызвать filelib:file_size
, чем file:read_file_info
и распаковывать элементы записи #file_info
.
Копирование и удаление файлов
file:copy(Source, Destination)
копирует файл Source в файл Destination
.
file:delete(File)
удаляет файл File
.
Всякая всячина
К текущему моменту мы упомянули ряд функций, которые я ежедневно использую для манипулирования файлами. И крайне редко мне приходится обращаться к документации за дополнительной информацией. Что же я пропустил такого, что может вам понадобиться? Я приведу краткий обзор основных вещей. А за подробными деталями обращайтесь к документации.
Режим файла: когда мы открываем файл функцией file:open
, мы открываем его в определённом режиме или комбинацией режимов. Вообще-то есть много разных режимов. К примеру, можно открыть файл на чтение и запись сжатого gzip файла. Ну и т. д. Полный список как обычно находится в
документации.
Время модификации, группы, ссылки: мы можем установить всё это функциями из file
.
Коды ошибок: я опрометчиво сказал, что у всех ошибок вид {error, Why}
. На самом деле Why
— это атом (к примеру, enoent
означает, что файл не существует и т. д.) - есть большое количество кодов ошибок и все они описаны в документации.
filename
: модуль filename
содержит некоторые полезные функции для сбора полных имён файлов и директорий, поиска расширений файлов и прочего, а также для построения имён файлов из компонентов пути. Всё это делается платформонезависимым образом.
fillib
: модуль filelib
содержит небольшое количество функций, которые помогают сэкономить нам время. Например, filelib:ensure_dir(Name)
обеспечивает, что все родительские директории для данного файла или директории существуют, создавая их при необходимости.
Программа поиска
И как финальный пример, мы используем file:list_dir
и file:read_file_info
для создания программы поиска общего назначения.
Главная точка входа в этот модуль следующая:
lib_find:files(Dir, RegExp, Recursive, Fun, Acc0)
Аргументы для неё:
Dir
— имя директории, откуда начинать поиск файла
RegExp
— регулярное выражение для проверки имени найденного файла. Если файлы, которые мы встретим, совпадают с этим регулярным выражением, то вызывается функция Fun(File, Acc)
, где File
— это имя файла, которое успешно сопоставлено с регулярным выражением.
Recursive = true | false
— это признак, который определяет будет ли поиск заходить в поддиректории текущей директории.
Fun(File, AccIn) -> AccOut
— это функция, которая применяется к файлу, если имя файла соответствует регулярному выражению RegExp
. Начальное значение аккумулятора Acc
— это Acc0
. Каждый раз, когда вызывается Fun
, она должна вернуть новое значение аккумулятора, которое будет передано в Fun при следующем вызове этого Fun
. Конечное значение аккумулятора — это значение, возвращаемое из функции lib_find:files/5
.
Мы можем передать в lib_find:files/5
любую функцию, какую только захотим. Например, мы можем построить список файлов, используя следующую функцию, передавая ей в начале пустой список:
fun(File, Acc) -> [File|Acc] end
Точка входа модуля lib_find:files(Dir, ShelRegExp, Flag)
обеспечивает упрощённый вызов для более общего использования программы. ShelRegExp
здесь — это упрощённое регулярное выражение, которое легче записать, чем полную форму регулярного выражения.
Пример такой короткой формы записи:
lib_find:files(Dir, "*.erl" , true)
рекурсивно ищет все файлы Эрланга, начиная с Dir
. Если бы последний аргумент был false
, то программа искала бы файлы Эрланга только в директории Dir
, но не спускалась в поддиректории.
Итак, код:
Загрузить lib_find.erl
-module(lib_find).
-export([files/3, files/5]).
-import(lists, [reverse/1]).
-include_lib("kernel/include/file.hrl" ).
files(Dir, Re, Flag) ->
Re1 = regexp:sh_to_awk(Re),
reverse(files(Dir, Re1, Flag, fun(File, Acc) ->[File|Acc] end, [])).
files(Dir, Reg, Recursive, Fun, Acc) ->
case file:list_dir(Dir) of
{ok, Files} -> find_files(Files, Dir, Reg, Recursive, Fun, Acc);
{error, _} -> Acc
end.
find_files([File|T], Dir, Reg, Recursive, Fun, Acc0) ->
FullName = filename:join([Dir,File]),
case file_type(FullName) of
regular ->
case regexp:match(FullName, Reg) of
{match, _, _} ->
Acc = Fun(FullName, Acc0),
find_files(T, Dir, Reg, Recursive, Fun, Acc);
_ ->
find_files(T, Dir, Reg, Recursive, Fun, Acc0)
end;
directory ->
case Recursive of
true ->
Acc1 = files(FullName, Reg, Recursive, Fun, Acc0),
find_files(T, Dir, Reg, Recursive, Fun, Acc1);
false ->
find_files(T, Dir, Reg, Recursive, Fun, Acc0)
end;
error ->
find_files(T, Dir, Reg, Recursive, Fun, Acc0)
end;
find_files([], _, _, _, _, A) ->
A.
file_type(File) ->
case file:read_file_info(File) of
{ok, Facts} ->
case Facts\#file_info.type of
regular -> regular;
directory -> directory;
_ -> error
end;
_ ->
error
end.