Files
n8n-openai-adapter/lib/elixir/lib/path.ex
T

567 lines
15 KiB
Elixir

defmodule Path do
@moduledoc """
This module provides conveniences for manipulating or
retrieving file system paths.
The functions in this module may receive a char data as
argument (i.e. a string or a list of characters / string)
and will always return a string (encoded in UTF-8).
The majority of the functions in this module do not
interact with the file system, except for a few functions
that require it (like `wildcard/1` and `expand/1`).
"""
alias :filename, as: FN
@type t :: :unicode.chardata()
@doc """
Converts the given path to an absolute one. Unlike
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
## Unix examples
Path.absname("foo")
#=> "/usr/local/foo"
Path.absname("../x")
#=> "/usr/local/../x"
## Windows
Path.absname("foo").
"D:/usr/local/foo"
Path.absname("../x").
"D:/usr/local/../x"
"""
@spec absname(t) :: binary
def absname(path) do
absname(path, System.cwd!)
end
@doc """
Builds a path from `relative_to` to `path`. If `path` is already
an absolute path, `relative_to` is ignored. See also `relative_to/2`.
Unlike `expand/2`, no attempt is made to
resolve `..`, `.` or `~`.
## Examples
iex> Path.absname("foo", "bar")
"bar/foo"
iex> Path.absname("../x", "bar")
"bar/../x"
"""
@spec absname(t, t) :: binary
def absname(path, relative_to) do
path = IO.chardata_to_string(path)
case type(path) do
:relative -> join(relative_to, path)
:absolute ->
cond do
path == "/" ->
path
:binary.last(path) == ?/ ->
binary_part(path, 0, byte_size(path) - 1)
true ->
path
end
:volumerelative ->
relative_to = IO.chardata_to_string(relative_to)
absname_vr(split(path), split(relative_to), relative_to)
end
end
## Absolute path on current drive
defp absname_vr(["/"|rest], [volume|_], _relative),
do: join([volume|rest])
## Relative to current directory on current drive.
defp absname_vr([<<x, ?:>>|rest], [<<x, _ :: binary>>|_], relative),
do: absname(join(rest), relative)
## Relative to current directory on another drive.
defp absname_vr([<<x, ?:>>|name], _, _relative) do
cwd =
case :file.get_cwd([x, ?:]) do
{:ok, dir} -> IO.chardata_to_string(dir)
{:error, _} -> <<x, ?:, ?/>>
end
absname(join(name), cwd)
end
@doc """
Converts the path to an absolute one and expands
any `.` and `..` characters and a leading `~`.
## Examples
Path.expand("/foo/bar/../bar")
"/foo/bar"
"""
@spec expand(t) :: binary
def expand(path) do
normalize absname(expand_home(path), System.cwd!)
end
@doc """
Expands the path relative to the path given as the second argument
expanding any `.` and `..` characters. If the path is already an
absolute path, `relative_to` is ignored.
Note, that this function treats `path` with a leading `~` as
an absolute one.
The second argument is first expanded to an absolute path.
## Examples
# Assuming that the absolute path to baz is /quux/baz
Path.expand("foo/bar/../bar", "baz")
#=> "/quux/baz/foo/bar"
Path.expand("foo/bar/../bar", "/baz")
"/baz/foo/bar"
Path.expand("/foo/bar/../bar", "/baz")
"/foo/bar"
"""
@spec expand(t, t) :: binary
def expand(path, relative_to) do
normalize absname(absname(expand_home(path), expand_home(relative_to)), System.cwd!)
end
@doc """
Returns the path type.
## Unix examples
Path.type("/") #=> :absolute
Path.type("/usr/local/bin") #=> :absolute
Path.type("usr/local/bin") #=> :relative
Path.type("../usr/local/bin") #=> :relative
Path.type("~/file") #=> :relative
## Windows examples
Path.type("D:/usr/local/bin") #=> :absolute
Path.type("usr/local/bin") #=> :relative
Path.type("D:bar.ex") #=> :volumerelative
Path.type("/bar/foo.ex") #=> :volumerelative
"""
@spec type(t) :: :absolute | :relative | :volumerelative
def type(name) when is_list(name) or is_binary(name) do
case :os.type() do
{:win32, _} -> win32_pathtype(name)
_ -> unix_pathtype(name)
end |> elem(0)
end
@doc """
Forces the path to be a relative path.
## Unix examples
Path.relative("/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
Path.relative("../usr/local/bin") #=> "../usr/local/bin"
## Windows examples
Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
Path.relative("D:bar.ex") #=> "bar.ex"
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
"""
@spec relative(t) :: binary
def relative(name) do
case :os.type() do
{:win32, _} -> win32_pathtype(name)
_ -> unix_pathtype(name)
end |> elem(1) |> IO.chardata_to_string
end
defp unix_pathtype(<<?/, relative :: binary>>), do:
{:absolute, relative}
defp unix_pathtype([?/|relative]), do:
{:absolute, relative}
defp unix_pathtype([list|rest]) when is_list(list), do:
unix_pathtype(list ++ rest)
defp unix_pathtype(relative), do:
{:relative, relative}
@slash [?/, ?\\]
defp win32_pathtype([list|rest]) when is_list(list), do:
win32_pathtype(list++rest)
defp win32_pathtype([char, list|rest]) when is_list(list), do:
win32_pathtype([char|list++rest])
defp win32_pathtype(<<c1, c2, relative :: binary>>) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<c, relative :: binary>>) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype(<<_letter, ?:, c, relative :: binary>>) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<_letter, ?:, relative :: binary>>), do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype([c | relative]) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2, list|rest]) when is_list(list), do:
win32_pathtype([c1, c2|list++rest])
defp win32_pathtype([_letter, ?:, c | relative]) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype([_letter, ?: | relative]), do:
{:volumerelative, relative}
defp win32_pathtype(relative), do:
{:relative, relative}
@doc """
Returns the given `path` relative to the given `from` path.
In other words, it tries to strip the `from` prefix from `path`.
This function does not query the file system, so it assumes
no symlinks in between the paths.
In case a direct relative path cannot be found, it returns
the original path.
## Examples
iex> Path.relative_to("/usr/local/foo", "/usr/local")
"foo"
iex> Path.relative_to("/usr/local/foo", "/")
"usr/local/foo"
iex> Path.relative_to("/usr/local/foo", "/etc")
"/usr/local/foo"
"""
@spec relative_to(t, t) :: binary
def relative_to(path, from) do
path = IO.chardata_to_string(path)
relative_to(split(path), split(from), path)
end
defp relative_to([h|t1], [h|t2], original) do
relative_to(t1, t2, original)
end
defp relative_to([_|_] = l1, [], _original) do
join(l1)
end
defp relative_to(_, _, original) do
original
end
@doc """
Convenience to get the path relative to the current working
directory. If, for some reason, the current working directory
cannot be retrieved, returns the full path.
"""
@spec relative_to_cwd(t) :: binary
def relative_to_cwd(path) do
case :file.get_cwd do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base))
_ -> path
end
end
@doc """
Returns the last component of the path or the path
itself if it does not contain any directory separators.
## Examples
iex> Path.basename("foo")
"foo"
iex> Path.basename("foo/bar")
"bar"
iex> Path.basename("/")
""
"""
@spec basename(t) :: binary
def basename(path) do
FN.basename(IO.chardata_to_string(path))
end
@doc """
Returns the last component of `path` with the `extension`
stripped. This function should be used to remove a specific
extension which may, or may not, be there.
## Examples
iex> Path.basename("~/foo/bar.ex", ".ex")
"bar"
iex> Path.basename("~/foo/bar.exs", ".ex")
"bar.exs"
iex> Path.basename("~/foo/bar.old.ex", ".ex")
"bar.old"
"""
@spec basename(t, t) :: binary
def basename(path, extension) do
FN.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Returns the directory component of `path`.
## Examples
Path.dirname("/foo/bar.ex")
#=> "/foo"
Path.dirname("/foo/bar/baz.ex")
#=> "/foo/bar"
"""
@spec dirname(t) :: binary
def dirname(path) do
FN.dirname(IO.chardata_to_string(path))
end
@doc """
Returns the extension of the last component of `path`.
## Examples
iex> Path.extname("foo.erl")
".erl"
iex> Path.extname("~/foo/bar")
""
"""
@spec extname(t) :: binary
def extname(path) do
FN.extension(IO.chardata_to_string(path))
end
@doc """
Returns the `path` with the `extension` stripped.
## Examples
iex> Path.rootname("/foo/bar")
"/foo/bar"
iex> Path.rootname("/foo/bar.ex")
"/foo/bar"
"""
@spec rootname(t) :: binary
def rootname(path) do
FN.rootname(IO.chardata_to_string(path))
end
@doc """
Returns the `path` with the `extension` stripped. This function should be used to
remove a specific extension which might, or might not, be there.
## Examples
iex> Path.rootname("/foo/bar.erl", ".erl")
"/foo/bar"
iex> Path.rootname("/foo/bar.erl", ".ex")
"/foo/bar.erl"
"""
@spec rootname(t, t) :: binary
def rootname(path, extension) do
FN.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Returns a string with one or more path components joined by the path separator.
This function should be used to convert a list of strings to a path.
Note that any trailing slash is removed on join.
## Examples
iex> Path.join(["~", "foo"])
"~/foo"
iex> Path.join(["foo"])
"foo"
iex> Path.join(["/", "foo", "bar/"])
"/foo/bar"
"""
@spec join([t]) :: binary
def join([name1, name2|rest]), do:
join([join(name1, name2)|rest])
def join([name]), do:
do_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
@doc """
Joins two paths.
## Examples
iex> Path.join("foo", "bar")
"foo/bar"
"""
@spec join(t, t) :: binary
def join(left, right),
do: do_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
defp major_os_type do
:os.type |> elem(0)
end
defp do_join(<<uc_letter, ?:, rest :: binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
do_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
defp do_join(<<?\\, rest :: binary>>, relativename, result, :win32), do:
do_join(<<?/, rest :: binary>>, relativename, result, :win32)
defp do_join(<<?/, rest :: binary>>, relativename, [?., ?/|result], os_type), do:
do_join(rest, relativename, [?/|result], os_type)
defp do_join(<<?/, rest :: binary>>, relativename, [?/|result], os_type), do:
do_join(rest, relativename, [?/|result], os_type)
defp do_join(<<>>, <<>>, result, os_type), do:
IO.iodata_to_binary(maybe_remove_dirsep(result, os_type))
defp do_join(<<>>, relativename, [?:|rest], :win32), do:
do_join(relativename, <<>>, [?:|rest], :win32)
defp do_join(<<>>, relativename, [?/|result], os_type), do:
do_join(relativename, <<>>, [?/|result], os_type)
defp do_join(<<>>, relativename, result, os_type), do:
do_join(relativename, <<>>, [?/|result], os_type)
defp do_join(<<char, rest :: binary>>, relativename, result, os_type), do:
do_join(rest, relativename, [char|result], os_type)
defp maybe_remove_dirsep([?/, ?:, letter], :win32), do:
[letter, ?:, ?/]
defp maybe_remove_dirsep([?/], _), do:
[?/]
defp maybe_remove_dirsep([?/|name], _), do:
:lists.reverse(name)
defp maybe_remove_dirsep(name, _), do:
:lists.reverse(name)
@doc """
Returns a list with the path split by the path separator.
If an empty string is given, returns the root path.
## Examples
iex> Path.split("")
[]
iex> Path.split("foo")
["foo"]
iex> Path.split("/foo/bar")
["/", "foo", "bar"]
"""
@spec split(t) :: [binary]
# Work around a bug in Erlang on UNIX
def split(""), do: []
def split(path) do
FN.split(IO.chardata_to_string(path))
end
@doc """
Traverses paths according to the given `glob` expression.
The wildcard looks like an ordinary path, except that certain
"wildcard characters" are interpreted in a special way. The
following characters are special:
* `?` - Matches one character.
* `*` - Matches any number of characters up to the end of
the filename, the next dot, or the next slash.
* `**` - Two adjacent <c>*</c>'s used as a single pattern will
match all files and zero or more directories and subdirectories.
* `[char1,char2,...]` - Matches any of the characters listed. Two characters
separated by a hyphen will match a range of characters.
* `{item1,item2,...}` - Matches one of the alternatives.
Other characters represent themselves. Only paths that have
exactly the same character in the same position will match. Note
that matching is case-sensitive; i.e. "a" will not match "A".
## Examples
Imagine you have a directory called `projects` with three Elixir projects
inside of it: `elixir`, `ex_doc` and `dynamo`. You can find all `.beam` files
inside the `ebin` directory of each project as follows:
Path.wildcard("projects/*/ebin/**/*.beam")
If you want to search for both `.beam` and `.app` files, you could do:
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t) :: [binary]
def wildcard(glob) do
glob
|> chardata_to_list
|> :filelib.wildcard
|> Enum.map(&IO.chardata_to_string/1)
end
# Normalize the given path by expanding "..", "." and "~".
defp chardata_to_list(chardata) do
case :unicode.characters_to_list(chardata) do
result when is_list(result) ->
result
{:error, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
{:incomplete, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
end
end
defp expand_home(type) do
case IO.chardata_to_string(type) do
"~" <> rest -> System.user_home! <> rest
rest -> rest
end
end
defp normalize(path), do: normalize(split(path), [])
defp normalize([".."|t], [_|acc]) do
normalize t, acc
end
defp normalize(["."|t], acc) do
normalize t, acc
end
defp normalize([h|t], acc) do
normalize t, [h|acc]
end
defp normalize([], acc) do
join :lists.reverse(acc)
end
end