15 января 2015 г.

Не используйте относительные имена файлов

Доступ к файлу можно получить по абсолютному или относительному имени (пути).

Абсолютное ("полностью квалифицированное") имя начинается с имени диска или сервера и указывает все компоненты пути, например: "C:\Projects\TestProject\Data.txt" или "\\SERVER\Projects\TestProject\Data.txt". Такое имя всегда однозначно указывает на файл - вне зависимости от любых внешних факторов.

Относительное имя содержит не все компоненты пути и указывает файл относительно другого каталога, имя которого в самом имени не указано, например: "Data.txt" или "..\Data.txt". Для определения точного положения файла недостаточно одного относительного имени, необходимо ещё имя каталога, относительно которого будет трактоваться это имя. Поэтому один и тот же относительный путь может ссылаться на разные файлы. К примеру, путь "Data.txt" ссылается на C:\Projects\TestProject\Data.txt, если текущий каталог (или каталог, относительно которого происходит разрешение имени) равен C:\Projects\TestProject, но этот же путь будет ссылаться на C:\Windows\Data.txt, если текущий каталог - C:\Windows.

Подробнее о файловых именах можно почитать здесь.

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

Суть проблемы

Очень часто начинающие программисты используют относительные пути к файлам для работы с файлами внутри папки своей программы, например:
Assign(F, 'input.txt');
Reset(F);
// ...
Что не так с этим кодом?

Начинающий программист считает, что имя "input.txt" будет вычисляться относительно пути к его программе. Иными словами, если программа (.exe-файл) лежит в "C:\Projects\TestProject", то, указав "input.txt" в Assign, мы откроем файл "C:\Projects\TestProject\input.txt". Это попросту неверно!

Все относительные пути вычисляются относительно т.н. текущего каталога. Проблема состоит в том, что начинающие программисты не понимают, что это такое. Они считают, что текущий каталог - это каталог программы. Это не так:
  1. Для начала, текущий каталог при старте вашей программы задаётся не вами, а вызывающим вас процессом. Все функции запуска программы имеют строковый параметр для передачи туда имени каталога, который станет текущим для запускаемой программы. И вызывающая вас программа может передать туда всё, что угодно. Это может быть папка с вашей программой, да. Но это может быть и любая другая папка;
  2. Далее, к примеру, если ваша программа запускается через ярлык на рабочем столе или ярлык в меню Пуск / Программы, то текущий каталог для вашей программы указан в свойствах ярлыка. Например, сама Delphi запускается с текущим каталогом = папке с проектами (например, C:\Program Files\Borland\Delphi 7\Projects\ или даже просто C:\Projects\) - что, очевидно, не равно папке с программой (C:\Program Files\Borland\Delphi 7\Bin\);
  3. А если вы пишете программу, которая открывает файлы какого-то типа, то вы, вероятно, назначите свою программу для открытия таких файлов (ассоциируете тип файлов в вашей программой). Но когда пользователь дважды-щёлкнет по такому файлу, ваша программа запустится с текущим каталогом равным каталогу открываемого файла. Т.е. текущий каталог будет C:\Documents, а не C:\Projects\TestProject;
  4. А если вы пишете код для службы (Win32 Service), то текущий каталог будет C:\Windows\System32.
Итак, поскольку текущий каталог при запуске задаётся не вами, то он не находится под вашим контролем. А раз так, то вам не следует делать предположений о том, чем он может оказаться. Достаточно наивно будет думать, что текущий каталог при старте всегда будет равен каталогу с программой.
P.S. Кроме того, размещение файлов с данными/конфигурацией в папке с программой - крайне плохая идея, если только вы не пишете портативную (portable) программу (см. также).

Далее, есть же такие функции как GetCurrentDir и (что интереснее) SetCurrentDir. "Set" решительным образом намекает на то, что текущий каталог - вещь не фиксированная и его можно менять. Иными словами, текущий каталог не только может быть не равен каталогу программы, но и вообще может меняться в процессе выполнения программы! Действительно:
Assign(F, 'input.txt');
Reset(F); // <- OK, нет ошибки
// ...
SetCurrentDir('C:\Windows');
Assign(F, 'input.txt');
Reset(F); // <- возбуждает ошибку "файл не найден", поскольку файла C:\Windows\input.txt нет
// ...

Конечно, тот факт, что текущий каталог можно менять, сам по себе ещё не означает проблему. Но посмотрите на такой код:
if not OpenDialog1.Execute then
  Exit;
OutputFileName := OpenDialog1.FileName;

Assign(F, 'input.txt');
Reset(F); // <- возбуждает ошибку "файл не найден"
// ...
Что случилось? Дело в том, что внешний код (а именно - код диалога открытия файла) поменял текущий каталог. Ваш код оказался не готов к этому.
P.S. Почему вообще диалог открытия файла меняет текущий каталог? Потому что вы (= прикладные программисты) пишете код с использованием относительных имён файлов.
Иными словами, проблема состоит в том, что кто угодно может менять текущий каталог в любой момент времени. Даже если в вашем коде нет вызовов другого внешнего кода, который меняет текущий каталог, всё равно текущий каталог может быть изменён другим потоком в вашей программе. Даже если вы сами не создаёте других потоков, потоки могут быть созданы DLL, которые загружены в вашу программу. Помимо системных DLL, это могут быть DLL от любых оконных ловушек, расширителей оболочек и даже антивирусов.

Вывод? Код, который адресует файл относительным именем работает благодаря случайности, а именно: благодаря тому, что никакой другой код не изменил текущий каталог перед тем, как вы вызвали функцию доступа к файлу, передав ей относительное имя файла.

Неправильные решения

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

Многие либо первым действием в программе, либо непосредственно перед выполнением участка кода с использованием относительных имён явно меняют текущий каталог на каталог программы. Например:
AppPath := ExtractFilePath(ParamStr(0));
SetCurrentDir(AppPath); // AppPath = 'C:\Projects\TestProject\'
Assign(F, 'input.txt');
Reset(F); // <- OK, нет ошибки
// ...
Или же сохраняют/восстанавливают текущий каталог перед вызовом кода, который потенциально может менять текущий каталог, например:
CurrentDir := GetCurrentDir;
if not OpenDialog1.Execute then
  Exit;
OutputFileName := OpenDialog1.FileName;
SetCurrentDir(CurrentDir);
Assign(F, 'input.txt');
Reset(F); // <- OK, нет ошибки
// ...

Что не так с этими решениями?

Во-первых, текущий каталог, являясь глобальной переменной, может быть изменён другим потоком как раз между вызовами SetCurrentDir и Assign. Если этого не происходит, то ваш (некорректный) код работает благодаря случайности (случайность состоит в том, что этого не произошло).

Во-вторых, если вы форсированно устанавливаете свой текущий каталог, отбрасывая каталог, заданный вам вызывающим, вы можете открыть не тот файл! Например, пусть вы пишете программу-конвертер, пусть она конвертирует картинки из формата .png в формат .jpg, пусть вашу программу можно вызвать, передав ей имя файла в командной строке. Тогда пользователь может вызвать вас так:
C:\Documents>"C:\Converter\convert.exe" "holidays.png"
(здесь C:\Documents> является приглашением командной строки, а "C:\Converter\convert.exe" "holidays.png" - непосредственно командной строкой).

Т.е. пользователь открыл консоль, он находится в папке C:\Documents и вызывает вас (convert.exe) из папки C:\Converter, передавая вам имя файла (holidays.png) параметром командной строки.

(Почти аналогичная ситуация будет если пользователь дважды-щёлкнет по файлу holidays.png в открытой папке C:\Documents в Проводнике - при условии, что ваша программа ассоциирована с .png файлами).

В этом случае ваша программа (convert.exe) запустится из папки C:\Converter, но текущим каталогом для неё будет C:\Documents. Если вы форсированно смените текущий каталог на папку с программой (C:\Converter), то не сможете открыть файл holidays.png, поскольку он находится в папке C:\Documents, а не C:\Converter.

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

Имя файла вам могли передать в относительном формате (к примеру, вызвали вас из командной строки). Вы передали это имя "как есть" в первую копию своей программы. Однако текущий каталог первой копии равен неизвестно чему, он не равен текущему каталогу вашей второй копии. Как бы вы не меняли текущий каталог в первой копии программы перед попыткой доступа к файлу, который вам передали, вы никак не можете знать, каким же был текущий каталог в другой программе (вашей второй копии). И снова, ваш код, оперирующий относительными путями файлов, будет работать только благодаря случайности (в этом случае случайность состоит в том, что текущий каталог оказался одинаков в обоих экземплярах вашей программы).

Более того, когда вы меняете текущий каталог на какой-то - этот каталог открывается вашей программой. В частности, это означает, что вы не можете удалить этот каталог.

Зачем нужны относительные пути?

Зачем же вообще относительные пути, если они так плохи?

Ну, относительные пути нужны для человека-оператора. Они экономят время на набор текста. Действительно, вместо того, чтобы вводить длинный путь вида C:\Documents and Settings\Admin\Documents\Data from 2014\March.doc - вы можете ввести просто March.doc (конечно же, при условии, что вы "находитесь" в каталоге C:\Documents and Settings\Admin\Documents\Data from 2014\).

Правильное решение

Из вышесказанного напрямую следует правильное решение. Раз относительные имена предназначены для человека, то вам (вашему коду) не следует их использовать. Всё, что вы можете сделать с относительным именем - перевести его в абсолютное. И оперировать в дальнейшем только абсолютным именем файла.

Для этого вам понадобится функция нормализации внешних данных, которая преобразует относительное имя файла, принятое от пользователя, в абсолютное, пригодное для использования в коде программы. Как минимум, функция нормализации должна преобразовывать относительное имя файла в абсолютное. Но помимо этого желательно также выполнить и другие операции. Вот конкретный список действий:
  1. Развернуть переменные окружения;
  2. Преобразовать относительное имя в абсолютное;
  3. Канонизировать путь, свернув '.', '..' и лишние разделители каталогов;
  4. Преобразовать короткий путь в длинный.
Как реализовать такую функцию нормализации - мы посмотрим ниже, в разделе практики.

А пока давайте сформулируем свод правил, которому вам стоит придерживаться:
  1. Каждый раз, когда вы получаете имя файла из внешнего источника (командной строки, конфигурации, диалога и т.п.) - сохраняйте его в переменную с суффиксом Unsafe. Например, DocumentFileNameUnsafe := ParamStr(1);
  2. Не передавайте переменные с суффиксом Unsafe в функции открытия файлов;
  3. Не передавайте переменные с суффиксом Unsafe в другие программы (через IPC, командную строку и т.п.);
  4. Вы можете сохранять переменные с суффиксом Unsafe в файл конфигурации;
  5. Передайте переменную с суффиксом Unsafe в функцию нормализации и сохраните результат в переменной без суффикса Unsafe. Например, DocumentFileName := PathSearchAndQualify(DocumentFileNameUnsafe);
  6. Вы можете передавать переменные без суффикса Unsafe в функции открытия файлов и другие программы (IPC, командная строка и т.п.);
  7. Если вам точно известно имя файла (оно задано константой в коде) и вы знаете папку, в которой лежит файл (не обязательно константа, но хотя бы логическое размещение вида "каталог программы", "подкаталог ABC папки Application Data"), то получите путь к каталогу, затем добавьте к нему имя файла и сохраните результат в переменную без суффикса Unsafe. Если имя файла, заданное в константе, содержит '.' или '..' - выполните нормализацию перед сохранением в переменную;
  8. Измените текущий каталог на, скажем, C:\Windows\System32 (разумеется, путь надо задавать не константой, а получать через GetSystemDirectory) сразу после того, как вы нормализовали все имена файлов из параметров командной строки;
  9. Если вы запускаете внешнюю программу, передавая ей имя файла для открытия, то задайте текущий каталог для запускаемой программы равным каталогу, содержащему открываемый файл (даже хотя вы передаёте полное имя файла);
  10. Используйте суффикс Dir для переменных и функций, которые хранят/возвращают путь (к каталогу) без ведомого разделителя (например, 'C:\Windows'). Используйте суффикс Path для переменных и функций, которые хранят/возвращают путь с ведомым разделителем (например, 'C:\Windows\'). Избегайте использования переменных и функций, для которых вы не знаете, будет ли в конце пути разделитель. Преобразуйте такие переменные и функции в Dir или Path с помощью ExcludeTrailingPathDelimiter и IncludeTrailingPathDelimiter соответственно, например: CurrentPath := IncludeTrailingPathDelimiter(GetCurrentFolder). Эта семантика с Dir/Path защитит вас от неверных результатов вида ExtractFileDir(...) + 'input.txt' = 'C:\Programinput.txt' или ExtractFilePath(...) + '\input.txt' = 'C:\Program\\input.txt'.
  11. Старайтесь хранить имена каталогов с ведущим разделителем, а имена файлов - без разделителя. Например, 'C:\Windows\', но 'C:\Windows\notepad.exe';
  12. Если вы работаете в Unicode-версии Delphi (Delphi 2009 и выше) и хотите передать имя файла во внешний код (программу или DLL) - преобразуйте имя файла в короткое имя файла (PathGetShortPath - см. ниже). Это увеличит шансы правильного открытия файла, если вызываемый код не поддерживает Unicode или неверно обрабатывает пробелы;
  13. Если вы передаёте имя файла по IPC - всегда предпочитайте Unicode-форму (используйте WideString).

Тем не менее, существует один случай, когда вам нужно использовать в своём коде относительные пути. Речь идёт о сохранении путей в "конфигурацию" относительно некого корневого элемента. Например, это может быть портативная (portable) программа, сохраняющая пути к файлам, относительно каталога с программой. Или это может быть многофайловый документ. Например, проект Delphi сохраняется в .dpr файл (а его настройки - в .dproj файл в том же каталоге). При этом настройки проекта Delphi сохраняют пути до файлов проекта в относительной форме - пути рассчитываются относительно каталога с .dpr/.dproj.

И если у вас возникает аналогичная ситуация, то действовать следует так:
  1. Проведите нормализацию имён файлов, как указано в алгоритме выше, сохранив их в переменные без суффикса Unsafe;
  2. Получите каталог, относительно которого вам нужно сохранять пути (каталог с программой, каталог с корневым файлом документа и т.п.). Нормализуйте его и сохраните в переменную без суффикса Unsafe;
  3. Получите относительный путь для вашего пути файла из п1 относительно каталога из п2 с помощью функции PathGetRelativePath (см. ниже раздел практики). Сохраните результат в переменную с префиксом Unsafe;
  4. Запишите переменную из п3 в вашу конфигурацию или документ.

Практические примеры

Давайте посмотрим, как эти рекомендации нужно делать на примерах.

Реализация: голая Delphi

Во-первых, даже в Delphi "из коробки" есть несколько подходящих функций (некоторые функции могут отсутствовать в старых версиях Delphi):
{ ExpandFileName expands the given filename to a fully qualified filename.
  The resulting string consists of a drive letter, a colon, a root relative
  directory path, and a filename. Embedded '.' and '..' directory references
  are removed. }

function ExpandFileName(const FileName: string): string; overload;

{ ExpandUNCFileName expands the given filename to a fully qualified filename.
  This function is the same as ExpandFileName except that it will return the
  drive portion of the filename in the format '\\servername\sharename if
  that drive is actually a network resource instead of a local resource.
  Like ExpandFileName, embedded '.' and '..' directory references are
  removed. }

function ExpandUNCFileName(const FileName: string): string; overload;

{ ExtractRelativePath will return a file path name relative to the given
  BaseName.  It strips the common path dirs and adds '..\' on Windows,
  and '../' on Linux for each level up from the BaseName path. Note: Directories
  passed in should include trailing backslashes}

function ExtractRelativePath(const BaseName, DestName: string): string; overload;

{ IsRelativePath returns a boolean value that indicates whether the specified
  path is a relative path. }

function IsRelativePath(const Path: string): Boolean;

{ ExtractShortPathName will convert the given filename to the short form
  by calling the GetShortPathName API.  Will return an empty string if
  the file or directory specified does not exist }

function ExtractShortPathName(const FileName: string): string;

Реализация: JCL

Во-вторых, хочу заметить, что если вы используете JCL (JEDI Code Library), то весь код у вас уже есть - в файле JclFileUtils:
// Возвращает длинное имя по короткому
function PathGetLongName(const APath: string): string;

// Возвращает короткое имя по длинному
function PathGetShortName(const APath: string): string;

// Конвертирует абсолютное имя в относительное
function PathGetRelativePath(const AOrigin, ADestination: string): string;

// Канонизирует путь, удаляя из него специальные каталоги '.' и '..'
function PathCanonicalize(const APath): string;

// Возвращает True, если путь - абсолютный
function PathIsAbsolute(const APath: string): Boolean;

// Возвращает True, если путь ABase содержится в APath
function PathIsChild(const APath, ABase: string): Boolean;
function PathIsEqualOrChild(const APath, ABase: string): Boolean;

// Возвращает True, если путь начинается с диска
function PathIsDiskDevice(const APath: string): Boolean;

// Возвращает True, если путь начинается с имени сервера
function PathIsUNC(const APath: string): Boolean;
И хотя здесь нет PathGetAbsolutePath/ExpandFileName, но эта функция тривиальна:
// Конвертирует относительное имя в абсолютное
function PathGetAbsolutePath(const APath: string; const ABase: string = ''): string;
var
  BaseDir: string;
begin
  if PathIsAbsolute(APath) then
  begin
    Result := APath;
    Exit;
  end;

  if ABase = '' then
    BaseDir := ExcludeTrailingPathDelimiter(GetCurrentDir)
  else
    BaseDir := ExcludeTrailingPathDelimiter(PathGetAbsolutePath(ExcludeTrailingPathDelimiter(ABase)));

  if APath = '' then
    Result := BaseDir
  else
  if APath[1] = PathDelim then
    Result := BaseDir + APath
  else
    Result := IncludeTrailingPathDelimiter(BaseDir) + APath;
end;

Реализация: системные функции

В-третьих, я также предлагаю вам воспользоваться функциями системы. Заметьте, что хотя эти функции относятся к функциям Оболочки (Shell), они также относятся к т.н. группе "легковесных вспомогательных функций" (Shell Lightweight Utility Functions) и импортируются из ShlwAPI.dll, а не из ShellAPI.dll. В частности, это означает, что у них нет тяжёлых зависимостей и им не нужен COM - в отличие от высококоуровневых функций Оболочки.

И для этого я предлагаю создать отдельный модуль (File / New / Other / Unit) и сохранить его, скажем, с именем ShellFileSupport.pas. В этот модуль мы поместим весь вспомогательный код. Сложные функции мы будем импортировать из системы, а простые функции напишем сами. Вот какие функции мы реализуем:
type
  TDriveNumber    = 0..25;
  TPathCharType   = (gctInvalid, gctLFNChar, gctSeparator, gctShortChar, gctWild);
  TPathCharTypes  = set of TPathCharType;
  TCleanupResult  = (pcsReplacedChar, pcsRemovedChar, pcsTruncated);
  TCleanupResults = set of TCleanupResult;
  PCleanupResults = ^TCleanupResults;

const
  InvalidDrive = TDriveNumber(-1);

// Возвращает тип символа из пути
function PathGetCharType(const AChar: Char): TPathCharTypes;

// Возвращает номер диска из пути (InvalidDrive при ошибке)
function PathGetDriveNumber(const APath: String): TDriveNumber;

// Формирует путь к корневому каталогу заданного диска
function PathBuildRoot(const ADrive: TDriveNumber): String;

// Канонизирует путь, удаляя из него специальные каталоги '.' и '..'
function PathCanonicalize(const APath: String): String;

// Соединяет два пути, добавляя, при необходимости, разделитель пути
function PathAppend(const APath, AMore: String): String;

// Аналог PathAppend, но возвращает каноничный путь (с удалёнными '.' и '..')
function PathCombine(const APath, AMore: String): String;

// Возвращает True, если указанный путь (файл/каталог) существует
// Реализуем на случай, если вы не хотите использовать бажный FileExists/DirectoryExists из Delphi
// См.
// http://qc.embarcadero.com/wc/qcmain.aspx?d=3513
// http://qc.embarcadero.com/wc/qcmain.aspx?d=10731
// http://qc.embarcadero.com/wc/qcmain.aspx?d=52905
function PathFileExists(const APath: String): Boolean; // включает в себя и файл и каталог
function PathIsDirectory(const APath: String): Boolean;

// Возвращает True, если путь не содержит разделителей пути (':' и '\')
function PathIsFileSpec(const APath: String): Boolean;

// Возвращает True, если путь - относительный
function PathIsRelative(const APath: String): Boolean;

// Возвращает True, если путь - абсолютный
function PathIsAbsolute(const APath: String): Boolean;

// Заключает строку в кавычки при необходимости (наличие пробелов)
function PathQuoteSpaces(const APath: String; const AForce: Boolean = False): String;

// Формирует относительный путь к ATo из (относительно) AFrom (ведомый '\' обозначает каталог)
function PathRelativePathTo(const AFrom, ATo: String): String;

// Разрешает относительное имя в абсолютное, дополнительно канонизируя путь
function PathSearchAndQualify(const APath: String): String;

// Возвращает короткое имя по длинному
function PathGetShortPath(const APath: String): String;

// Возвращает длинное имя по короткому
function PathGetLFNPath(const APath: String): String;

// Возвращает True, если путь - допустим
function PathIsValid(const APath: String): Boolean;

// Создаёт командную строку для запуска программы. Результат этой функции можно передавать в CreateProcess
function PathProcessCommand(const AProgram: String; const AParameters: array of String): String;
Заметьте, что некоторые функции мы реализуем сами, поэтому они работают несколько иначе, чем системные. Кроме того, для некоторых функций мы добавляем дополнительный функционал. Всё это сделано для того, чтобы упростить использование функций. Дело в том, что эти функции несколько узко-специализированы. Например, системный PathQuoteSpaces не обрабатывает кавычки внутри строки, а PathCanonicalize не преобразует некорректные разделители каталогов. Поэтому в своих функциях мы дополнительно исправляем эти упущения.

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

P.S. К сожалению, Delphi не поддерживает передачу кавычек в параметрах командной строки. В этом случае функции PathQuoteSpaces и PathProcessCommand используют семантику C runtime: кавычки внутри командной строки защищаются символом '\'. Но в любом случае такие параметры нельзя будет прочитать внутри Delphi-программы, если только вы не реализуете свой собственный разбор командной строки. Но зато их может прочитать другая программа, которая использует CommandLineToArgvW.

Реализации: вывод

Так или иначе, у вас есть богатый выбор: вы можете использовать встроенные функции, свои собственные полностью реализованные функции (взяв готовые из библиотеки JCL или написав свои) или же вы можете использовать системные функции, импортировав их из ShlwAPI.dll.

Вот как может выглядеть "функция нормализации", упомянутая выше, в разделе "Правильное решение":
// Delphi
function SafePath(const APath: String): String;
begin
  Result := ExpandFileName(APath);
end;
// JCL (JclFileUtils + JclSysInfo)
function SafePath(const APath: String): String;
var
  Path: String;
begin
  Path := APath;
  ExpandEnvironmentVar(Path);
  Result := PathGetLongName(PathCanonicalize(PathGetAbsolutePath(Path)));
end;
// Shell
function SafePath(const APath: String): String;
begin
  Result := PathGetLFNPath(PathSearchAndQualify(APath));
end;
А вот также упомянутая функция создания относительного пути:
// Delphi
function UnsafePath(const ARootPath, ATarget: String): String;
begin
  Result := ExtractRelativePath(ARootPath, ATarget);
end;
// JCL (JclFileUtils + JclSysInfo)
function UnsafePath(const ARootPath, ATarget: String): String;
begin
  Result := PathGetRelativePath(ARootPath, ATarget);
end;
// Shell
function UnsafePath(const ARootPath, ATargetPathUnsafe: String): String;
begin
  Result := PathRelativePathTo(ARootPath, ATarget);
end;
Здесь ARootPath - каталог с программой, документом и т.п., относительно которого нужно создать путь. ATarget - имя файла, которое нужно сохранить в конфигурацию, документ и т.п. Result - собственно результат, который нужно сохранить. Например:
PathUnsafe := UnsafePath('C:\Windows\System32\', 'C:\Windows\Temp\input.txt');
// PathUnsafe = '..\Temp\input.txt'

Да, вам также понадобится функция, чтобы "увести" текущий каталог на безопасное место. Вот подходящий код:
function GetWindowsSystemPath: String;
var
  Required: Cardinal;
begin
  Result := '';
  Required := GetSystemDirectory(nil, 0);
  if Required <> 0 then
  begin
    SetLength(Result, Required);
    SetLength(Result, GetSystemDirectory(PChar(Result), Required);
  end;

  if Result <> '' then
    Result := IncludeTrailingPathDelimiter(Result);
end;

procedure ResetCurrentDirectory;
begin
  SetCurrentDir(GetWindowsSystemPath);
end;

Примеры кода

Открываем файл в известной папке

Неправильный код:
// ВНИМАНИЕ: код ниже не корректен
Assign(F, 'input.txt');
Reset(F);
// ...
А вот правильный вариант этого кода:
FileName := ExtractFilePath(ParamStr(0)) + 'input.txt';
AssignFile(F, FileName);
Reset(F);
// ...
или:
FileName := GetApplicationDataPath + 'MyApplication\input.txt';
ForceDirectories(ExtractFilePath(FileName));
AssignFile(F, FileName);
Reset(F);
// ...
Где GetApplicationDataPath - ваша функция получения пути к папке Application Data (подробнее).

Читаем имя файла из командной строки

Неправильный код:
// ВНИМАНИЕ: код ниже не корректен
function ProcessCommandLine: Boolean;
begin
  Result := (ParamCount > 0);
  if not Result then
    Exit;

  OpenDocument(ParamStr(1));
end;
А вот правильный вариант этого кода:
function ProcessCommandLine: Boolean;
var
  FileName: String;
  FileNameUnsafe: String;
begin
  Result := (ParamCount > 0);
  if not Result then
    Exit;

  FileNameUnsafe := ParamStr(1);
  FileName := SafePath(FileNameUnsafe);

  OpenDocument(FileName);

  ResetCurrentDirectory;
end;
Здесь OpenDocument - какая-то ваша функция открытия файла (например, загрузить .png файл и показать его на форме). Предполагается, что вы вызовете ProcessCommandLine при запуске программы, например:
procedure TMainForm.FormCreate(Sender: TObject);
begin
  if not ProcessCommandLine then
    ClearDocument; // тоже какая-то ваша функция (если надо)
end;
(в примере выше ProcessCommandLine удобно сделать методом главной формы)

Просмотрщик: передаём имя файла в первый экземпляр программы

Неправильный код:
// ВНИМАНИЕ: код ниже не корректен
function ProcessCommandLine: Boolean;
begin
  Result := (ParamCount > 0);
  if not Result then
    Exit;

  if PassFileToFirstInstance(ParamStr(1)) then
    TerminateProcess(GetCurrentProcess, 0)
  else
    OpenDocument(ParamStr(1));
end;
А вот правильный вариант этого кода:
function ProcessCommandLine: Boolean;
var
  FileName: String;
  FileNameUnsafe: String;
begin
  Result := (ParamCount > 0);
  if not Result then
    Exit;

  FileNameUnsafe := ParamStr(1);
  FileName := SafePath(FileNameUnsafe);

  if PassFileToFirstInstance(FileName) then
    TerminateProcess(GetCurrentProcess, 0)
  else
    OpenDocument(FileName);

  ResetCurrentDirectory;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  if not ProcessCommandLine then
    ClearDocument; 
end;
Здесь PassFileToFirstInstance - ваша функция, которая проверяет, запущена ли уже ваша программа, и если да, то передаёт ей имя файла. Она может выглядеть как-то так (показаны фрагменты):
type
  PIPCSharedData = ^TIPCSharedData;
  TIPCSharedData = packed record
    MainProcessPID: DWord;
    MainProcessWND: HWND;
  end;

const
  IPCName = '28A68ADBB80941648FDABF83E6EC5E7E'; 
  замените на своё значение! (Ctrl + Shift + G в редакторе кода)

function PassFileToFirstInstance(const AFileName: WideString): Boolean;
// ...
begin
  SharedMemHandle := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0, SizeOf(TIPCSharedData), IPCName);
  Win32Check(SharedMemHandle <> 0);
  if ((SharedMemHandle <> 0) and (GetLastError = ERROR_ALREADY_EXISTS)) then
  begin
    P := MapViewOfFile(SharedMemHandle, FILE_SHARE_READ, 0, 0, 0);
    Win32Check(P <> nil);
    try
      // ...

      MPWnd := P^.MainProcessWND;
      MPPID := P^.MainProcessPID;

      if MPWnd <> 0 then
      begin
        AllowSetForegroundWindow := GetProcAddress(GetModuleHandle('user32.dll'), 'AllowSetForegroundWindow');
        if Assigned(AllowSetForegroundWindow) then
          AllowSetForegroundWindow(MPPID);

        FillChar(CDP, SizeOf(CDP), 0);
        CDP.dwData := 1;
        CDP.lpData := Pointer(AFileName);
        CDP.cbData := Length(AFileName) * SizeOf(AFileName[1]);
        Result := (SendMessage(MPWnd, WM_COPYDATA, 0, Integer(@CDP)) > 0);
      end;

      // ...
    finally
      UnmapViewOfFile(P);
    end;
  // ...
end;

14 комментариев :

  1. и не лень, тебе шизику, такое выдумывать?
    я люблю шмар жарить, потому что они огонь!

    ОтветитьУдалить
    Ответы
    1. и не лень, тебе жарителю шмар, такое читать?

      Удалить
  2. При открытии программы нужно копировать путь ExtractFilePath(ParamStr(0)) или ExtractFileDir(Application.ExeName) в переменную и везде подставлять к пути файлов

    ОтветитьУдалить
    Ответы
    1. +1
      а помимо этого еще и GetCurrentDIr сохранять в переменной при старте. И подставлять там, где нужно открыть файл относительно текущей папки на момент запуска. (как защиту от диалогов)

      Удалить
  3. Я у себя весь код по работе с папками обычно выношу в отдельный класс, с методами для получения имён файлов и папок.
    Примерно так:

    type
    TLazyDirs = class
    strict private
    FTempDirs: TStringList;
    FStartupDirectory: string;
    public
    class function GetUserSettingsFolderOld: string;
    class function GetUserSettingsFolder2: string;

    class function GetBuildOptionsPresetsFileName: string;
    class function GetCustomDelphiInstallationsPresetsFileName: string;
    ...

    class function GetLazyBuilderIniFilename: string;
    class function GetDefaultLazyProfileFilename: string;

    class function GetFormSizesFilename: string;

    function GenNewUnexistingTempDirName(const aDeleteDirOnAppExit: Boolean): string;

    procedure DeleteAllCreatedTempDirs;

    constructor Create; virtual;
    destructor Destroy; override;
    end;

    Помимо группировки, это еще и упрощает автоматическую миграцию настроек (считали из старого места, сохранили в новое).

    ОтветитьУдалить
  4. Это реальный бред сивой кобылы. Профилирование путей из покон веков делается относительными CurDir по причине расширения программы относительно пользователей и методов старта. Например если одним компьютером пользуется несколько пользователей и exe файл храниться в C:\Program Files\MyApp\myapp.exe то неужели все пользователи должны получать конфиг из Program Files? А как писать конфиг пользователю у которого нет и не будет Admin Rights? Во первых это экономия char* буффера, во вторых программа не обязана работать только с одними и теми-же файлами. В третьих программа ДОЛЖНА ЗНАТЬ свою папку запуска в любом случае. При правильном чтении/установке текущего пути никаких таких косяков быть не должно.

    Для примера скажу, я работаю в компании где работает около 1400 сотрудников, из них около 700 пользуется так называемым TeamServer через Remote Desktop Protocol. Некоторый из нашего софта определяет текущую папку запуска, и профилирует настройки и доступы относительно текущего пользователя, а не засирает папки общего назначения. Это помогает системным администраторам правильно удалять старые файлы пользователей. Aleksey Timohin правильно написал - НУЖНО ИСПОЛЬЗОВАТЬ directory helper. который бы правильно знал где, и что лежит.

    ОтветитьУдалить
    Ответы
    1. И чем же использование полного абсолютного пути мешает адресовать файл, скажем, в AppData?

      Удалить
  5. Мсье знает толк... то кричит про устаревшие функции, то спокойно приводит овердохрена кода с использованием Assign и Reset. Может быть стоит слезть с палёной Delphi7 ?

    ОтветитьУдалить
    Ответы
    1. Слова "...начинающие программисты используют..." никаких мыслей в голове не вызывают? :)

      Удалить
  6. После фразы "P.S. Кроме того, размещение файлов с данными/конфигурацией в папке с программой - крайне плохая идея" стало понятно откуда берутся такие горе-программисты, которые засирают AppData своей фигней (и которая, конечно же, после анинсталла проги там и остается).
    Нормальные люди определяют путь к екзешнику, имя юзера и оппа! файл вида "конфиг_юзернэйм.***" в каталоге с программой готов.

    ОтветитьУдалить
    Ответы
    1. Рекомендую дальше "учить" писать подобное. Потому что, когда приезжаешь на вызов по поводу "помогите нет места на диске" и видишь засранный остатками "чудо"-программам от "чудо"-программистов каталог AppData - душа аж радуется. 5 минут на чистку, 55 минут на глубокомысленное сидение за компом, оплата - за час работы.

      Удалить
    2. Соглашусь. Идеи AppData и реестра были бы хорошими, если бы система хоть как-то следила за соответствиями между программами и их данными (да вообще внесёнными в систему изменениями). В Андроиде и в браузерах с этим чуть строже.

      В этом блоге (по крайней мере, в переводах Чена) пару раз проскакивала мысль, что это в порядке вещей, «для удобства пользователя» сохранить некую информацию о программе перманентно, чтобы при переустановке она могла сказать «оп-па, я уже всё знаю» и перенять прежние настройки. Может быть, в идеальном мире это и работает. Но с тем бардаком, что есть, возможность удалить программу полностью и навсегда (в т. ч. переустановить с нуля) мне кажется более приоритетной. Некоторые предоставляют импорт-экспорт настроек самостоятельно.

      Удалить
  7. Поддерживаю. И сам уже наелся с относительными путями (после чего вытравливаю везде), и коллеги до сих пор мучаются. Текущая директория - глобальная переменная аж в рамках системы, поэтому рекомендация ее НЕ применять на порядок сильнее, чем касательно глобальных переменных.

    Единственное замечание - по поводу расширения переданных в параметрах путей. Мне кажется, что здесь произвол программы неуместен и вызывающий должен получить то, что заказывал. Как пример:

    cd c:\mydocs
    megaeditor.exe readme.txt

    А megaeditor расширяет readme.txt до своей папки и либо ругается на отсутствие файла, либо открывает не то.

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.