Традиционные пути DOS
Стандартный путь DOS может состоять из трех компонентов:
- Буква тома или диска, после которой следует разделитель томов (
:
). - Имя каталога. Символ разделителя каталогов служит для разделения подкаталогов во внутренней иерархии каталога.
- Необязательное имя файла. Символ разделителя каталогов служит для разделения пути к файлу и его имени.
Если присутствуют все три компонента, путь является абсолютным. Если буква тома или диска не указана и имя каталога начинается с символа разделителя каталогов, такой путь задан относительно корня текущего диска. В противном случае путь задан относительно текущего каталога. В следующей таблице показаны некоторые возможные пути к каталогам и файлам.
Путь | Описание: |
---|---|
C:\Documents\Newsletters\Summer2018.pdf |
Абсолютный путь к файлу из корня диска C: . |
\Program Files\Custom Utilities\StringFinder.exe |
Относительный путь из корня текущего диска. |
2018\January.xlsx |
Относительный путь к файлу в подкаталоге текущего каталога. |
..\Publications\TravelBrochure.pdf |
Относительный путь к файлу в каталоге, начиная с текущего каталога. |
C:\Projects\apilibrary\apilibrary.sln |
Абсолютный путь к файлу из корня диска C: . |
C:Projects\apilibrary\apilibrary.sln |
Относительный путь из текущего каталога диска C: . |
Важно!
Обратите внимание на различия между двумя последними путями. В обоих случаях задается необязательный описатель тома (C:
), однако первый путь, в отличие от второго, начинается с корня указанного тома. В результате первый путь является абсолютным из корневого каталога диска C:
, тогда как второй — относительным из текущего каталога C:
. Использование второй формы пути в тех случаях, когда предполагается наличие первой, является распространенным источником ошибок, связанных с путями к файлам в Windows.
Можно определить, является ли путь к файлу полным (то есть, если путь не зависит от текущего каталога и не изменяется при изменении текущего Path.IsPathFullyQualified каталога), вызвав метод . Обратите внимание, что такой путь может включать сегменты с относительным путем к каталогу (.
и ..
), но при этом по-прежнему будет полным, если разрешенный путь всегда указывает на одно и то же место.
В приведенном ниже примере показано различие между абсолютными и относительными путями. Предполагается, что каталог D:\FY2018\
существует и вы не установили какой-либо текущий каталог для диска D:\
из командной строки перед запуском этого примера.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
public class Example
{
public static void Main(string[] args)
{
Console.WriteLine($"Current directory is '{Environment.CurrentDirectory}'");
Console.WriteLine("Setting current directory to 'C:\\'");
Directory.SetCurrentDirectory(@"C:\");
string path = Path.GetFullPath(@"D:\FY2018");
Console.WriteLine($"'D:\\FY2018' resolves to {path}");
path = Path.GetFullPath(@"D:FY2018");
Console.WriteLine($"'D:FY2018' resolves to {path}");
Console.WriteLine("Setting current directory to 'D:\\Docs'");
Directory.SetCurrentDirectory(@"D:\Docs");
path = Path.GetFullPath(@"D:\FY2018");
Console.WriteLine($"'D:\\FY2018' resolves to {path}");
path = Path.GetFullPath(@"D:FY2018");
// This will be "D:\Docs\FY2018" as it happens to match the drive of the current directory
Console.WriteLine($"'D:FY2018' resolves to {path}");
Console.WriteLine("Setting current directory to 'C:\\'");
Directory.SetCurrentDirectory(@"C:\");
path = Path.GetFullPath(@"D:\FY2018");
Console.WriteLine($"'D:\\FY2018' resolves to {path}");
// This will be either "D:\FY2018" or "D:\FY2018\FY2018" in the subprocess. In the sub process,
// the command prompt set the current directory before launch of our application, which
// sets a hidden environment variable that is considered.
path = Path.GetFullPath(@"D:FY2018");
Console.WriteLine($"'D:FY2018' resolves to {path}");
if (args.Length < 1)
{
Console.WriteLine(@"Launching again, after setting current directory to D:\FY2018");
Uri currentExe = new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase, UriKind.Absolute);
string commandLine = $"/C cd D:\\FY2018 & \"{currentExe.LocalPath}\" stop";
ProcessStartInfo psi = new ProcessStartInfo("cmd", commandLine); ;
Process.Start(psi).WaitForExit();
Console.WriteLine("Sub process returned:");
path = Path.GetFullPath(@"D:\FY2018");
Console.WriteLine($"'D:\\FY2018' resolves to {path}");
path = Path.GetFullPath(@"D:FY2018");
Console.WriteLine($"'D:FY2018' resolves to {path}");
}
Console.WriteLine("Press any key to continue... ");
Console.ReadKey();
}
}
// The example displays the following output:
// Current directory is 'C:\Programs\file-paths'
// Setting current directory to 'C:\'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to d:\FY2018
// Setting current directory to 'D:\Docs'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to D:\Docs\FY2018
// Setting current directory to 'C:\'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to d:\FY2018
// Launching again, after setting current directory to D:\FY2018
// Sub process returned:
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to d:\FY2018
// The subprocess displays the following output:
// Current directory is 'C:\'
// Setting current directory to 'C:\'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to D:\FY2018\FY2018
// Setting current directory to 'D:\Docs'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to D:\Docs\FY2018
// Setting current directory to 'C:\'
// 'D:\FY2018' resolves to D:\FY2018
// 'D:FY2018' resolves to D:\FY2018\FY2018
UNC-пути
UNC-пути (универсальное соглашение об именовании) используются для доступа к сетевым ресурсам и имеют следующий формат:
- Имя сервера или узла, которому предшествуют символы
\\
. В качестве имени сервера может выступать имя компьютера NetBIOS, а также IP-адрес или полное доменное имя (поддерживаются адреса IPv4 и IPv6). - Имя общего ресурса, которое отделяется от имени узла символами
\
. Имя сервера и имя общего ресурса в совокупности образуют том. - Имя каталога. Символ разделителя каталогов служит для разделения подкаталогов во внутренней иерархии каталога.
- Необязательное имя файла. Символ разделителя каталогов служит для разделения пути к файлу и его имени.
Ниже приводятся некоторые примеры UNC-путей:
Path | Описание |
---|---|
\\system07\C$\ |
Корневой каталог диска C: на компьютере system07 . |
\\Server2\Share\Test\Foo.txt |
Файл Foo.txt в тестовом каталоге тома \\Server2\Share . |
UNC-пути всегда должны быть полными. Они могут включать сегменты с относительным путем к каталогу (.
и ..
), однако они должны быть частью полного пути. Использовать относительные пути можно только посредством сопоставления UNC-пути с буквой диска.
Пути к устройствам DOS
В операционной системе Windows используется унифицированная объектная модель, которая указывает на все ресурсы, включая файлы. Эти пути к объектам доступны из окна консоли и предоставляются на уровень Win32 с использованием специальной папки с символьными ссылками, с которыми сопоставляются устаревшие пути DOS и UNC. Доступ к этой специальной папке осуществляется с использованием синтаксиса пути к устройству DOS, который может иметь одну из приведенных ниже форм:
\\.\C:\Test\Foo.txt
\\?\C:\Test\Foo.txt
Помимо использования буквы диска, вы можете указать том с помощью его GUID. Синтаксис будет иметь вид:
\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test\Foo.txt
\\?\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test\Foo.txt
Примечание
Синтаксис пути к устройству DOS поддерживается в реализациях платформы .NET для ОС Windows, начиная с версий .NET Core 1.1 и .NET Framework 4.6.2.
Путь к устройству DOS состоит из следующих компонентов:
- Описатель пути к устройству (
\\.\
или\\?\
), который идентифицирует путь как путь к устройству DOS.Примечание
Описатель
\\?\
поддерживается во всех версиях .NET Core, в .NET 5 и более поздних версий, а также в .NET Framework, начиная с версии 4.6.2. - Символьная ссылка на «реальный» объект устройства (C: в случае имени диска или Volume{b75e2c83-0000-0000-0000-602f00000000} в случае GUID тома).
Первый сегмент пути к устройству DOS после описателя пути к устройству идентифицирует том или диск. (Например,
\\?\C:\
и\\.\BootPartition\
.)Для UNC-путей существует специальная ссылка, которая называется
UNC
. Пример:\\.\UNC\Server\Share\Test\Foo.txt
\\?\UNC\Server\Share\Test\Foo.txt
Для UNC-путей к устройствам часть сервера или общего сетевого ресурса образует том. Например, в пути
\\?\server1\e:\utilities\\filecomparer\
частьserver1\utilities
представляет сервер или общий сетевой ресурс. Это важно при вызове такого метода, как Path.GetFullPath(String, String) с сегментами с относительным путем к каталогу, поскольку переход дальше тома невозможен.
Пути устройств DOS являются полными по определению и не могут начинаться с относительного сегмента каталога (.
или ..
). Они никогда не задаются относительно текущего каталога.
Пример. Способы задать ссылку на один и тот же файл
В следующем примере демонстрируются некоторые способы задать ссылку на файл с использованием API в пространстве имен System.IO. В этом примере создается экземпляр объекта FileInfo и используются его свойства Name и Length, чтобы отобразить имя и длину файла.
using System;
using System.IO;
class Program
{
static void Main()
{
string[] filenames = {
@"c:\temp\test-file.txt",
@"\\127.0.0.1\c$\temp\test-file.txt",
@"\\LOCALHOST\c$\temp\test-file.txt",
@"\\.\c:\temp\test-file.txt",
@"\\?\c:\temp\test-file.txt",
@"\\.\UNC\LOCALHOST\c$\temp\test-file.txt",
@"\\127.0.0.1\c$\temp\test-file.txt" };
foreach (var filename in filenames)
{
FileInfo fi = new FileInfo(filename);
Console.WriteLine($"file {fi.Name}: {fi.Length:N0} bytes");
}
}
}
// The example displays output like the following:
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
// file test-file.txt: 22 bytes
Нормализация путей
Практически все передаваемые в API Windows пути нормализуются. При нормализации в Windows выполняются следующие действия:
- Идентифицируется путь.
- Текущий каталог применяется к неполным (относительным) путям.
- Выполняется канонизация разделителей каталогов.
- Вычисляются относительные компоненты каталога (
.
для текущего и..
для родительского каталога). - Удаляются некоторые символы.
Эта нормализация происходит неявно, но это можно сделать явным образом, вызвав Path.GetFullPath метод , который заключает в оболочку вызов функции GetFullPathName(). Также можно вызвать функцию GetFullPathName() Windows напрямую с помощью P/Invoke.
Идентификация пути
На первом шаге процесса нормализации осуществляется идентификация типа пути. Пути могут относиться к одной из нескольких категорий:
- Пути к устройствам: начинаются с двух разделителей и знака вопроса или точки (
\\?
или\\.
). - UNC-пути: начинаются с двух разделителей без знака вопроса или точки.
- Полные пути DOS: начинаются с буквы диска, разделителя томов и компонентов (
C:\
). - Пути к устаревшим устройствам (
CON
,LPT1
). - Пути относительно корня текущего диска: начинаются с одного разделителя компонентов (
\
). - Пути относительно текущего каталога указанного диска: начинаются с буквы диска и разделителя томов, но не содержат разделителя компонентов (
C:
). - Пути относительно текущего каталога: начинаются с любых других символов (
temp\testfile.txt
).
Тип пути определяет, будет ли каким-либо образом применяться текущий каталог. Кроме того, от типа пути зависит применяемый корень.
Работа с устаревшими устройствами
Если путь указывает на устаревшее устройство DOS, например CON
, COM1
или LPT1
, он преобразуется в путь к устройству путем добавления перед ним последовательности \\.\
и возвращается в таком виде.
Путь, который начинается с имени устаревшего устройства, всегда интерпретируется как путь к устаревшему устройству с помощью метода Path.GetFullPath(String). Например, путь к устройству DOS CON.TXT
будет выглядеть как \\.\CON
, а путь к устройству DOS COM1.TXT\file1.txt
будет выглядеть как \\.\COM1
.
Применение текущего каталога
Если путь не является полным, система Windows применяет к нему текущий каталог. К UNC-путям и путям к устройствам текущий каталог не применяется. Также текущий каталог не применяется к полным путям к диску с разделителем C:\
.
Если путь начинается с одного разделителя компонентов, применяется диск текущего каталога. Например, для пути к файлу \utilities
и текущего каталога C:\temp\
в результате нормализации будет получен путь C:\utilities
.
Если путь начинается с буквы диска, разделителя томов и не содержит разделителя компонентов, применяется последний текущий каталог, установленный из командной оболочки. Если последний текущий каталог не был установлен, применяется диск сам по себе. Например, для пути D:sources
, текущего каталога C:\Documents\
и последнего текущего каталога D:\sources\
на диске D: в результате будет получен путь D:\sources\sources
. Пути, задаваемые относительно диска, являются распространенными источниками ошибок программ и логики скрипта. Предположение, что путь, начинающийся с буквы и двоеточия, не является относительным, очевидно неверно.
Если путь не начинается с разделителя, применяются текущий диск и текущий каталог. Например, для пути к файлу filecompare
и текущего каталога C:\utilities\
в результате будет получен путь C:\utilities\filecompare\
.
Важно!
Применение относительных путей в многопотоковых приложениях (то есть в большинстве приложений) сопряжено с определенными рисками, поскольку текущий каталог задается на уровне процесса. Таким образом, любой поток может в любое время изменить текущий каталог. Начиная с версии .NET Core 2.1, вы можете вызвать метод Path.GetFullPath(String, String) для получения абсолютного пути на основе относительного и базового (текущий каталог) путей, относительно которых требуется выполнить разрешение.
Канонизация разделителей
Все символы косой черты (/
) преобразуются в стандартные разделители Windows, то есть символы обратной косой черты (\
). Если они присутствуют, последовательность символов косой черты после первых двух таких символов свертывается в один символ косой черты.
Вычисление относительных компонентов
При обработке пути выполняется вычисление любых его компонентов или сегментов, которые состоят из одной или двух точек (.
или ..
):
- Если обнаруживается одна точка, текущий сегмент удаляется, поскольку он ссылается на текущий каталог.
- Если обнаруживаются две точки, удаляются текущий и родительский сегмент, поскольку в этом случае задается ссылка на родительский каталог.
Родительские каталоги удаляются только в том случае, если они не находятся после корня пути. Корень пути зависит от его типа. Это будет диск (
C:\
) для путей DOS, сервер или общий сетевой ресурс для UNC-путей (\\Server\Share
) и префикс пути к устройству для путей к устройствам (\\?\
или\\.\
).
Удаление знаков
Помимо удаленных ранее разделителей и относительных сегментов во время нормализации также удаляются некоторые дополнительные знаки:
- Если сегмент заканчивается одной точкой, эта точка удаляется. (Сегмент одиночного или двойного периода нормализуется на предыдущем шаге. Сегмент из трех или более точек не нормализуется и фактически является допустимым именем файла или каталога.)
- Если путь не заканчивается разделителем, удаляются все конечные точки и пробелы (U+0020). Если последний сегмент содержит только одну или две точки, к нему применяется приведенное выше правило для относительных компонентов.
Это правило устанавливает, что вы можете создать имя каталога с конечным пробелом, добавив разделитель после пробела.
Важно!
Создавать имена каталогов или файлов с конечным пробелом нельзя. Наличие конечных пробелов может затруднить или исключить возможность доступа к каталогу. В связи с этим при попытке обработать каталоги или файлы, имена которых содержат конечные пробелы, происходит сбой приложения.
Пропуск нормализации
Как правило, любой путь, передаваемый в API Windows передается в функцию GetFullPathName и нормализуется. Существует одно важное исключение: путь к устройству, который начинается со знака вопроса, а не с точки. Если путь не начинается с последовательности \\?\
(обратите внимание на использование канонической формы с обратной косой чертой), он нормализуется.
Зачем нужно пропускать нормализацию? Существует три основных причины:
- Получение путей, которые в обычных обстоятельствах недоступны, но являются допустимыми. Например, невозможно каким-либо иным способом получить доступ к файлу или каталогу с именем
hidden.
. - Повышение производительности за счет пропуска нормализации в тех случаях, когда нормализация уже выполнена.
- Только на платформе .NET Framework пропуск проверки длины пути
MAX_PATH
для использования путей длиной более 259 символов. Такое поведение допускается в большинстве API за некоторыми исключениями.
Примечание
.NET Core и .NET 5 или более поздней версии обрабатывают длинные пути неявным образом и не выполняют проверку MAX_PATH
. Проверка MAX_PATH
применяется только для платформы .NET Framework.
Пропуск нормализации и проверки максимальной длины пути является единственным отличием между двумя видами синтаксиса путей к устройствам. В остальных аспектах они идентичны. Пропуск нормализации следует использовать с осторожностью, поскольку в этом случае легко получить пути, при работе с которыми в обычных приложениях будут возникать трудности.
Пути, начинающиеся с последовательности \\?\
, по-прежнему нормализуются, если явно передать их в функцию GetFullPathName.
Вы можете передавать пути длиной более MAX_PATH
символов в функцию GetFullPathName без \\?\
. Она поддерживает пути произвольной длины, которая ограничивается лишь максимальным размером строки, поддерживаемым в Windows.
Регистр символов и файловая система Windows
Особенность файловой системы Windows заключается в том, что пользователи и разработчики, имеющие дело с другими операционными системами, могут сталкиваться с проблемами из-за того, что в именах каталогов и путях не учитывается регистр символов. Это значит, что в именах каталогов и файлов сохраняется регистр строк, используемый в момент их создания. Например, вызов метода
Directory.Create("TeStDiReCtOrY");
создает каталог с именем TeStDiReCtOrY. Если переименовать каталог или файл так, чтобы изменился регистр символов, в имени будет отражен регистр, используемый в момент переименования. Например, следующий код переименовывает файл test.txt в Test.txt:
using System.IO;
class Example
{
static void Main()
{
var fi = new FileInfo(@".\test.txt");
fi.MoveTo(@".\Test.txt");
}
}
Тем не менее при сравнении имен каталогов и файлов регистр символов не учитывается. Если выполнить поиск файла с именем «test.txt», API файловой системы .NET будут игнорировать регистр символов при сравнении. Таким образом, при поиске файла «test.txt» будут возвращены совпадения для файлов «Test.txt», «TEST.TXT», «test.TXT», а также любых других их вариантов с различным сочетанием букв в верхнем и нижнем регистре.