检查Python中的文件系统是否区分大小写

19

有没有一种简单的方法在Python中检查文件系统是否不区分大小写?我特别考虑像HFS+(OSX)和NTFS(Windows)这样的文件系统,在这些文件系统上,您可以访问与foo、Foo或FOO相同的文件,即使文件名大小写不同也可以。

10个回答

19
import os
import tempfile

# By default mkstemp() creates a file with
# a name that begins with 'tmp' (lowercase)
tmphandle, tmppath = tempfile.mkstemp()
if os.path.exists(tmppath.upper()):
    # Case insensitive.
else:
    # Case sensitive.

如果tmppath全是大写字母会发生什么?规范保证不会发生吗? - Lorin Hochstein
@LorinHochstein - mkstemp()函数中'prefix'参数的默认值,如代码注释中所述,是"tmp"(小写)。http://docs.python.org/library/tempfile.html#tempfile.mkstemp 因此生成的文件名将始终以3个小写字符开头。 - Amber
@Lorin Hochstein:文档表明默认前缀是“ tmp”,你可以检查gettempprefix() == gettempprefix.lower(),或在mkstemp()中明确设置前缀。 - jfs
3
如果临时文件不在所需的文件系统中,会发生什么情况? - xuhdev
1
不要忘记在完成后删除该文件!os.path.remove(tmppath) - Romuald Brunet

8

除非显式处理关闭和删除,否则Amber提供的答案将留下临时文件碎片。为了避免这种情况,我使用:

import os
import tempfile

def is_fs_case_sensitive():
    #
    # Force case with the prefix
    #
    with tempfile.NamedTemporaryFile(prefix='TmP') as tmp_file:
        return(not os.path.exists(tmp_file.name.lower()))

尽管我的使用情况通常会测试这个结果不止一次,因此我将结果存储起来以避免多次触碰文件系统。

def is_fs_case_sensitive():
    if not hasattr(is_fs_case_sensitive, 'case_sensitive'):
        with tempfile.NamedTemporaryFile(prefix='TmP') as tmp_file:
            setattr(is_fs_case_sensitive,
                    'case_sensitive',
                    not os.path.exists(tmp_file.name.lower()))
    return(is_fs_case_sensitive.case_sensitive)

如果只调用一次,则速度略慢,而在其他情况下速度显著更快。


2
目前最佳解决方案,但该功能应接受源目录作为输入参数,因为至少在OSX上,路径可能会有所不同。不开玩笑。 - Erik Aronesty

4
很好,Eric Smith提到了不同的文件系统等问题。但是为什么不使用带有dir参数的tempfile.NamedTemporaryFile来避免自己进行所有上下文管理呢?
def is_fs_case_sensitive(path):
    #
    # Force case with the prefix
    #
    with tempfile.NamedTemporaryFile(prefix='TmP',dir=path, delete=True) as tmp_file:
        return(not os.path.exists(tmp_file.name.lower()))

我应该提到,你的解决方案并不能保证你真正测试了大小写敏感性。除非你检查默认前缀(使用tempfile.gettempprefix())以确保它包含一个小写字符。因此,在这里包括前缀并不是真正可选的。
你的解决方案清理了临时文件。我同意这似乎很显然,但人们总是无法预料。

4

对@Shrikant答案的变化,适用于模块内部(即不在REPL中),即使您的用户没有主目录:

import os.path
is_fs_case_insensitive = os.path.exists(__file__.upper()) and os.path.exists(__file__.lower())
print(f"{is_fs_case_insensitive=}")

输出(macOS):

is_fs_case_insensitive=True 

还有 Linux 方面的事情:

(ssha)vagrant ~$python3.8 test.py
is_fs_case_insensitive=False 
(ssha)vagrant ~$lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:    20.04
Codename:   focal

顺便提一下,我通过以下方式检查了 pathlib, os, os.path 的内容:

[k for k in vars(pathlib).keys() if "case" in k.lower()]

然而没有发现类似的内容,虽然它确实有一个 pathlib.supports_symlinks,但是没有关于大小写敏感度的信息。

并且以下也将在 REPL 中起作用:

is_fs_case_insensitive = os.path.exists(os.path.__file__.upper()) and os.path.exists(os.path.__file__.lower())

这种方法在区分大小写的操作系统上会出现错误报告,因为脚本的邪恶兄弟也存在:/some/dir/foo/SOME/DIR/FOO - FMc

3

我认为这个问题有一个更简单(也可能更快)的解决方案。在我测试的地方,以下方法似乎有效:

import os.path
home = os.path.expanduser('~')
is_fs_case_insensitive = os.path.exists(home.upper()) and os.path.exists(home.lower())

1
这适用于macOS,但有一个警告,如果作为无登录/无shell用户运行,可能会遇到问题,这对于守护进程上的低权限用户出于安全原因有时是必要的(例如,某人在用户“nobody”下运行Django)。除此之外,这是不会通过临时文件搞乱事情的那个。 - JL Peyret

2

从Amber的回答开始,我想出了这段代码。我不确定它是否完全健壮,但它试图解决原始代码中的一些问题(我将在下面提到)。

import os
import sys
import tempfile
import contextlib


def is_case_sensitive(path):
    with temp(path) as tmppath:
        head, tail = os.path.split(tmppath)
        testpath = os.path.join(head, tail.upper())
        return not os.path.exists(testpath)


@contextlib.contextmanager
def temp(path):
    tmphandle, tmppath = tempfile.mkstemp(dir=path)
    os.close(tmphandle)
    try:
        yield tmppath
    finally:
        os.unlink(tmppath)


if __name__ == '__main__':
    path = os.path.abspath(sys.argv[1])
    print(path)
    print('Case sensitive: ' + str(is_case_sensitive(path)))

如果在mkstemp中未指定“dir”参数,则大小写敏感性的问题不明确。您正在测试临时目录所在位置的大小写敏感性,但您可能想了解特定路径的信息。
如果将从mkstemp返回的完整路径转换为大写,您可能会错过路径某个地方的转换。例如,我在Linux上使用vfat挂载了一台USB闪存驱动器,在/media/FLASH下。测试/MEDIA/FLASH中的任何内容的存在将始终失败,因为/media位于(区分大小写的)ext4分区上,但闪存驱动器本身不区分大小写。挂载的网络共享可能是另一种类似情况。
最后,也许这是显而易见的,您需要清理由mkstemp创建的临时文件。

1
import os

if os.path.normcase('A') == os.path.normcase('a'):
    # case insensitive
else:
    # case sensitive

5
至少在Mac OS上是错误的。文件系统不区分大小写,而normcase返回2个不同的结果。 - Romuald Brunet
2
那这会是一个Python的bug吗? - Oberon

0

检查路径的大写/小写变体是否存在是有缺陷的。在撰写本文时,有七个答案依赖于相同的策略:从路径(临时文件、主目录或Python文件本身)开始,然后检查该路径的大小写变体是否存在。即使忽略每个目录的区分大小写配置问题,这种方法也是根本无效的。

为什么这种方法在区分大小写的文件系统上会失败。考虑使用临时文件的方法。当tempfile库返回一个临时文件时,唯一的保证是在创建之前的瞬间,该路径不存在 - 就是这样。如果该路径的文件名部分是FoO,我们对fooFOO或任何其他大小写变体的存在状态一无所知。诚然,tempfile库倾向于返回像TmP5pq3us96这样的名称,它的恶意大小写变体的可能性非常低 - 但我们并不知道。同样的缺陷也影响了使用主目录或Python文件的方法:很可能/HOME/FOO/FOO/BAR/FUBB.PY不存在...但我们没有理由确信。

更好的方法:从你控制的目录开始。一种更健壮的方法是从一个临时目录开始,这个目录在创建时保证为空。在该目录中,您可以执行概念上合理的区分大小写测试。

更好的方法:区分大小写不敏感和大小写保留。对于我正在处理的项目,我需要进行这种区分(并且可以忽略每个目录的大小写敏感设置),因此我最终得出了以下结论。

from functools import cache
from pathlib import Path
from tempfile import TemporaryDirectory

@cache
def file_system_case_sensitivity():
    # Determines the file system's case sensitivity.
    # This approach ignore the complexity of per-directory
    # sensitivity settings supported by some operating systems.
    with TemporaryDirectory() as dpath:
        # Create an empty temp directory.
        # Inside it, touch two differently-cased file names.
        d = Path(dpath)
        f1 = d / 'FoO'
        f2 = d / 'foo'
        f1.touch()
        f2.touch()
        # Ask the file system to report the contents of the temp directory.
        # - If two files, system is case-sensitive.
        # - If the parent reports having 'FoO', case-preserving.
        # - Case-insensitive systems will report having 'foo' or 'FOO'.
        contents = tuple(d.iterdir())
        return (
            'case-sensitive' if len(contents) == 2 else
            'case-preserving' if contents == (f1,) else
            'case-insensitive'
        )

0

我认为我们可以在Python 3.5+上使用pathlib一行代码完成此操作,而无需创建临时文件:

from pathlib import Path

def is_case_insensitive(path) -> bool:
    return Path(str(Path.home()).upper()).exists()

或者反过来:

def is_case_sensitive(path) -> bool:
    return not Path(str(Path.home()).upper()).exists()

如果用户名和他们的主目录实际上在文件系统中是大写的/HOME/FOO,那么这种方法在区分大小写的操作系统上会报告错误。诚然,这是一种奇怪的设置,但我不知道在Linux系统上是否有任何禁止它的东西。 - FMc

-1

我相信这是这个问题最简单的解决方案:

from fnmatch import fnmatch
os_is_case_insensitive = fnmatch('A','a')

来自:https://docs.python.org/3.4/library/fnmatch.html

如果操作系统不区分大小写,则在执行比较之前,两个参数都将被规范化为全小写或全大写。


不幸的是,这并不能处理每个路径的不敏感性。只有@eric-smith的答案似乎在这里起作用。 - Erik Aronesty
不对。OSX是不区分大小写的,但仍然返回了False。 - JL Peyret

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