Skip to content

Commit cb1b4c5

Browse files
author
José Valim
committed
Add Path.absname and expand ~ on Path.expand
1 parent fda6f0c commit cb1b4c5

File tree

2 files changed

+175
-10
lines changed

2 files changed

+175
-10
lines changed

lib/elixir/lib/path.ex

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
defmodule Path do
2+
defexception NoHomeError,
3+
message: "could not find the user home, please set the HOME environment variable"
4+
25
@doc """
36
This module provides conveniences for manipulating or
47
retrieving filesystem paths.
@@ -9,11 +12,57 @@ defmodule Path do
912
The majority of the functions in this module do not
1013
interact with the file system, unless some few functions
1114
that needs to query the filesystem to retrieve paths
12-
(like `Path.wildcard` and `Path.tmpdir`).
15+
(like `Path.wildcard` and `Path.expand`).
1316
"""
1417

1518
alias :filename, as: FN
1619

20+
@doc """
21+
Converts the given filename and returns an absolute name.
22+
Differently from `Path.expand/1`, no attempt is made to
23+
resolve `..`, `.` or `~`.
24+
25+
## Unix examples
26+
27+
Path.absname("foo")
28+
#=> "/usr/local/foo"
29+
30+
Path.absname("../x")
31+
#=> "/usr/local/../x"
32+
33+
## Windows
34+
35+
Path.absname("foo").
36+
"D:/usr/local/foo"
37+
Path.absname("../x").
38+
"D:/usr/local/../x"
39+
40+
"""
41+
def absname(path) do
42+
FN.absname(path, get_cwd(path))
43+
end
44+
45+
@doc """
46+
Converts the given filename and returns an absolute name
47+
relative to the given location. If the path is already
48+
an absolute path, the relative path is ignored.
49+
50+
Differently from `Path.expand/2`, no attempt is made to
51+
resolve `..`, `.` or `~`.
52+
53+
## Examples
54+
55+
Path.absname("foo", "bar")
56+
#=> "bar/foo"
57+
58+
Path.absname("../x", "bar")
59+
#=> "bar/../x"
60+
61+
"""
62+
def absname(path, relative_to) do
63+
FN.absname(path, relative_to)
64+
end
65+
1766
@doc """
1867
Expands the path by returning its absolute name and expanding
1968
any `.` and `..` characters.
@@ -42,6 +91,46 @@ defmodule Path do
4291
normalize FN.absname(FN.absname(path, relative_to), get_cwd(path))
4392
end
4493

94+
@doc """
95+
Returns the given `path` relative to the given `from` path.
96+
97+
This function does not query the filesystem, so it assumes
98+
no symlinks in between the paths.
99+
100+
In case a direct relative path cannot be found, it returns
101+
the original path.
102+
103+
## Examples
104+
105+
Path.relative_to("/usr/local/foo", "/usr/local") #=> "foo"
106+
Path.relative_to("/usr/local/foo", "/") #=> "foo"
107+
Path.relative_to("/usr/local/foo", "/etc") #=> "/usr/local/foo"
108+
109+
"""
110+
def relative_to(path, from) when is_list(path) and is_binary(from) do
111+
relative_to(FN.split(list_to_binary(path)), FN.split(from), list_to_binary(path))
112+
end
113+
114+
def relative_to(path, from) when is_binary(path) and is_list(from) do
115+
relative_to(FN.split(path), FN.split(list_to_binary(from)), path)
116+
end
117+
118+
def relative_to(path, from) do
119+
relative_to(FN.split(path), FN.split(from), path)
120+
end
121+
122+
defp relative_to([h|t1], [h|t2], original) do
123+
relative_to(t1, t2, original)
124+
end
125+
126+
defp relative_to(t1, [], _original) do
127+
FN.join(t1)
128+
end
129+
130+
defp relative_to(_, _, original) do
131+
original
132+
end
133+
45134
@doc """
46135
Returns the last component of the path or the path
47136
itself if it does not contain any directory separators.
@@ -233,25 +322,54 @@ defmodule Path do
233322

234323
## Helpers
235324

325+
defp get_home do
326+
get_unix_home || get_windows_home || raise NoHomeError
327+
end
328+
329+
defp get_unix_home do
330+
System.get_env("HOME")
331+
end
332+
333+
defp get_windows_home do
334+
System.get_env("USERPROFILE") || (
335+
hd = System.get_env("HOMEDRIVE")
336+
hp = System.get_env("HOMEPATH")
337+
hd && hp && hd <> hp
338+
)
339+
end
340+
236341
defp get_cwd(path) when is_list(path), do: File.cwd! |> binary_to_list
237342
defp get_cwd(_), do: File.cwd!
238343

239-
# Normalize the given path by removing "..".
240-
defp normalize(path), do: normalize(FN.split(path), [])
344+
# Normalize the given path by expanding "..", "." and "~".
345+
346+
defp normalize(path), do: do_normalize(FN.split(path))
347+
348+
defp do_normalize(["~"|t]) do
349+
do_normalize t, [get_home]
350+
end
351+
352+
defp do_normalize(['~'|t]) do
353+
do_normalize t, [get_home |> binary_to_list]
354+
end
355+
356+
defp do_normalize(t) do
357+
do_normalize t, []
358+
end
241359

242-
defp normalize([top|t], [_|acc]) when top in ["..", '..'] do
243-
normalize t, acc
360+
defp do_normalize([top|t], [_|acc]) when top in ["..", '..'] do
361+
do_normalize t, acc
244362
end
245363

246-
defp normalize([top|t], acc) when top in [".", '.'] do
247-
normalize t, acc
364+
defp do_normalize([top|t], acc) when top in [".", '.'] do
365+
do_normalize t, acc
248366
end
249367

250-
defp normalize([h|t], acc) do
251-
normalize t, [h|acc]
368+
defp do_normalize([h|t], acc) do
369+
do_normalize t, [h|acc]
252370
end
253371

254-
defp normalize([], acc) do
372+
defp do_normalize([], acc) do
255373
join Enum.reverse(acc)
256374
end
257375
end

lib/elixir/test/elixir/path_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
1+
Code.require_file "../test_helper.exs", __FILE__
2+
13
defmodule PathTest do
24
use ExUnit.Case, async: true
35

6+
test :absname_with_binary do
7+
assert Path.absname("/foo/bar") == "/foo/bar"
8+
assert Path.absname("/foo/bar/") == "/foo/bar"
9+
assert Path.absname("/foo/bar/../bar") == "/foo/bar/../bar"
10+
11+
assert Path.absname("bar", "/foo") == "/foo/bar"
12+
assert Path.absname("bar/", "/foo") == "/foo/bar"
13+
assert Path.absname("bar/.", "/foo") == "/foo/bar/."
14+
assert Path.absname("bar/../bar", "/foo") == "/foo/bar/../bar"
15+
assert Path.absname("bar/../bar", "foo") == "foo/bar/../bar"
16+
end
17+
18+
test :absname_with_list do
19+
assert Path.absname('/foo/bar') == '/foo/bar'
20+
assert Path.absname('/foo/bar/') == '/foo/bar'
21+
assert Path.absname('/foo/bar/.') == '/foo/bar/.'
22+
assert Path.absname('/foo/bar/../bar') == '/foo/bar/../bar'
23+
end
24+
25+
test :expand_path_with_user_home do
26+
assert is_binary Path.expand("~")
27+
assert is_binary Path.expand("~/foo")
28+
29+
assert is_list Path.expand('~')
30+
assert is_list Path.expand('~/foo')
31+
end
32+
433
test :expand_path_with_binary do
534
assert Path.expand("/foo/bar") == "/foo/bar"
635
assert Path.expand("/foo/bar/") == "/foo/bar"
@@ -24,6 +53,24 @@ defmodule PathTest do
2453
assert Path.expand('/foo/bar/../bar') == '/foo/bar'
2554
end
2655

56+
test :relative_to_with_binary do
57+
assert Path.relative_to("/usr/local/foo", "/usr/local") == "foo"
58+
assert Path.relative_to("/usr/local/foo", "/") == "usr/local/foo"
59+
assert Path.relative_to("/usr/local/foo", "/etc") == "/usr/local/foo"
60+
61+
assert Path.relative_to("usr/local/foo", "usr/local") == "foo"
62+
assert Path.relative_to("usr/local/foo", "etc") == "usr/local/foo"
63+
end
64+
65+
test :relative_to_with_list do
66+
assert Path.relative_to('/usr/local/foo', '/usr/local') == 'foo'
67+
assert Path.relative_to('/usr/local/foo', '/') == 'usr/local/foo'
68+
assert Path.relative_to('/usr/local/foo', '/etc') == '/usr/local/foo'
69+
70+
assert Path.relative_to("usr/local/foo", 'usr/local') == "foo"
71+
assert Path.relative_to('usr/local/foo', "etc") == "usr/local/foo"
72+
end
73+
2774
test :rootname_with_binary do
2875
assert Path.rootname("~/foo/bar.ex", ".ex") == "~/foo/bar"
2976
assert Path.rootname("~/foo/bar.exs", ".ex") == "~/foo/bar.exs"

0 commit comments

Comments
 (0)