如何确定两个文件路径或文件 URL是否标识 macOS 上的同一个文件或目录?

问题描述

想象一下 macOS 上有两个路径的这个简单示例:

  • /etc/hosts
  • /private/etc/hosts

两者都指向同一个文件。但是您如何确定这一点?

一个例子:

  • ~/Desktop
  • /Users/yourname/Desktop

或者不区分大小写的文件系统上的大小写混合怎么样:

  • /Volumes/external/my file
  • /Volumes/External/My File

甚至这个:

  • /Applications/Über.app

此处:可以以两种 unicode composition 格式(NFD、NFC)指定“Ü”。有关使用 (NS)URL API 时可能发生这种情况的示例,请参阅 this gist of mine

从 macOS 10.15 (Catalina) 开始,另外还有 firmlinks一个链接到卷组中的另一个。同一个 FS 对象的路径可以写成:

  • /Applications/Find Any File.app
  • /System/Volumes/Data/Applications/Find Any File.app

我喜欢记录可靠地处理所有这些复杂问题的方法,目标是高效(即快速)。

解决方法

有两种方法可以检查两个路径(或它们的文件 URL)是否指向同一个文件系统项:

  • 比较他们的路径。这需要先准备好路径。
  • 比较它们的 ID(索引节点)。这总体上更安全,因为它避免了 Unicode 错综复杂和大小写错误的所有并发症。

比较文件 ID

在 ObjC 中,这相当容易(注意:因此,对于知识渊博的 Apple 开发人员来说,不应依赖 [NSURL fileReferenceURL],因此此代码使用了一种更简洁的方式):

NSString *p1 = @"/etc/hosts";
NSString *p2 = @"/private/etc/hosts";
NSURL *url1 = [NSURL fileURLWithPath:p1];
NSURL *url2 = [NSURL fileURLWithPath:p2];

id ref1 = nil,ref2 = nil;
[url1 getResourceValue:&ref1 forKey:NSURLFileResourceIdentifierKey error:nil];
[url2 getResourceValue:&ref2 forKey:NSURLFileResourceIdentifierKey error:nil];

BOOL equal = [ref1 isEqual:ref2];

Swift 中的等效项(注意:不要使用 fileReferenceURL,请参阅 this bug report):

let p1 = "/etc/hosts"
let p2 = "/private/etc/hosts"
let url1 = URL(fileURLWithPath: p1)
let url2 = URL(fileURLWithPath: p2)

let ref1 = try url1.resourceValues(forKeys[.fileResourceIdentifierKey])
                   .fileResourceIdentifier
let ref2 = try url2.resourceValues(forKeys[.fileResourceIdentifierKey])
                   .fileResourceIdentifier
let equal = ref1?.isEqual(ref2) ?? false

这两种解决方案都在底层使用了 BSD 函数 lstat,因此您也可以用普通的 C 语言编写:

static bool paths_are_equal (const char *p1,const char *p2) {
  struct stat stat1,stat2;
  int res1 = lstat (p1,&stat1);
  int res2 = lstat (p2,&stat2);
  return (res1 == 0 && res2 == 0) &&
         (stat1.st_dev == stat2.st_dev) && (stat1.st_ino == stat2.st_ino);
}

但是,请注意有关使用此类文件引用的 warning

此标识符的值在系统重新启动后不会保持不变。

这主要用于卷 ID,但也可能影响不支持持久文件 ID 的文件系统上的文件 ID。

比较路径

要比较路径,您必须首先获得它们的规范路径

如果不这样做,就无法确定case 是否正确,进而会导致非常复杂的比较代码。 (有关详细信息,请参阅 using NSURLCanonicalPathKey。)

案件有多种不同的处理方式:

  • 用户可能手动输入了名称,大小写错误。
  • 您之前存储了路径,但用户在此期间重命名了文件的大小写。您的路径仍将标识同一个文件,但现在情况是错误的,相等路径的比较可能会失败,具体取决于您如何获得与之比较的其他路径。

仅当您从文件系统操作中获得路径时,您不能错误地指定路径的任何部分(即错误的大小写),您不需要获取规范路径,而只需调用 {{1} } 然后比较它们的路径是否相等(不需要不区分大小写的选项)。

否则,为了安全起见,从这样的 URL 获取规范路径:

standardizingPath

如果您的路径存储在 String 而不是 URL 对象中,您可以调用 import Foundation let uncleanPath = "/applications" let url = URL(fileURLWithPath: uncleanPath) if let resourceValues = try? url.resourceValues(forKeys: [.canonicalPathKey]),let resolvedPath = resourceValues.canonicalPath { print(resolvedPath) // gives "/Applications" } (Apple Docs)。但这既不能解决不正确的大小写,也不能分解字符,这可能会导致问题,如 aforementioned gist 所示。

因此,从 String 创建文件 URL 然后使用上述方法获取规范路径更安全,或者更好的是,使用 lstat() 解决方案来比较文件 ID,如上所示。

还有一个 BSD 函数可以从 C 字符串中获取规范路径:stringByStandardizingPath。但是,这并不安全,因为它不会将卷组中不同路径的情况(如问题中所示)解析为相同的字符串。因此,为此应避免使用此功能。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...