Check if file system is case-insensitive in Python
Asked Answered
S

10

20

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.

Sialagogue answered 23/10, 2011 at 23:29 Comment(0)
M
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.
Mansized answered 23/10, 2011 at 23:38 Comment(5)
What happens if tmppath happens to be all uppercase? Is that guaranteed not to happen by the spec?Sialagogue
@LorinHochstein - the default value for the 'prefix' parameter for mkstemp(), as noted in the comments in the code, is "tmp" (lowercase). docs.python.org/library/tempfile.html#tempfile.mkstemp Thus the generated filename will always begin with 3 lowercase characters.Mansized
@Lorin Hochstein: The docs say that default prefix is 'tmp', you could check that gettempprefix() == gettempprefix.lower() or set prefix in mkstemp() explicitly.Astray
What happens if the temporary file is not in the filesystem with the one of interest?Veinlet
don't forget to delete the file afterwards! os.path.remove(tmppath)During
S
8

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.

Smilax answered 12/4, 2016 at 18:2 Comment(1)
Best solution so far, but the function should take a source directory as an input argument, because, at least on OSX, it's possible for that to vary per path. Not kidding.Warnke
S
4

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?

Smilax answered 14/4, 2016 at 2:7 Comment(0)
T
4

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())
Triable answered 27/6, 2020 at 4:57 Comment(0)
P
3

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())
Peripheral answered 13/11, 2019 at 12:30 Comment(1)
this works on macOS, with the caveat that you might run into issues if running as no-login/no-shell user, which is sometimes done for security reasons for low privilege users on daemon processes (think someone running Django under user nobody for example). That aside, this is the one that doesn't mess things up with a temp file.Triable
V
2
import os

if os.path.normcase('A') == os.path.normcase('a'):
    # case insensitive
else:
    # case sensitive
Vange answered 5/2, 2013 at 13:5 Comment(2)
Wrong on Mac OS at least. Filesystem is case insensitive and normcase returns 2 different resultsDuring
Then this would be a Python bug?Ukrainian
N
2

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.

Nilsanilsen answered 8/3, 2016 at 18:24 Comment(0)
B
0

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()
Brigidbrigida answered 15/4, 2021 at 0:53 Comment(0)
D
0

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'
        )
Diandrous answered 19/3, 2023 at 19:35 Comment(0)
I
-1

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.

Impassioned answered 28/3, 2018 at 21:53 Comment(2)
Unfortunately this doesn't handle per-path insensitivity. Only @eric-smith answer seems to work here.Warnke
Nope. OSX is case-insensitive and returned False anyway.Triable

© 2022 - 2024 — McMap. All rights reserved.