Home | History | Annotate | Download | only in Lib
      1 # Module 'ntpath' -- common operations on WinNT/Win95 pathnames
      2 """Common pathname manipulations, WindowsNT/95 version.
      3 
      4 Instead of importing this module directly, import os and refer to this
      5 module as os.path.
      6 """
      7 
      8 import os
      9 import sys
     10 import stat
     11 import genericpath
     12 from genericpath import *
     13 
     14 __all__ = ["normcase","isabs","join","splitdrive","split","splitext",
     15            "basename","dirname","commonprefix","getsize","getmtime",
     16            "getatime","getctime", "islink","exists","lexists","isdir","isfile",
     17            "ismount", "expanduser","expandvars","normpath","abspath",
     18            "splitunc","curdir","pardir","sep","pathsep","defpath","altsep",
     19            "extsep","devnull","realpath","supports_unicode_filenames","relpath",
     20            "samefile", "sameopenfile", "samestat", "commonpath"]
     21 
     22 # strings representing various path-related bits and pieces
     23 # These are primarily for export; internally, they are hardcoded.
     24 curdir = '.'
     25 pardir = '..'
     26 extsep = '.'
     27 sep = '\\'
     28 pathsep = ';'
     29 altsep = '/'
     30 defpath = '.;C:\\bin'
     31 devnull = 'nul'
     32 
     33 def _get_bothseps(path):
     34     if isinstance(path, bytes):
     35         return b'\\/'
     36     else:
     37         return '\\/'
     38 
     39 # Normalize the case of a pathname and map slashes to backslashes.
     40 # Other normalizations (such as optimizing '../' away) are not done
     41 # (this is done by normpath).
     42 
     43 def normcase(s):
     44     """Normalize case of pathname.
     45 
     46     Makes all characters lowercase and all slashes into backslashes."""
     47     s = os.fspath(s)
     48     try:
     49         if isinstance(s, bytes):
     50             return s.replace(b'/', b'\\').lower()
     51         else:
     52             return s.replace('/', '\\').lower()
     53     except (TypeError, AttributeError):
     54         if not isinstance(s, (bytes, str)):
     55             raise TypeError("normcase() argument must be str or bytes, "
     56                             "not %r" % s.__class__.__name__) from None
     57         raise
     58 
     59 
     60 # Return whether a path is absolute.
     61 # Trivial in Posix, harder on Windows.
     62 # For Windows it is absolute if it starts with a slash or backslash (current
     63 # volume), or if a pathname after the volume-letter-and-colon or UNC-resource
     64 # starts with a slash or backslash.
     65 
     66 def isabs(s):
     67     """Test whether a path is absolute"""
     68     s = os.fspath(s)
     69     s = splitdrive(s)[1]
     70     return len(s) > 0 and s[0] in _get_bothseps(s)
     71 
     72 
     73 # Join two (or more) paths.
     74 def join(path, *paths):
     75     path = os.fspath(path)
     76     if isinstance(path, bytes):
     77         sep = b'\\'
     78         seps = b'\\/'
     79         colon = b':'
     80     else:
     81         sep = '\\'
     82         seps = '\\/'
     83         colon = ':'
     84     try:
     85         if not paths:
     86             path[:0] + sep  #23780: Ensure compatible data type even if p is null.
     87         result_drive, result_path = splitdrive(path)
     88         for p in map(os.fspath, paths):
     89             p_drive, p_path = splitdrive(p)
     90             if p_path and p_path[0] in seps:
     91                 # Second path is absolute
     92                 if p_drive or not result_drive:
     93                     result_drive = p_drive
     94                 result_path = p_path
     95                 continue
     96             elif p_drive and p_drive != result_drive:
     97                 if p_drive.lower() != result_drive.lower():
     98                     # Different drives => ignore the first path entirely
     99                     result_drive = p_drive
    100                     result_path = p_path
    101                     continue
    102                 # Same drive in different case
    103                 result_drive = p_drive
    104             # Second path is relative to the first
    105             if result_path and result_path[-1] not in seps:
    106                 result_path = result_path + sep
    107             result_path = result_path + p_path
    108         ## add separator between UNC and non-absolute path
    109         if (result_path and result_path[0] not in seps and
    110             result_drive and result_drive[-1:] != colon):
    111             return result_drive + sep + result_path
    112         return result_drive + result_path
    113     except (TypeError, AttributeError, BytesWarning):
    114         genericpath._check_arg_types('join', path, *paths)
    115         raise
    116 
    117 
    118 # Split a path in a drive specification (a drive letter followed by a
    119 # colon) and the path specification.
    120 # It is always true that drivespec + pathspec == p
    121 def splitdrive(p):
    122     """Split a pathname into drive/UNC sharepoint and relative path specifiers.
    123     Returns a 2-tuple (drive_or_unc, path); either part may be empty.
    124 
    125     If you assign
    126         result = splitdrive(p)
    127     It is always true that:
    128         result[0] + result[1] == p
    129 
    130     If the path contained a drive letter, drive_or_unc will contain everything
    131     up to and including the colon.  e.g. splitdrive("c:/dir") returns ("c:", "/dir")
    132 
    133     If the path contained a UNC path, the drive_or_unc will contain the host name
    134     and share up to but not including the fourth directory separator character.
    135     e.g. splitdrive("//host/computer/dir") returns ("//host/computer", "/dir")
    136 
    137     Paths cannot contain both a drive letter and a UNC path.
    138 
    139     """
    140     p = os.fspath(p)
    141     if len(p) >= 2:
    142         if isinstance(p, bytes):
    143             sep = b'\\'
    144             altsep = b'/'
    145             colon = b':'
    146         else:
    147             sep = '\\'
    148             altsep = '/'
    149             colon = ':'
    150         normp = p.replace(altsep, sep)
    151         if (normp[0:2] == sep*2) and (normp[2:3] != sep):
    152             # is a UNC path:
    153             # vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
    154             # \\machine\mountpoint\directory\etc\...
    155             #           directory ^^^^^^^^^^^^^^^
    156             index = normp.find(sep, 2)
    157             if index == -1:
    158                 return p[:0], p
    159             index2 = normp.find(sep, index + 1)
    160             # a UNC path can't have two slashes in a row
    161             # (after the initial two)
    162             if index2 == index + 1:
    163                 return p[:0], p
    164             if index2 == -1:
    165                 index2 = len(p)
    166             return p[:index2], p[index2:]
    167         if normp[1:2] == colon:
    168             return p[:2], p[2:]
    169     return p[:0], p
    170 
    171 
    172 # Parse UNC paths
    173 def splitunc(p):
    174     """Deprecated since Python 3.1.  Please use splitdrive() instead;
    175     it now handles UNC paths.
    176 
    177     Split a pathname into UNC mount point and relative path specifiers.
    178 
    179     Return a 2-tuple (unc, rest); either part may be empty.
    180     If unc is not empty, it has the form '//host/mount' (or similar
    181     using backslashes).  unc+rest is always the input path.
    182     Paths containing drive letters never have a UNC part.
    183     """
    184     import warnings
    185     warnings.warn("ntpath.splitunc is deprecated, use ntpath.splitdrive instead",
    186                   DeprecationWarning, 2)
    187     drive, path = splitdrive(p)
    188     if len(drive) == 2:
    189          # Drive letter present
    190         return p[:0], p
    191     return drive, path
    192 
    193 
    194 # Split a path in head (everything up to the last '/') and tail (the
    195 # rest).  After the trailing '/' is stripped, the invariant
    196 # join(head, tail) == p holds.
    197 # The resulting head won't end in '/' unless it is the root.
    198 
    199 def split(p):
    200     """Split a pathname.
    201 
    202     Return tuple (head, tail) where tail is everything after the final slash.
    203     Either part may be empty."""
    204     p = os.fspath(p)
    205     seps = _get_bothseps(p)
    206     d, p = splitdrive(p)
    207     # set i to index beyond p's last slash
    208     i = len(p)
    209     while i and p[i-1] not in seps:
    210         i -= 1
    211     head, tail = p[:i], p[i:]  # now tail has no slashes
    212     # remove trailing slashes from head, unless it's all slashes
    213     head = head.rstrip(seps) or head
    214     return d + head, tail
    215 
    216 
    217 # Split a path in root and extension.
    218 # The extension is everything starting at the last dot in the last
    219 # pathname component; the root is everything before that.
    220 # It is always true that root + ext == p.
    221 
    222 def splitext(p):
    223     p = os.fspath(p)
    224     if isinstance(p, bytes):
    225         return genericpath._splitext(p, b'\\', b'/', b'.')
    226     else:
    227         return genericpath._splitext(p, '\\', '/', '.')
    228 splitext.__doc__ = genericpath._splitext.__doc__
    229 
    230 
    231 # Return the tail (basename) part of a path.
    232 
    233 def basename(p):
    234     """Returns the final component of a pathname"""
    235     return split(p)[1]
    236 
    237 
    238 # Return the head (dirname) part of a path.
    239 
    240 def dirname(p):
    241     """Returns the directory component of a pathname"""
    242     return split(p)[0]
    243 
    244 # Is a path a symbolic link?
    245 # This will always return false on systems where os.lstat doesn't exist.
    246 
    247 def islink(path):
    248     """Test whether a path is a symbolic link.
    249     This will always return false for Windows prior to 6.0.
    250     """
    251     try:
    252         st = os.lstat(path)
    253     except (OSError, AttributeError):
    254         return False
    255     return stat.S_ISLNK(st.st_mode)
    256 
    257 # Being true for dangling symbolic links is also useful.
    258 
    259 def lexists(path):
    260     """Test whether a path exists.  Returns True for broken symbolic links"""
    261     try:
    262         st = os.lstat(path)
    263     except OSError:
    264         return False
    265     return True
    266 
    267 # Is a path a mount point?
    268 # Any drive letter root (eg c:\)
    269 # Any share UNC (eg \\server\share)
    270 # Any volume mounted on a filesystem folder
    271 #
    272 # No one method detects all three situations. Historically we've lexically
    273 # detected drive letter roots and share UNCs. The canonical approach to
    274 # detecting mounted volumes (querying the reparse tag) fails for the most
    275 # common case: drive letter roots. The alternative which uses GetVolumePathName
    276 # fails if the drive letter is the result of a SUBST.
    277 try:
    278     from nt import _getvolumepathname
    279 except ImportError:
    280     _getvolumepathname = None
    281 def ismount(path):
    282     """Test whether a path is a mount point (a drive root, the root of a
    283     share, or a mounted volume)"""
    284     path = os.fspath(path)
    285     seps = _get_bothseps(path)
    286     path = abspath(path)
    287     root, rest = splitdrive(path)
    288     if root and root[0] in seps:
    289         return (not rest) or (rest in seps)
    290     if rest in seps:
    291         return True
    292 
    293     if _getvolumepathname:
    294         return path.rstrip(seps) == _getvolumepathname(path).rstrip(seps)
    295     else:
    296         return False
    297 
    298 
    299 # Expand paths beginning with '~' or '~user'.
    300 # '~' means $HOME; '~user' means that user's home directory.
    301 # If the path doesn't begin with '~', or if the user or $HOME is unknown,
    302 # the path is returned unchanged (leaving error reporting to whatever
    303 # function is called with the expanded path as argument).
    304 # See also module 'glob' for expansion of *, ? and [...] in pathnames.
    305 # (A function should also be defined to do full *sh-style environment
    306 # variable expansion.)
    307 
    308 def expanduser(path):
    309     """Expand ~ and ~user constructs.
    310 
    311     If user or $HOME is unknown, do nothing."""
    312     path = os.fspath(path)
    313     if isinstance(path, bytes):
    314         tilde = b'~'
    315     else:
    316         tilde = '~'
    317     if not path.startswith(tilde):
    318         return path
    319     i, n = 1, len(path)
    320     while i < n and path[i] not in _get_bothseps(path):
    321         i += 1
    322 
    323     if 'HOME' in os.environ:
    324         userhome = os.environ['HOME']
    325     elif 'USERPROFILE' in os.environ:
    326         userhome = os.environ['USERPROFILE']
    327     elif not 'HOMEPATH' in os.environ:
    328         return path
    329     else:
    330         try:
    331             drive = os.environ['HOMEDRIVE']
    332         except KeyError:
    333             drive = ''
    334         userhome = join(drive, os.environ['HOMEPATH'])
    335 
    336     if isinstance(path, bytes):
    337         userhome = os.fsencode(userhome)
    338 
    339     if i != 1: #~user
    340         userhome = join(dirname(userhome), path[1:i])
    341 
    342     return userhome + path[i:]
    343 
    344 
    345 # Expand paths containing shell variable substitutions.
    346 # The following rules apply:
    347 #       - no expansion within single quotes
    348 #       - '$$' is translated into '$'
    349 #       - '%%' is translated into '%' if '%%' are not seen in %var1%%var2%
    350 #       - ${varname} is accepted.
    351 #       - $varname is accepted.
    352 #       - %varname% is accepted.
    353 #       - varnames can be made out of letters, digits and the characters '_-'
    354 #         (though is not verified in the ${varname} and %varname% cases)
    355 # XXX With COMMAND.COM you can use any characters in a variable name,
    356 # XXX except '^|<>='.
    357 
    358 def expandvars(path):
    359     """Expand shell variables of the forms $var, ${var} and %var%.
    360 
    361     Unknown variables are left unchanged."""
    362     path = os.fspath(path)
    363     if isinstance(path, bytes):
    364         if b'$' not in path and b'%' not in path:
    365             return path
    366         import string
    367         varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
    368         quote = b'\''
    369         percent = b'%'
    370         brace = b'{'
    371         rbrace = b'}'
    372         dollar = b'$'
    373         environ = getattr(os, 'environb', None)
    374     else:
    375         if '$' not in path and '%' not in path:
    376             return path
    377         import string
    378         varchars = string.ascii_letters + string.digits + '_-'
    379         quote = '\''
    380         percent = '%'
    381         brace = '{'
    382         rbrace = '}'
    383         dollar = '$'
    384         environ = os.environ
    385     res = path[:0]
    386     index = 0
    387     pathlen = len(path)
    388     while index < pathlen:
    389         c = path[index:index+1]
    390         if c == quote:   # no expansion within single quotes
    391             path = path[index + 1:]
    392             pathlen = len(path)
    393             try:
    394                 index = path.index(c)
    395                 res += c + path[:index + 1]
    396             except ValueError:
    397                 res += c + path
    398                 index = pathlen - 1
    399         elif c == percent:  # variable or '%'
    400             if path[index + 1:index + 2] == percent:
    401                 res += c
    402                 index += 1
    403             else:
    404                 path = path[index+1:]
    405                 pathlen = len(path)
    406                 try:
    407                     index = path.index(percent)
    408                 except ValueError:
    409                     res += percent + path
    410                     index = pathlen - 1
    411                 else:
    412                     var = path[:index]
    413                     try:
    414                         if environ is None:
    415                             value = os.fsencode(os.environ[os.fsdecode(var)])
    416                         else:
    417                             value = environ[var]
    418                     except KeyError:
    419                         value = percent + var + percent
    420                     res += value
    421         elif c == dollar:  # variable or '$$'
    422             if path[index + 1:index + 2] == dollar:
    423                 res += c
    424                 index += 1
    425             elif path[index + 1:index + 2] == brace:
    426                 path = path[index+2:]
    427                 pathlen = len(path)
    428                 try:
    429                     index = path.index(rbrace)
    430                 except ValueError:
    431                     res += dollar + brace + path
    432                     index = pathlen - 1
    433                 else:
    434                     var = path[:index]
    435                     try:
    436                         if environ is None:
    437                             value = os.fsencode(os.environ[os.fsdecode(var)])
    438                         else:
    439                             value = environ[var]
    440                     except KeyError:
    441                         value = dollar + brace + var + rbrace
    442                     res += value
    443             else:
    444                 var = path[:0]
    445                 index += 1
    446                 c = path[index:index + 1]
    447                 while c and c in varchars:
    448                     var += c
    449                     index += 1
    450                     c = path[index:index + 1]
    451                 try:
    452                     if environ is None:
    453                         value = os.fsencode(os.environ[os.fsdecode(var)])
    454                     else:
    455                         value = environ[var]
    456                 except KeyError:
    457                     value = dollar + var
    458                 res += value
    459                 if c:
    460                     index -= 1
    461         else:
    462             res += c
    463         index += 1
    464     return res
    465 
    466 
    467 # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
    468 # Previously, this function also truncated pathnames to 8+3 format,
    469 # but as this module is called "ntpath", that's obviously wrong!
    470 
    471 def normpath(path):
    472     """Normalize path, eliminating double slashes, etc."""
    473     path = os.fspath(path)
    474     if isinstance(path, bytes):
    475         sep = b'\\'
    476         altsep = b'/'
    477         curdir = b'.'
    478         pardir = b'..'
    479         special_prefixes = (b'\\\\.\\', b'\\\\?\\')
    480     else:
    481         sep = '\\'
    482         altsep = '/'
    483         curdir = '.'
    484         pardir = '..'
    485         special_prefixes = ('\\\\.\\', '\\\\?\\')
    486     if path.startswith(special_prefixes):
    487         # in the case of paths with these prefixes:
    488         # \\.\ -> device names
    489         # \\?\ -> literal paths
    490         # do not do any normalization, but return the path unchanged
    491         return path
    492     path = path.replace(altsep, sep)
    493     prefix, path = splitdrive(path)
    494 
    495     # collapse initial backslashes
    496     if path.startswith(sep):
    497         prefix += sep
    498         path = path.lstrip(sep)
    499 
    500     comps = path.split(sep)
    501     i = 0
    502     while i < len(comps):
    503         if not comps[i] or comps[i] == curdir:
    504             del comps[i]
    505         elif comps[i] == pardir:
    506             if i > 0 and comps[i-1] != pardir:
    507                 del comps[i-1:i+1]
    508                 i -= 1
    509             elif i == 0 and prefix.endswith(sep):
    510                 del comps[i]
    511             else:
    512                 i += 1
    513         else:
    514             i += 1
    515     # If the path is now empty, substitute '.'
    516     if not prefix and not comps:
    517         comps.append(curdir)
    518     return prefix + sep.join(comps)
    519 
    520 
    521 # Return an absolute path.
    522 try:
    523     from nt import _getfullpathname
    524 
    525 except ImportError: # not running on Windows - mock up something sensible
    526     def abspath(path):
    527         """Return the absolute version of a path."""
    528         path = os.fspath(path)
    529         if not isabs(path):
    530             if isinstance(path, bytes):
    531                 cwd = os.getcwdb()
    532             else:
    533                 cwd = os.getcwd()
    534             path = join(cwd, path)
    535         return normpath(path)
    536 
    537 else:  # use native Windows method on Windows
    538     def abspath(path):
    539         """Return the absolute version of a path."""
    540 
    541         if path: # Empty path must return current working directory.
    542             path = os.fspath(path)
    543             try:
    544                 path = _getfullpathname(path)
    545             except OSError:
    546                 pass # Bad path - return unchanged.
    547         elif isinstance(path, bytes):
    548             path = os.getcwdb()
    549         else:
    550             path = os.getcwd()
    551         return normpath(path)
    552 
    553 # realpath is a no-op on systems without islink support
    554 realpath = abspath
    555 # Win9x family and earlier have no Unicode filename support.
    556 supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
    557                               sys.getwindowsversion()[3] >= 2)
    558 
    559 def relpath(path, start=None):
    560     """Return a relative version of a path"""
    561     path = os.fspath(path)
    562     if isinstance(path, bytes):
    563         sep = b'\\'
    564         curdir = b'.'
    565         pardir = b'..'
    566     else:
    567         sep = '\\'
    568         curdir = '.'
    569         pardir = '..'
    570 
    571     if start is None:
    572         start = curdir
    573 
    574     if not path:
    575         raise ValueError("no path specified")
    576 
    577     start = os.fspath(start)
    578     try:
    579         start_abs = abspath(normpath(start))
    580         path_abs = abspath(normpath(path))
    581         start_drive, start_rest = splitdrive(start_abs)
    582         path_drive, path_rest = splitdrive(path_abs)
    583         if normcase(start_drive) != normcase(path_drive):
    584             raise ValueError("path is on mount %r, start on mount %r" % (
    585                 path_drive, start_drive))
    586 
    587         start_list = [x for x in start_rest.split(sep) if x]
    588         path_list = [x for x in path_rest.split(sep) if x]
    589         # Work out how much of the filepath is shared by start and path.
    590         i = 0
    591         for e1, e2 in zip(start_list, path_list):
    592             if normcase(e1) != normcase(e2):
    593                 break
    594             i += 1
    595 
    596         rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
    597         if not rel_list:
    598             return curdir
    599         return join(*rel_list)
    600     except (TypeError, ValueError, AttributeError, BytesWarning, DeprecationWarning):
    601         genericpath._check_arg_types('relpath', path, start)
    602         raise
    603 
    604 
    605 # Return the longest common sub-path of the sequence of paths given as input.
    606 # The function is case-insensitive and 'separator-insensitive', i.e. if the
    607 # only difference between two paths is the use of '\' versus '/' as separator,
    608 # they are deemed to be equal.
    609 #
    610 # However, the returned path will have the standard '\' separator (even if the
    611 # given paths had the alternative '/' separator) and will have the case of the
    612 # first path given in the sequence. Additionally, any trailing separator is
    613 # stripped from the returned path.
    614 
    615 def commonpath(paths):
    616     """Given a sequence of path names, returns the longest common sub-path."""
    617 
    618     if not paths:
    619         raise ValueError('commonpath() arg is an empty sequence')
    620 
    621     paths = tuple(map(os.fspath, paths))
    622     if isinstance(paths[0], bytes):
    623         sep = b'\\'
    624         altsep = b'/'
    625         curdir = b'.'
    626     else:
    627         sep = '\\'
    628         altsep = '/'
    629         curdir = '.'
    630 
    631     try:
    632         drivesplits = [splitdrive(p.replace(altsep, sep).lower()) for p in paths]
    633         split_paths = [p.split(sep) for d, p in drivesplits]
    634 
    635         try:
    636             isabs, = set(p[:1] == sep for d, p in drivesplits)
    637         except ValueError:
    638             raise ValueError("Can't mix absolute and relative paths") from None
    639 
    640         # Check that all drive letters or UNC paths match. The check is made only
    641         # now otherwise type errors for mixing strings and bytes would not be
    642         # caught.
    643         if len(set(d for d, p in drivesplits)) != 1:
    644             raise ValueError("Paths don't have the same drive")
    645 
    646         drive, path = splitdrive(paths[0].replace(altsep, sep))
    647         common = path.split(sep)
    648         common = [c for c in common if c and c != curdir]
    649 
    650         split_paths = [[c for c in s if c and c != curdir] for s in split_paths]
    651         s1 = min(split_paths)
    652         s2 = max(split_paths)
    653         for i, c in enumerate(s1):
    654             if c != s2[i]:
    655                 common = common[:i]
    656                 break
    657         else:
    658             common = common[:len(s1)]
    659 
    660         prefix = drive + sep if isabs else drive
    661         return prefix + sep.join(common)
    662     except (TypeError, AttributeError):
    663         genericpath._check_arg_types('commonpath', *paths)
    664         raise
    665 
    666 
    667 # determine if two files are in fact the same file
    668 try:
    669     # GetFinalPathNameByHandle is available starting with Windows 6.0.
    670     # Windows XP and non-Windows OS'es will mock _getfinalpathname.
    671     if sys.getwindowsversion()[:2] >= (6, 0):
    672         from nt import _getfinalpathname
    673     else:
    674         raise ImportError
    675 except (AttributeError, ImportError):
    676     # On Windows XP and earlier, two files are the same if their absolute
    677     # pathnames are the same.
    678     # Non-Windows operating systems fake this method with an XP
    679     # approximation.
    680     def _getfinalpathname(f):
    681         return normcase(abspath(f))
    682 
    683 
    684 try:
    685     # The genericpath.isdir implementation uses os.stat and checks the mode
    686     # attribute to tell whether or not the path is a directory.
    687     # This is overkill on Windows - just pass the path to GetFileAttributes
    688     # and check the attribute from there.
    689     from nt import _isdir as isdir
    690 except ImportError:
    691     # Use genericpath.isdir as imported above.
    692     pass
    693