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.
Check if file system is case-insensitive in Python
4.8k views Asked by Lorin Hochstein AtThere are 10 answers
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()
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.
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.
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.
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?
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'
)
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())