Check if file system is case-insensitive in Python

Question:

Is there a simple way to check in Python if a file system is case insensitive? I’m thinking in particular of file systems like HFS+ (OSX) and NTFS (Windows), where you can access the same file as foo, Foo or FOO, even though the file case is preserved.

Asked By: Lorin Hochstein

||

Answers:

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.
Answered By: Amber
import os

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

Starting with Amber’s answer, I came up with this code. I’m not sure it is totally robust, but it attempts to address some issues in the original (that I’ll mention below).

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

Without specifying the dir parameter in mkstemp, the question of case sensitivity is vague. You’re testing case sensitivity of wherever the temporary directory happens to be, but you may want to know about a specific path.

If you convert the full path returned from mkstemp to upper-case, you could potentially miss a transition somewhere in the path. For example, I have a USB flash drive on Linux mounted using vfat at /media/FLASH. Testing the existence of anything under /MEDIA/FLASH will always fail because /media is on a (case-sensitive) ext4 partition, but the flash drive itself is case-insensitive. Mounted network shares could be another situation like this.

Finally, and maybe it goes without saying in Amber’s answer, you’ll want to clean up the temp file created by mkstemp.

Answered By: Eric Smith

The answer provided by Amber will leave temporary file debris unless closing and deleting are handled explicitly. To avoid this I use:

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

Though my usage cases generally test this more than once, so I stash the result to avoid having to touch the filesystem more than once.

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)

Which is marginally slower if only called once, and significantly faster in every other case.

Answered By: Steve Cohen

Good point on the different file systems, etc., Eric Smith. But why not use tempfile.NamedTemporaryFile with the dir parameter and avoid doing all that context manager lifting yourself?

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

I should also mention that your solution does not guarantee that you are actually testing for case sensitivity. Unless you check the default prefix (using tempfile.gettempprefix()) to make sure it contains a lower-case character. So including the prefix here is not really optional.

Your solution cleans up the temp file. I agree that it seemed obvious, but one never knows, do one?

Answered By: Steve Cohen

I believe this to be the simplest solution to the question:

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

From: https://docs.python.org/3.4/library/fnmatch.html

If the operating system is case-insensitive, then both parameters will
be normalized to all lower- or upper-case before the comparison is
performed.

Answered By: Drone Brain

I think there’s a much simpler (and probably faster) solution to this. The following seemed to be working for where I tested:

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

Variation on @Shrikant’s answer, applicable within a module (i.e. not in the REPL), even if your user doesn’t have a home:

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

output (macOS):

is_fs_case_insensitive=True  

And the Linux side of things:

(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, I checked pathlib, os, os.path‘s contents via:

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

and nothing looks like it, though it does have a pathlib.supports_symlinks but nothing about case-sensitivity.

And the following will work in the REPL as well:

is_fs_case_insensitive = os.path.exists(os.path.__file__.upper()) and os.path.exists(os.path.__file__.lower())
Answered By: JL Peyret

I think we can do this in one line with pathlib on Python 3.5+ without creating temporary files:

from pathlib import Path

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

Or for the inverse:

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

Checking for the existence of an uppercase/lowercase variant of a path
is flawed
. At the time of this writing, there are seven answers that rely on
the same strategy: start with a path (temp file, home directory, or the Python
file itself) and then check for the existence of a case-altered variant of that
path. Even setting aside the issue of per-directory case-sensitivity
configuration, that approach is fundamentally invalid.

Why the approach fails on case-sensitive file systems. Consider the temp
file approach. When the tempfile library returns a temp file, the only
guarantee is that at the instant before creation, the path did not
exist – that’s it. If the file-name
portion of that path is FoO, we know nothing about the existence status of
foo, FOO, or any other case-variant. Granted, the tempfile library tends
to return names like TmP5pq3us96 and the odds are very low that its evil
case-altered twin exists – but we don’t know that. The same flaw affects the
approaches using the home directory or the Python file: in all likelihood,
/HOME/FOO or /FOO/BAR/FUBB.PY do not exist … but we have no reason to
assume that with certainty.

A better approach: start with a directory that you control. A more robust
approach is to begin with a temp directory, which is guaranteed to be empty at
the moment of creation. Within that directory, you can perform conceptually
sound tests for case sensitivity.

A better approach: distinguish between case-insensitive and
case-preserving
. For a project I’m working on, I need to make that
distinction (and I can ignore per-directory case-sensitivity settings), so I
ended up with the following.

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'
        )
Answered By: FMc
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.