1# Copyright 2017 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Skylib module containing file path manipulation functions. 16 17NOTE: The functions in this module currently only support paths with Unix-style 18path separators (forward slash, "/"); they do not handle Windows-style paths 19with backslash separators or drive letters. 20""" 21 22def _basename(p): 23 """Returns the basename (i.e., the file portion) of a path. 24 25 Note that if `p` ends with a slash, this function returns an empty string. 26 This matches the behavior of Python's `os.path.basename`, but differs from 27 the Unix `basename` command (which would return the path segment preceding 28 the final slash). 29 30 Args: 31 p: The path whose basename should be returned. 32 33 Returns: 34 The basename of the path, which includes the extension. 35 """ 36 return p.rpartition("/")[-1] 37 38def _dirname(p): 39 """Returns the dirname of a path. 40 41 The dirname is the portion of `p` up to but not including the file portion 42 (i.e., the basename). Any slashes immediately preceding the basename are not 43 included, unless omitting them would make the dirname empty. 44 45 Args: 46 p: The path whose dirname should be returned. 47 48 Returns: 49 The dirname of the path. 50 """ 51 prefix, sep, _ = p.rpartition("/") 52 if not prefix: 53 return sep 54 else: 55 # If there are multiple consecutive slashes, strip them all out as Python's 56 # os.path.dirname does. 57 return prefix.rstrip("/") 58 59def _is_absolute(path): 60 """Returns `True` if `path` is an absolute path. 61 62 Args: 63 path: A path (which is a string). 64 65 Returns: 66 `True` if `path` is an absolute path. 67 """ 68 return path.startswith("/") or (len(path) > 2 and path[1] == ":") 69 70def _join(path, *others): 71 """Joins one or more path components intelligently. 72 73 This function mimics the behavior of Python's `os.path.join` function on POSIX 74 platform. It returns the concatenation of `path` and any members of `others`, 75 inserting directory separators before each component except the first. The 76 separator is not inserted if the path up until that point is either empty or 77 already ends in a separator. 78 79 If any component is an absolute path, all previous components are discarded. 80 81 Args: 82 path: A path segment. 83 *others: Additional path segments. 84 85 Returns: 86 A string containing the joined paths. 87 """ 88 result = path 89 90 for p in others: 91 if _is_absolute(p): 92 result = p 93 elif not result or result.endswith("/"): 94 result += p 95 else: 96 result += "/" + p 97 98 return result 99 100def _normalize(path): 101 """Normalizes a path, eliminating double slashes and other redundant segments. 102 103 This function mimics the behavior of Python's `os.path.normpath` function on 104 POSIX platforms; specifically: 105 106 - If the entire path is empty, "." is returned. 107 - All "." segments are removed, unless the path consists solely of a single 108 "." segment. 109 - Trailing slashes are removed, unless the path consists solely of slashes. 110 - ".." segments are removed as long as there are corresponding segments 111 earlier in the path to remove; otherwise, they are retained as leading ".." 112 segments. 113 - Single and double leading slashes are preserved, but three or more leading 114 slashes are collapsed into a single leading slash. 115 - Multiple adjacent internal slashes are collapsed into a single slash. 116 117 Args: 118 path: A path. 119 120 Returns: 121 The normalized path. 122 """ 123 if not path: 124 return "." 125 126 if path.startswith("//") and not path.startswith("///"): 127 initial_slashes = 2 128 elif path.startswith("/"): 129 initial_slashes = 1 130 else: 131 initial_slashes = 0 132 is_relative = (initial_slashes == 0) 133 134 components = path.split("/") 135 new_components = [] 136 137 for component in components: 138 if component in ("", "."): 139 continue 140 if component == "..": 141 if new_components and new_components[-1] != "..": 142 # Only pop the last segment if it isn't another "..". 143 new_components.pop() 144 elif is_relative: 145 # Preserve leading ".." segments for relative paths. 146 new_components.append(component) 147 else: 148 new_components.append(component) 149 150 path = "/".join(new_components) 151 if not is_relative: 152 path = ("/" * initial_slashes) + path 153 154 return path or "." 155 156def _relativize(path, start): 157 """Returns the portion of `path` that is relative to `start`. 158 159 Because we do not have access to the underlying file system, this 160 implementation differs slightly from Python's `os.path.relpath` in that it 161 will fail if `path` is not beneath `start` (rather than use parent segments to 162 walk up to the common file system root). 163 164 Relativizing paths that start with parent directory references only works if 165 the path both start with the same initial parent references. 166 167 Args: 168 path: The path to relativize. 169 start: The ancestor path against which to relativize. 170 171 Returns: 172 The portion of `path` that is relative to `start`. 173 """ 174 segments = _normalize(path).split("/") 175 start_segments = _normalize(start).split("/") 176 if start_segments == ["."]: 177 start_segments = [] 178 start_length = len(start_segments) 179 180 if (path.startswith("/") != start.startswith("/") or 181 len(segments) < start_length): 182 fail("Path '%s' is not beneath '%s'" % (path, start)) 183 184 for ancestor_segment, segment in zip(start_segments, segments): 185 if ancestor_segment != segment: 186 fail("Path '%s' is not beneath '%s'" % (path, start)) 187 188 length = len(segments) - start_length 189 result_segments = segments[-length:] 190 return "/".join(result_segments) 191 192def _replace_extension(p, new_extension): 193 """Replaces the extension of the file at the end of a path. 194 195 If the path has no extension, the new extension is added to it. 196 197 Args: 198 p: The path whose extension should be replaced. 199 new_extension: The new extension for the file. The new extension should 200 begin with a dot if you want the new filename to have one. 201 202 Returns: 203 The path with the extension replaced (or added, if it did not have one). 204 """ 205 return _split_extension(p)[0] + new_extension 206 207def _split_extension(p): 208 """Splits the path `p` into a tuple containing the root and extension. 209 210 Leading periods on the basename are ignored, so 211 `path.split_extension(".bashrc")` returns `(".bashrc", "")`. 212 213 Args: 214 p: The path whose root and extension should be split. 215 216 Returns: 217 A tuple `(root, ext)` such that the root is the path without the file 218 extension, and `ext` is the file extension (which, if non-empty, contains 219 the leading dot). The returned tuple always satisfies the relationship 220 `root + ext == p`. 221 """ 222 b = _basename(p) 223 last_dot_in_basename = b.rfind(".") 224 225 # If there is no dot or the only dot in the basename is at the front, then 226 # there is no extension. 227 if last_dot_in_basename <= 0: 228 return (p, "") 229 230 dot_distance_from_end = len(b) - last_dot_in_basename 231 return (p[:-dot_distance_from_end], p[-dot_distance_from_end:]) 232 233paths = struct( 234 basename = _basename, 235 dirname = _dirname, 236 is_absolute = _is_absolute, 237 join = _join, 238 normalize = _normalize, 239 relativize = _relativize, 240 replace_extension = _replace_extension, 241 split_extension = _split_extension, 242) 243