如何检查给定的字符串在 Windows 下是否是合法/有效的文件名?

我想在我的应用程序中包含一个批处理文件重命名功能。用户可以键入目标文件名模式,(在替换模式中的一些通配符之后)我需要检查它是否是 Windows 下的合法文件名。我尝试过使用像 [a-zA-Z0-9_]+这样的正则表达式,但是它不包含来自不同语言的许多特定于国家的字符(例如元音变调等)。做这种检查的最好方法是什么?

183307 次浏览

您可以使用正则表达式检查是否存在非法字符,然后报告错误,而不是显式地包含所有可能的字符。理想情况下,您的应用程序应该按照用户的意愿精确地命名文件,并且只有在偶然发现错误时才会大喊大叫。

下面是 MSDN中不允许使用的字符列表:

使用当前代码页中的几乎任何字符作为名称,包括 Unicode 字符和扩展字符集(128-255)中的字符,但以下字符除外:

  • 不允许使用下列保留字符: < > : “/| ? *
  • 不允许其整数表示形式在0到31之间的字符。
  • 目标文件系统不允许的任何其他字符。

可以从 Path.GetInvalidPathCharsGetInvalidFileNameChars获得无效字符列表。

UPD: 请参阅 史蒂夫 · 库珀的建议关于如何在正则表达式中使用这些。

UPD2: 注意,根据 MSDN 中的“备注”部分,“从此方法返回的数组不保证包含在文件和目录名中无效的完整字符集。”由六个字母的可变量提供的答案进入更多细节。

Windows 文件名是非常不受限制的,所以实际上它甚至可能不是 那个的一个问题。Windows 不允许使用的字符如下:

\ / : * ? " < > |

您可以很容易地编写一个表达式来检查这些字符是否存在。不过,更好的解决方案是尝试根据用户的需要为文件命名,并在文件名不符合要求时提醒他们。

此外,CON,PRN,AUX,NUL,COM # 和其他一些从来没有合法的文件名在任何目录中的任何扩展名。

Microsoft Windows: Windows 内核禁止使用范围1-31(即0x01-0x1F)和字符“ * : < > ?\ |.虽然 NTFS 允许每个路径组件(目录或文件名)长度为255个字符,路径长度最多为32767个字符,但 Windows 内核只支持最多259个字符的路径。此外,Windows 禁止使用 MS-DOS 设备名称 AUX、 CLOCK $、 COM1、 COM2、 COM3、 COM4、 COM5、 COM6、 COM7、 COM8、 COM9、 CON、 LPT1、 LPT2、 LPT3、 LPT4、 LPT5、 LPT6、 LPT7、 LPT8、 LPT9、 NUL 和 PRN,以及任何扩展名(例如,AUX.txt) ,除非使用 Long UNC 路径(例如:。\.C: nul.txt 还是?D: aux con).(实际上,如果提供了扩展,可以使用 CLOCK $。)这些限制只适用于 Windows-Linux,例如,允许使用“ * : < > ?即使在 NTFS。

资料来源: http://en.wikipedia.org/wiki/Filename

对于 .3.5之前的网络框架来说,这应该是可行的:

正则表达式匹配应该可以让你了解一些方法

bool IsValidFilename(string testName)
{
Regex containsABadCharacter = new Regex("["
+ Regex.Escape(System.IO.Path.InvalidPathChars) + "]");
if (containsABadCharacter.IsMatch(testName)) { return false; };


// other checks for UNC, drive-path format, etc


return true;
}

对于 .3.0后的网络框架来说,这应该是可行的:

Http://msdn.microsoft.com/en-us/library/system.io.path.getinvalidpathchars(v=vs.90).aspx

正则表达式匹配应该可以让你了解一些方法

bool IsValidFilename(string testName)
{
Regex containsABadCharacter = new Regex("["
+ Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]");
if (containsABadCharacter.IsMatch(testName)) { return false; };


// other checks for UNC, drive-path format, etc


return true;
}

一旦你知道,你也应该检查不同的格式,例如 c:\my\drive\\server\share\dir\file.ext

问题是,您是否试图确定路径名是合法的 Windows 路径,还是合法的 在运行代码的系统上。路径?我认为后者更重要,所以就我个人而言,我可能会分解完整路径并尝试使用 _ mkdir 创建文件所属的目录,然后尝试创建文件。

通过这种方式,您不仅可以知道路径是否只包含有效的 windows 字符,而且还可以知道它是否实际表示可由此进程编写的路径。

以下是 MSDN 的“命名文件或目录”中关于 Windows 下合法文件名的一般约定:

您可以在当前代码页(Unicode/ANSI 大于127)中使用任何字符,但以下字符除外:

  • < > : " / \ | ? *
  • 其整数表示形式为0-31(小于 ASCII 空间)的字符
  • 目标文件系统不允许的任何其他字符(例如,尾随句点或空格)
  • 任何 DOS 名称: CON、 PRN、 AUX、 NUL、 COM0、 COM1、 COM2、 COM3、 COM4、 COM5、 COM6、 COM7、 COM8、 COM9、 LPT0、 LPT1、 LPT2、 LPT3、 LPT4、 LPT5、 LPT6、 LPT7、 LPT8、 LPT9(避免使用 AUX.txt 等)
  • 文件名是所有句点

检查一些可选项:

  • 文件路径(包括文件名)可能不超过260个字符(不使用 \?\前缀)
  • 使用 \?\时,具有超过32,000个字符的 Unicode 文件路径(包括文件名)(注意,前缀可能会扩展目录组件并导致其溢出32,000个限制)

尝试使用它,并捕获错误。允许的集合可以跨文件系统或不同版本的 Windows 更改。换句话说,如果你想知道 Windows 是否喜欢这个名字,把名字递给它,让它告诉你。

我用的是这个:

    public static bool IsValidFileName(this string expression, bool platformIndependent)
{
string sPattern = @"^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\"";|/]+$";
if (platformIndependent)
{
sPattern = @"^(([a-zA-Z]:|\\)\\)?(((\.)|(\.\.)|([^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?))\\)*[^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?$";
}
return (Regex.IsMatch(expression, sPattern, RegexOptions.CultureInvariant));
}

第一种模式创建一个正则表达式,其中包含仅适用于 Windows 平台的无效/非法文件名和字符。第二种方法也是如此,但是要确保这个名称对于任何平台都是合法的。

对于这种情况,正则表达式有些过头了。您可以将 String.IndexOfAny()方法与 Path.GetInvalidPathChars()Path.GetInvalidFileNameChars()结合使用。

还要注意,这两个 Path.GetInvalidXXX()方法都克隆一个内部数组并返回克隆。因此,如果你要经常这样做(成千上万次) ,你可以缓存一个无效的字符数组的副本,以便重新使用。

需要记住的一个角落情况是,当我第一次发现它时,我感到很惊讶: Windows 允许在文件名中使用前导空格字符!例如,下面的文件名在 Windows 上都是合法和独立的(减去引号) :

"file.txt"
" file.txt"
"  file.txt"

这样做的一个好处是: 在编写从文件名字符串中删除前/后空格的代码时要小心。

这个类清除文件名和路径; 使用它就像

var myCleanPath = PathSanitizer.SanitizeFilename(myBadPath, ' ');

这是密码

/// <summary>
/// Cleans paths of invalid characters.
/// </summary>
public static class PathSanitizer
{
/// <summary>
/// The set of invalid filename characters, kept sorted for fast binary search
/// </summary>
private readonly static char[] invalidFilenameChars;
/// <summary>
/// The set of invalid path characters, kept sorted for fast binary search
/// </summary>
private readonly static char[] invalidPathChars;


static PathSanitizer()
{
// set up the two arrays -- sorted once for speed.
invalidFilenameChars = System.IO.Path.GetInvalidFileNameChars();
invalidPathChars = System.IO.Path.GetInvalidPathChars();
Array.Sort(invalidFilenameChars);
Array.Sort(invalidPathChars);


}


/// <summary>
/// Cleans a filename of invalid characters
/// </summary>
/// <param name="input">the string to clean</param>
/// <param name="errorChar">the character which replaces bad characters</param>
/// <returns></returns>
public static string SanitizeFilename(string input, char errorChar)
{
return Sanitize(input, invalidFilenameChars, errorChar);
}


/// <summary>
/// Cleans a path of invalid characters
/// </summary>
/// <param name="input">the string to clean</param>
/// <param name="errorChar">the character which replaces bad characters</param>
/// <returns></returns>
public static string SanitizePath(string input, char errorChar)
{
return Sanitize(input, invalidPathChars, errorChar);
}


/// <summary>
/// Cleans a string of invalid characters.
/// </summary>
/// <param name="input"></param>
/// <param name="invalidChars"></param>
/// <param name="errorChar"></param>
/// <returns></returns>
private static string Sanitize(string input, char[] invalidChars, char errorChar)
{
// null always sanitizes to null
if (input == null) { return null; }
StringBuilder result = new StringBuilder();
foreach (var characterToTest in input)
{
// we binary search for the character in the invalid set. This should be lightning fast.
if (Array.BinarySearch(invalidChars, characterToTest) >= 0)
{
// we found the character in the array of
result.Append(errorChar);
}
else
{
// the character was not found in invalid, so it is valid.
result.Append(characterToTest);
}
}


// we're done.
return result.ToString();
}


}

目标文件系统也很重要。

在 NTFS 下,某些文件无法在特定目录中创建。 在根中引导

为了补充其他的答案,这里有一些额外的边缘情况,您可能需要考虑。

这是一个已经得到回答的问题,但仅仅为了“其他选择”,这里有一个不理想的选择:

(不理想,因为使用异常作为流控制通常是一件“坏事”)

public static bool IsLegalFilename(string name)
{
try
{
var fileInfo = new FileInfo(name);
return true;
}
catch
{
return false;
}
}

我使用它来去除文件名中的无效字符,而不会抛出异常:

private static readonly Regex InvalidFileRegex = new Regex(
string.Format("[{0}]", Regex.Escape(@"<>:""/\|?*")));


public static string SanitizeFileName(string fileName)
{
return InvalidFileRegex.Replace(fileName, string.Empty);
}

我建议只使用 Path. getFullPath ()

string tagetFileFullNameToBeChecked;
try
{
Path.GetFullPath(tagetFileFullNameToBeChecked)
}
catch(AugumentException ex)
{
// invalid chars found
}

简化 Eugene Katz 的回答:

bool IsFileNameCorrect(string fileName){
return !fileName.Any(f=>Path.GetInvalidFileNameChars().Contains(f))
}

或者

bool IsFileNameCorrect(string fileName){
return fileName.All(f=>!Path.GetInvalidFileNameChars().Contains(f))
}

如果文件名太长并且在 Windows10之前的环境中运行,许多这样的回答将不起作用。类似地,考虑一下你想用句点做什么——允许前导或尾随在技术上是有效的,但是如果你不希望文件分别难以看到或者难以删除,就会产生问题。

这是我创建的一个验证属性,用于检查有效的文件名。

public class ValidFileNameAttribute : ValidationAttribute
{
public ValidFileNameAttribute()
{
RequireExtension = true;
ErrorMessage = "{0} is an Invalid Filename";
MaxLength = 255; //superseeded in modern windows environments
}
public override bool IsValid(object value)
{
//http://stackoverflow.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists
var fileName = (string)value;
if (string.IsNullOrEmpty(fileName)) { return true;  }
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1 ||
(!AllowHidden && fileName[0] == '.') ||
fileName[fileName.Length - 1]== '.' ||
fileName.Length > MaxLength)
{
return false;
}
string extension = Path.GetExtension(fileName);
return (!RequireExtension || extension != string.Empty)
&& (ExtensionList==null || ExtensionList.Contains(extension));
}
private const string _sepChar = ",";
private IEnumerable<string> ExtensionList { get; set; }
public bool AllowHidden { get; set; }
public bool RequireExtension { get; set; }
public int MaxLength { get; set; }
public string AllowedExtensions {
get { return string.Join(_sepChar, ExtensionList); }
set {
if (string.IsNullOrEmpty(value))
{ ExtensionList = null; }
else {
ExtensionList = value.Split(new char[] { _sepChar[0] })
.Select(s => s[0] == '.' ? s : ('.' + s))
.ToList();
}
} }


public override bool RequiresValidationContext => false;
}

还有那些测试

[TestMethod]
public void TestFilenameAttribute()
{
var rxa = new ValidFileNameAttribute();
Assert.IsFalse(rxa.IsValid("pptx."));
Assert.IsFalse(rxa.IsValid("pp.tx."));
Assert.IsFalse(rxa.IsValid("."));
Assert.IsFalse(rxa.IsValid(".pp.tx"));
Assert.IsFalse(rxa.IsValid(".pptx"));
Assert.IsFalse(rxa.IsValid("pptx"));
Assert.IsFalse(rxa.IsValid("a/abc.pptx"));
Assert.IsFalse(rxa.IsValid("a\\abc.pptx"));
Assert.IsFalse(rxa.IsValid("c:abc.pptx"));
Assert.IsFalse(rxa.IsValid("c<abc.pptx"));
Assert.IsTrue(rxa.IsValid("abc.pptx"));
rxa = new ValidFileNameAttribute { AllowedExtensions = ".pptx" };
Assert.IsFalse(rxa.IsValid("abc.docx"));
Assert.IsTrue(rxa.IsValid("abc.pptx"));
}

如果您只是想检查包含您的文件名/路径的字符串是否有无效字符,那么我发现的最快方法是使用 Split()将文件名分解为一个包含无效字符的数组。如果结果只是一个1的数组,则不存在无效字符。:-)

var nameToTest = "Best file name \"ever\".txt";
bool isInvalidName = nameToTest.Split(System.IO.Path.GetInvalidFileNameChars()).Length > 1;


var pathToTest = "C:\\My Folder <secrets>\\";
bool isInvalidPath = pathToTest.Split(System.IO.Path.GetInvalidPathChars()).Length > 1;

在 LinqPad,我尝试在一个文件/路径名上运行这个方法和上面提到的其他方法1,000,000次。

使用 Split()只有大约850ms。

使用 Regex("[" + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]")大约需要6秒钟。

更复杂的正则表达式更糟糕,其他一些选项也是如此,比如使用 Path类上的各种方法来获取文件名,并让它们的内部验证来完成这项工作(很可能是由于异常处理的开销)。

当然,您并不经常需要验证100万个文件名,所以对于这些方法中的大多数来说,一次迭代就足够了。但是如果您只是在寻找无效字符,那么它仍然是非常有效的。

我的尝试:

using System.IO;


static class PathUtils
{
public static string IsValidFullPath([NotNull] string fullPath)
{
if (string.IsNullOrWhiteSpace(fullPath))
return "Path is null, empty or white space.";


bool pathContainsInvalidChars = fullPath.IndexOfAny(Path.GetInvalidPathChars()) != -1;
if (pathContainsInvalidChars)
return "Path contains invalid characters.";


string fileName = Path.GetFileName(fullPath);
if (fileName == "")
return "Path must contain a file name.";


bool fileNameContainsInvalidChars = fileName.IndexOfAny(Path.GetInvalidFileNameChars()) != -1;
if (fileNameContainsInvalidChars)
return "File name contains invalid characters.";


if (!Path.IsPathRooted(fullPath))
return "The path must be absolute.";


return "";
}
}

这并不完美,因为 Path.GetInvalidPathChars没有返回在文件和目录名中无效的完整字符集,当然还有很多细微之处。

所以我用这个方法作为补充:

public static bool TestIfFileCanBeCreated([NotNull] string fullPath)
{
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("Value cannot be null or whitespace.", "fullPath");


string directoryName = Path.GetDirectoryName(fullPath);
if (directoryName != null) Directory.CreateDirectory(directoryName);
try
{
using (new FileStream(fullPath, FileMode.CreateNew)) { }
File.Delete(fullPath);
return true;
}
catch (IOException)
{
return false;
}
}

它尝试创建文件,如果有异常,则返回 false。当然,我需要创建文件,但我认为这是最安全的方式来做到这一点。还请注意,我不会删除已经创建的目录。

您还可以使用第一个方法进行基本验证,然后在使用路径时小心处理异常。

我从别人那里得到了这个想法。-不知道是谁。让操作系统来做这些繁重的工作。

public bool IsPathFileNameGood(string fname)
{
bool rc = Constants.Fail;
try
{
this._stream = new StreamWriter(fname, true);
rc = Constants.Pass;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Problem opening file");
rc = Constants.Fail;
}
return rc;
}

这张支票

static bool IsValidFileName(string name)
{
return
!string.IsNullOrWhiteSpace(name) &&
name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 &&
!Path.GetFullPath(name).StartsWith(@"\\.\");
}

过滤出带有无效字符的名称(<>:"/\|?*和 ASCII 0-31) ,以及保留的 DOS 设备(CONNULCOMx)。它允许前导空格和全点名,与 Path.GetFullPath一致。(在我的系统上成功创建带有前导空格的文件)。


使用.NET Framework 4.7.1,在 Windows 7上测试。

用于验证字符串中非法字符的一行:

public static bool IsValidFilename(string testName) => !Regex.IsMatch(testName, "[" + Regex.Escape(new string(System.IO.Path.InvalidPathChars)) + "]");

在我看来,这个问题的唯一正确答案是尝试使用路径,并让操作系统和文件系统验证它。否则,您只是重新实现了操作系统和文件系统已经使用的所有验证规则(可能效果很差) ,如果将来这些规则发生了变化,您将不得不更改代码以匹配它们。