Files
n8n-openai-adapter/lib/elixir/lib/path.ex
T
2021-05-31 21:14:57 +02:00

725 lines
20 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 chardata as
argument (i.e. a string or a list of characters / string)
and will always return a string (encoded in UTF-8). If a binary
is given, in whatever encoding, its encoding will be kept.
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/2` and `expand/1`).
"""
@type t :: IO.chardata()
@doc """
Converts the given path to an absolute one. Unlike
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
## Examples
### Unix-like operating systems
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, File.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 ->
absname_join([relative_to, path])
:absolute ->
absname_join([path])
: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: absname_join([volume | rest])
# Relative to current directory on current drive
defp absname_vr([<<x, ?:>> | rest], [<<x, _::binary>> | _], relative),
do: absname(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(absname_join(name), cwd)
end
@slash [?/, ?\\]
defp absname_join([]), do: ""
defp absname_join(list), do: absname_join(list, major_os_type())
defp absname_join([name1, name2 | rest], os_type) do
joined = do_absname_join(IO.chardata_to_string(name1), relative(name2), [], os_type)
absname_join([joined | rest], os_type)
end
defp absname_join([name], os_type) do
do_absname_join(IO.chardata_to_string(name), <<>>, [], os_type)
end
defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32)
when uc_letter in ?A..?Z,
do: do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)
defp do_absname_join(<<c1, c2, rest::binary>>, relativename, [], :win32)
when c1 in @slash and c2 in @slash,
do: do_absname_join(rest, relativename, '//', :win32)
defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32),
do: do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)
defp do_absname_join(<<?/, rest::binary>>, relativename, [?., ?/ | result], os_type),
do: do_absname_join(rest, relativename, [?/ | result], os_type)
defp do_absname_join(<<?/, rest::binary>>, relativename, [?/ | result], os_type),
do: do_absname_join(rest, relativename, [?/ | result], os_type)
defp do_absname_join(<<>>, <<>>, result, os_type),
do: IO.iodata_to_binary(reverse_maybe_remove_dir_sep(result, os_type))
defp do_absname_join(<<>>, relativename, [?: | rest], :win32),
do: do_absname_join(relativename, <<>>, [?: | rest], :win32)
defp do_absname_join(<<>>, relativename, [?/ | result], os_type),
do: do_absname_join(relativename, <<>>, [?/ | result], os_type)
defp do_absname_join(<<>>, relativename, result, os_type),
do: do_absname_join(relativename, <<>>, [?/ | result], os_type)
defp do_absname_join(<<char, rest::binary>>, relativename, result, os_type),
do: do_absname_join(rest, relativename, [char | result], os_type)
defp reverse_maybe_remove_dir_sep([?/, ?:, letter], :win32), do: [letter, ?:, ?/]
defp reverse_maybe_remove_dir_sep([?/], _), do: [?/]
defp reverse_maybe_remove_dir_sep([?/ | name], _), do: :lists.reverse(name)
defp reverse_maybe_remove_dir_sep(name, _), do: :lists.reverse(name)
@doc """
Converts the path to an absolute one and expands
any `.` and `..` characters and a leading `~`.
## Examples
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
"""
@spec expand(t) :: binary
def expand(path) do
expand_dot(absname(expand_home(path), File.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 a `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
expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!()))
end
@doc """
Returns the path type.
## Examples
### Unix-like operating systems
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
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)
when is_binary(name) do
pathtype(name, major_os_type()) |> elem(0)
end
@doc """
Forces the path to be a relative path.
## Examples
### Unix-like operating systems
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
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
relative(name, major_os_type())
end
defp relative(name, os_type) do
pathtype(name, os_type)
|> elem(1)
|> IO.chardata_to_string()
end
defp pathtype(name, os_type) do
case os_type do
:win32 -> win32_pathtype(name)
_ -> unix_pathtype(name)
end
end
defp unix_pathtype(path) when path in ["/", '/'], do: {:absolute, "."}
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}
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(<<char, relative::binary>>) when char in @slash,
do: {:volumerelative, relative}
defp win32_pathtype(<<_letter, ?:, char, relative::binary>>) when char 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([char | relative]) when char 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, ?:, char | relative]) when char 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, this function tries to strip the `from` prefix from `path`.
This function does not query the file system, so it assumes
no symlinks 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"
iex> Path.relative_to("/usr/local/foo", "/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(path, path, _original) do
"."
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, this function returns the given `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
:filename.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
:filename.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Returns the directory component of `path`.
## Examples
iex> Path.dirname("/foo/bar.ex")
"/foo"
iex> Path.dirname("/foo/bar/baz.ex")
"/foo/bar"
iex> Path.dirname("/foo/bar/")
"/foo/bar"
iex> Path.dirname("bar.ex")
"."
"""
@spec dirname(t) :: binary
def dirname(path) do
:filename.dirname(IO.chardata_to_string(path))
end
@doc """
Returns the extension of the last component of `path`.
The behaviour of this function changed in Erlang/OTP 24 for filenames
starting with a dot and without an extension. For example, for a file
named ".gitignore", `extname/1` now returns an empty string, while it
would return ".gitignore" in previous Erlang/OTP versions. This was
done to match the behaviour of `rootname/1`, which would return
".gitignore" as its name (and therefore it cannot also be an extension).
See `basename/1` and `rootname/1` for related functions to extract
information from paths.
## Examples
iex> Path.extname("foo.erl")
".erl"
iex> Path.extname("~/foo/bar")
""
"""
@spec extname(t) :: binary
def extname(path) do
:filename.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
:filename.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 may
or may 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
:filename.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Joins a list of paths.
This function should be used to convert a list of paths to a path.
Note that any trailing slash is removed when joining.
## Examples
iex> Path.join(["~", "foo"])
"~/foo"
iex> Path.join(["foo"])
"foo"
iex> Path.join(["/", "foo", "bar/"])
"/foo/bar"
"""
@spec join(nonempty_list(t)) :: binary
def join([name1, name2 | rest]), do: join([join(name1, name2) | rest])
def join([name]), do: IO.chardata_to_string(name)
@doc """
Joins two paths.
The right path will always be expanded to its relative format
and any trailing slash will be removed when joining.
## Examples
iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.join("/foo", "/bar/")
"/foo/bar"
The functions in this module support chardata, so giving a list will
treat it as a single entity:
iex> Path.join("foo", ["bar", "fiz"])
"foo/barfiz"
iex> Path.join(["foo", "bar"], "fiz")
"foobar/fiz"
"""
@spec join(t, t) :: binary
def join(left, right) do
left = IO.chardata_to_string(left)
os_type = major_os_type()
do_join(left, right, os_type) |> remove_dir_sep(os_type)
end
defp do_join(left, "/", os_type), do: remove_dir_sep(left, os_type)
defp do_join("", right, os_type), do: relative(right, os_type)
defp do_join("/", right, os_type), do: "/" <> relative(right, os_type)
defp do_join(left, right, os_type),
do: remove_dir_sep(left, os_type) <> "/" <> relative(right, os_type)
defp remove_dir_sep("", _os_type), do: ""
defp remove_dir_sep("/", _os_type), do: "/"
defp remove_dir_sep(bin, os_type) do
last = :binary.last(bin)
if last == ?/ or (last == ?\\ and os_type == :win32) do
binary_part(bin, 0, byte_size(bin) - 1)
else
bin
end
end
@doc ~S"""
Splits the path into a list at the path separator.
If an empty string is given, returns an empty list.
On Windows, path is split on both "\" and "/" separators
and the driver letter, if there is one, is always returned
in lowercase.
## Examples
iex> Path.split("")
[]
iex> Path.split("foo")
["foo"]
iex> Path.split("/foo/bar")
["/", "foo", "bar"]
"""
@spec split(t) :: [binary]
def split(path) do
:filename.split(IO.chardata_to_string(path))
end
defmodule Wildcard do
@moduledoc false
def read_link_info(file) do
:file.read_link_info(file)
end
def list_dir(dir) do
case :file.list_dir(dir) do
{:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
other -> other
end
end
end
@doc """
Traverses paths according to the given `glob` expression and returns a
list of matches.
The wildcard looks like an ordinary path, except that the following
"wildcard characters" are interpreted in a special way:
* `?` - matches one character.
* `*` - matches any number of characters up to the end of the filename, the
next dot, or the next slash.
* `**` - two adjacent `*`'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.
Do not add spaces before and after the comma as it would then match
paths containing the space character itself.
* `{item1,item2,...}` - matches one of the alternatives.
Do not add spaces before and after the comma as it would then match
paths containing the space character itself.
Other characters represent themselves. Only paths that have
exactly the same character in the same position will match. Note
that matching is case-sensitive: `"a"` will not match `"A"`.
Directory separators must always be written as `/`, even on Windows.
You may call `Path.expand/1` to normalize the path before invoking
this function.
By default, the patterns `*` and `?` do not match files starting
with a dot `.`. See the `:match_dot` option in the "Options" section
below.
## Options
* `:match_dot` - (boolean) if `false`, the special wildcard characters `*` and `?`
will not match files starting with a dot (`.`). If `true`, files starting with
a `.` will not be treated specially. Defaults to `false`.
## Examples
Imagine you have a directory called `projects` with three Elixir projects
inside of it: `elixir`, `ex_doc`, and `plug`. 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, keyword) :: [binary]
def wildcard(glob, opts \\ []) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
glob
|> chardata_to_list!()
|> :filelib.wildcard(mod)
|> Enum.map(&IO.chardata_to_string/1)
end
defp chardata_to_list!(chardata) do
case :unicode.characters_to_list(chardata) do
result when is_list(result) ->
if 0 in result do
raise ArgumentError,
"cannot execute Path.wildcard/2 for path with null byte, got: #{inspect(chardata)}"
else
result
end
{: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 -> resolve_home(rest)
rest -> rest
end
end
defp resolve_home(""), do: System.user_home!()
defp resolve_home(rest) do
case {rest, major_os_type()} do
{"\\" <> _, :win32} -> System.user_home!() <> rest
{"/" <> _, _} -> System.user_home!() <> rest
_ -> "~" <> rest
end
end
# expand_dot the given path by expanding "..", "." and "~".
defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest)
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
do: <<letter, ":/">> <> do_expand_dot(rest)
defp expand_dot(path), do: do_expand_dot(path)
defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), [])
defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc)
defp do_expand_dot([".." | t], []), do: do_expand_dot(t, [])
defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc)
defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc])
defp do_expand_dot([], []), do: ""
defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc))
defp major_os_type do
:os.type() |> elem(0)
end
end