Проверьте, является ли файловая система нечувствительной к регистру в Python

Есть ли простой способ проверить в Python, является ли файловая система нечувствительной к регистру? Я имею в виду, в частности, файловые системы, такие как HFS+ (OSX) и NTFS (Windows), где вы можете получить доступ к тому же файлу, что и foo, Foo или FOO, даже если регистр файлов сохранен.

10 ответов

Решение
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.

Ответ, предоставленный 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)

Который немного медленнее, если вызывается только один раз, и значительно быстрее в любом другом случае.

Вариант ответа @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

FWIW, проверил pathlib, os, os.pathсодержимое через:

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

и ничто не похоже на это, хотя у него есть pathlib.supports_symlinks но ничего о чувствительности к регистру.

Хороший вопрос по различным файловым системам и т. Д. Эрик Смит. Но почему бы не использовать tempfile.NamedTeilitaryFile с параметром dir и избегать того, чтобы весь этот контекстный менеджер поднимал себя?

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

Я должен также упомянуть, что ваше решение не гарантирует, что вы действительно проверяете чувствительность к регистру. Если вы не проверите префикс по умолчанию (используя tempfile.gettempprefix()), чтобы убедиться, что он содержит символ нижнего регистра. Поэтому включение префикса здесь не является обязательным.

Ваше решение очищает временный файл. Я согласен, что это казалось очевидным, но никто не знает, не так ли?

Я думаю, что есть гораздо более простое (и, вероятно, более быстрое) решение этой проблемы. Следующее, казалось, работало там, где я тестировал:

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

Начиная с ответа Амбер, я придумал этот код. Я не уверен, что он полностью устойчив, но он пытается решить некоторые проблемы в оригинале (о которых я расскажу ниже).

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)))

Без указания dir параметр в mkstempвопрос чувствительности к регистру неясен. Вы проверяете чувствительность к регистру, где бы ни находился временный каталог, но вы, возможно, захотите узнать о конкретном пути.

Если вы преобразуете полный путь, возвращенный из mkstemp в верхнем регистре, вы можете пропустить переход где-нибудь на пути. Например, у меня есть флешка в Linux, смонтированная с помощью vfat на /media/FLASH, Проверка существования чего-либо под /MEDIA/FLASH всегда потерпит неудачу, потому что /media находится на (чувствительном к регистру) разделе ext4, но сама флешка нечувствительна к регистру. Установленные сетевые ресурсы могут быть другой ситуацией, подобной этой.

Наконец, и, возможно, само собой разумеется, в ответе Амбер вы захотите очистить временный файл, созданный mk stemp.

import os

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

Проверка существования варианта пути в верхнем/строчном регистре ошибочна. На момент написания этой статьи существовало семь ответов, основанных на одной и той же стратегии: начать с пути (временный файл, домашний каталог или сам файл Python), а затем проверить наличие варианта этого пути с измененным регистром. . Даже если оставить в стороне проблему настройки чувствительности к регистру для каждого каталога, этот подход в корне неверен.

Почему этот подход не работает в файловых системах, чувствительных к регистру. Рассмотрим подход к использованию временных файлов. Когда библиотека возвращает временный файл, единственной гарантией является то, что на момент создания путь не существовал – и все. Если часть имени файла этого пути, мы ничего не знаем о статусе существования,или любой другой вариант регистра. Конечно,библиотека имеет тенденцию возвращать такие имена, каки вероятность того, что его злой близнец с измененным регистром существует, очень мала – но мы этого не знаем. Тот же недостаток касается подходов с использованием домашнего каталога или файла Python: по всей вероятности,илине существуют... но у нас нет оснований предполагать это с уверенностью.

Лучший подход: начните с каталога, которым вы управляете. Более надежный подход — начать с временного каталога, который гарантированно будет пуст в момент создания. В этом каталоге вы можете выполнить концептуально обоснованные тесты на чувствительность к регистру.

Лучший подход: различать регистронезависимые и регистро-сохраняющие. Для проекта, над которым я работаю, мне нужно провести это различие (и я могу игнорировать настройки чувствительности к регистру для каждого каталога), поэтому в итоге я получил следующее.

      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'
        )

Я думаю, мы можем сделать это заодно с pathlib на Python 3.5+ без создания временных файлов:

      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()

Я считаю, что это самое простое решение вопроса:

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

От: https://docs.python.org/3.4/library/fnmatch.html

Если операционная система нечувствительна к регистру, то перед выполнением сравнения оба параметра будут нормализованы для всех строчных и прописных букв.

Другие вопросы по тегам