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