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)
View as plain text