Check if file system is case-insensitive in Python

4.8k Views Asked by At

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.

10

There are 10 best solutions below

5
On BEST ANSWER
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.
0
On

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

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.

0
On

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.

1
On

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

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.

0
On

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?

2
On
import os

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

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

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