• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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