如何找到文件名的正确大小写?

7

这是在Mac上的问题:

如果我有两个文件名 /foo/foo 和 /foo/FOO,它们可能根据文件系统引用同一个文件或者是不同的文件。如何确定它们是否都指向同一个文件?如果是,如何获取正确的文件名表示方式?

我的问题是由链接引起的。一个链接可能指向 /foo/FOO,但实际目录名为 /foo/foo。

是否有任何函数可以跟随链接并给我链接文件的完整路径?[NSFileManager pathContentOfSymbolicLinkAtPath] 给出的是可能大小写不正确的相对路径。

最终,我想要做的是缓存文件信息。但是,如果我有两个不同的路径指向同一个文件,我的缓存就会失去同步。

谢谢


如果两个文件路径指向同一个i节点,则没有优先级。没有单一的“正确表示”。因此,/foo/FOO和/foo/foo是同样合法的。更改其中一个会更改另一个。删除一个不会影响另一个。文件空间直到所有引用都消失才被释放。 - dkretz
HFS+不区分大小写,但保留大小写。因此/foo/foo是实际的文件名。fopen("/foo/FOO", "r")将打开相同的文件,但你可以说它不是“正确”的文件名。我不担心有两个硬链接指向同一个文件的情况。 - Roland Rabien
5个回答

15
你的问题实际上有几个不同的部分。根据我的理解,您希望:

1 一种方法来判断两个不同路径是否为相同的磁盘文件

2 确定磁盘上文件的规范名称,并使用正确的大小写

还有一个涉及到显示名称的第三个问题,因为在OS X中,文件可以本地化其名称,并对不同的语言环境显示不同的名称。所以我们再加上:

3 获取显示名称的方法,因为我们可能想要根据用户看到文件系统的方式缓存某些内容,而不是终端中文件系统的显示方式。

我们可以通过@boaz-stuller提出的FSRef技巧来解决1。或者这里有一些使用更高级别的Cocoa调用的代码,这样我们就可以节省一些内存操作(因为我们可以让NSAutoreleasePool代劳):
long getInode(NSString* path) {
    NSFileManager* fm = [NSFileManager defaultManager];
    NSError* error;
    NSDictionary* info = [fm attributesOfItemAtPath:path error:&error];
    NSNumber* inode = [info objectForKey:NSFileSystemFileNumber];
    return [inode longValue];
}

但要解决2,我们必须使用FSRefs来找到文件的规范大小写:

NSString* getActualPath(NSString* path) {
    FSRef ref;
    OSStatus sts;
    UInt8* actualPath;
    
    //first get an FSRef for the path
    sts = FSPathMakeRef((const UInt8 *)[path UTF8String], &ref, NULL);
    if (sts) return [NSString stringWithFormat:@"Error #%d making ref.", sts];
    
    //then get a path from the FSRef
    actualPath = malloc(sizeof(UInt8)*MAX_PATH_LENGTH);
    sts = FSRefMakePath(&ref, actualPath, MAX_PATH_LENGTH);
    if (sts) return [NSString stringWithFormat:@"Error #%d making path.", sts];
    
    return [NSString stringWithUTF8String:(const char*)actualPath];
}

这并不错,但是当我们能够使用Cocoa方法解决3时,我们会更加高兴:

NSString* getDisplayPath(NSString* path) {
    NSFileManager* fm = [NSFileManager defaultManager];
    NSString* mine = [fm displayNameAtPath:path];
    NSString* parentPath = [path stringByDeletingLastPathComponent];
    NSString* parents = [@"/" isEqualToString:parentPath]
        ? @""
        : getDisplayPath(parentPath);
    return [NSString stringWithFormat:@"%@/%@", parents, mine];
}

最后,我们可以添加一些驱动程序代码,并将其全部绑定到CoreFoundation命令行工具中(我不得不添加AppKit框架才能使其编译)。
NSString* fileInfoString(NSString* path) {
    long inode = getInode(path);
    return [NSString stringWithFormat:
        @"\t%@  [inode #%d]\n\t\tis actually %@\n\t\tand displays as %@",
        path,
        inode,
        getActualPath(path),
        getDisplayPath(path)];
}

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    if (argc < 2) {
        NSLog(@"Usage: %s <path1> [<path2>]", argv[0]);
        return -1;
    }

    NSString* path1 = [NSString stringWithCString:argv[1]];
    NSString* path2 = argc > 2
        ? [NSString stringWithCString:argv[1]]
        : [path1 uppercaseString];
    
    long inode1 = getInode(path1);
    long inode2 = getInode(path2);
    
    NSString* prefix = [NSString stringWithFormat:
        @"Comparing Files:\n%@\n%@", 
        fileInfoString(path1), 
        fileInfoString(path2)];
    
    int retval = 0;
    if (inode1 == inode2) {
        NSLog(@"%@\nSame file.", prefix);
    } else {
        NSLog(@"%@\nDifferent files.", prefix);
        retval = 1;
    }
    
    [pool drain];
    return retval;
}

现在,我们可以把所有内容都放在一起并运行它:

 $ checkpath /users/tal
 2008-12-15 23:59:10.605 checkpath[22375:10b] 比较文件:
    /users/tal  [inode #1061692]
        实际路径为 /Users/tal
        显示路径为 /Users/tal
    /USERS/TAL  [inode #1061692]
        实际路径为 /Users/tal
        显示路径为 /Users/tal
 同一个文件。

哇,太棒了!我没想到你会为我做这么多。非常感谢!getActualPath() 正是我所需要的。 - Roland Rabien
1
这段代码有几个问题。1)i节点仅在每个设备上是唯一的,因此您应该比较设备/ i节点对。2)malloc分配的actualPath未被释放。要解决这个问题,您可以将其声明为UInt8 actualPath [PATH_MAX + 1]。 - Boaz Stuller
此外,我通常发现使用FSRefs比路径更简单。路径有许多令人烦恼的边缘情况(例如,请参见此问题:) ,而FSRefs基本上只需工作即可。 FSRefs与路径相比唯一的重大限制是,路径可以引用尚不存在的文件,而FSRefs则不能。 - Boaz Stuller
没问题。虽然,一个更简单的方法(直到现在才想到)只要这些键不会显示给用户,就是在使用路径之前将它们全部转换为小写。简单而有效! - TALlama
除非在区分大小写的文件系统中。路径很难。 - Boaz Stuller
1
由于FSPathMakeRef和相关的API已被弃用,现在该如何做呢? - user187676

6

使用FSPathMakeRef()函数处理两个路径,然后使用FSCompareFSRefs()函数来比较它们是否是同一个文件/文件夹。你可以使用FSRefMakePath()函数获取规范表示法,但如果你要将文件名显示给用户,应该使用NSFileManager-displayNameAtPath:方法,因为它能正确处理本地化和显示/隐藏扩展名。


2
如果OS X是Unix的派生系统,那么您是否可以访问inode号码?

1
这将是最好的想法。stat(2)函数应返回有用的inode号码。请注意,由于不同的设备具有自己的inode号码序列,因此配对(设备,inode)是唯一键。 - Greg Hewgill
这应该适用于主文件系统。我不知道它是否适用于已挂载的NTFS或FAT32驱动器。 - Roland Rabien
然后,“文件名的正确表示”是一个不合逻辑的说法。对于inode的文件引用,这些引用本身就是一级文件名——不存在主文件名。 - dkretz

0

如果有人在iOS上尝试做这件事,FSRef不可用。我能想到的唯一方法是列出父目录的内容,然后进行匹配,以获取具有正确大小写的“实际”文件名。

iOS是一个区分大小写的文件系统,而OS X(和模拟器)则不是。我编写了这段代码来检测给定路径是否存在于模拟器中但大小写错误。

如果有人有更好的方法来做到这一点(除了这个丑陋的循环),我非常乐意听取建议。

void checkForCapsIssues(NSString* compiledPath)
{

NSArray* validFilePaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[[compiledPath stringByDeletingLastPathComponent] stringByResolvingSymlinksInPath] error:nil];
NSString* lastPathComponent = [compiledPath lastPathComponent];
for (NSString* fileName in validFilePaths) {
    if([fileName isEqualToString:lastPathComponent])
    {
        return;
    }
    if([[fileName lowercaseString] isEqualToString:[lastPathComponent lowercaseString]])
    {
        NSLog(@"Warning! Caps Problem Found! %@", compiledPath);
        return;
    }
}

}

-2
据我所知,默认情况下,Mac OS X 中的文件系统是不区分大小写的,因此链接或文件名的大小写不应该影响。

1
我的新iMac与Leopard系统在我第一次开机时给了我一个选择。因此,完全有可能拥有一个带有区分大小写文件系统的出厂安装的Mac。 - Rob Kennedy
2
用户默认获取什么是不相关的。用户可以在任何设备上拥有区分大小写的文件系统,无论是HFSX还是UFS。任何不能正确处理这一点的应用程序都是错误的。 - Peter Hosey
将Photoshop CS3添加到“损坏”的应用程序列表中。我仍然很烦恼,因为我不得不重新格式化才能安装它,如果是其他应用程序,我就会直接删除该应用程序。 - Marc Charbonneau
1
与我之前的评论相关的推论:不要以引导文件系统为基础来确定你的行为。用户可能挂载了其他格式、具有不同大小写行为的FS。对于每个路径都要适当地表现。 - Peter Hosey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接