Packages

Elixir-inspired standard library modules for Erlang

Current section

Files

Jump to
ex_stdlib src path.erl
Raw

src/path.erl

%%% @doc
%%% Path manipulation.
%%%
%%% This module provides functions for manipulating file system paths
%%% in a cross-platform manner. It handles both Unix-style and Windows-style
%%% paths appropriately.
%%%
%%% Examples:
%%% ```
%%% AbsPath = path:absname("../relative/path"),
%%% JoinedPath = path:join(["/home", "user", "documents"]),
%%% BaseName = path:basename("/path/to/file.txt"),
%%% DirName = path:dirname("/path/to/file.txt").
%%% '''
%%% @end
-module(path).
%% API
-export([absname/1, absname/2]).
-export([basename/1, basename/2, dirname/1]).
-export([extname/1, rootname/1, rootname/2]).
-export([join/1, join/2, split/1]).
-export([relative_to/2, relative_to_cwd/1]).
-export([expand/1, expand/2]).
-export([type/1, wildcard/1, wildcard/2]).
%% Types
-type path() :: string() | binary().
-type path_type() :: absolute | relative | volumerelative.
-export_type([path/0, path_type/0]).
%%% @doc
%%% Returns the absolute name of the given path.
%%%
%%% Converts a relative path to an absolute path using the current working directory.
%%% @end
-spec absname(path()) -> string().
absname(Path) ->
filename:absname(to_list(Path)).
%%% @doc
%%% Returns the absolute name of the given path relative to the specified directory.
%%%
%%% Converts a relative path to an absolute path using the given directory as base.
%%% @end
-spec absname(path(), path()) -> string().
absname(Path, Dir) ->
filename:absname(to_list(Path), to_list(Dir)).
%%% @doc
%%% Returns the basename of the given path.
%%%
%%% The basename is the last component of the path after the last directory separator.
%%% @end
-spec basename(path()) -> string().
basename(Path) ->
filename:basename(to_list(Path)).
%%% @doc
%%% Returns the basename of the given path with the specified extension removed.
%%%
%%% If the basename ends with the given extension, it is removed.
%%% @end
-spec basename(path(), string()) -> string().
basename(Path, Ext) ->
filename:basename(to_list(Path), Ext).
%%% @doc
%%% Returns the directory part of the given path.
%%%
%%% Returns all components of the path except the last one.
%%% @end
-spec dirname(path()) -> string().
dirname(Path) ->
filename:dirname(to_list(Path)).
%%% @doc
%%% Returns the extension of the given path.
%%%
%%% The extension includes the dot (e.g., ".txt").
%%% Returns an empty string if there is no extension.
%%% @end
-spec extname(path()) -> string().
extname(Path) ->
case filename:extension(to_list(Path)) of
[] -> "";
Ext -> Ext
end.
%%% @doc
%%% Returns the path with the extension removed.
%%%
%%% If there is no extension, returns the path unchanged.
%%% @end
-spec rootname(path()) -> string().
rootname(Path) ->
filename:rootname(to_list(Path)).
%%% @doc
%%% Returns the path with the specified extension removed.
%%%
%%% Only removes the extension if it matches the given extension.
%%% @end
-spec rootname(path(), string()) -> string().
rootname(Path, Ext) ->
filename:rootname(to_list(Path), Ext).
%%% @doc
%%% Joins a list of path components into a single path.
%%%
%%% Uses the appropriate path separator for the current platform.
%%% @end
-spec join([path()]) -> string().
join([]) ->
"";
join([Path]) ->
to_list(Path);
join([First | Rest]) ->
lists:foldl(fun(Component, Acc) ->
filename:join(Acc, to_list(Component))
end, to_list(First), Rest).
%%% @doc
%%% Joins two path components into a single path.
%%%
%%% Uses the appropriate path separator for the current platform.
%%% @end
-spec join(path(), path()) -> string().
join(Path1, Path2) ->
filename:join(to_list(Path1), to_list(Path2)).
%%% @doc
%%% Splits a path into its components.
%%%
%%% Returns a list of path components.
%%% @end
-spec split(path()) -> [string()].
split(Path) ->
filename:split(to_list(Path)).
%%% @doc
%%% Returns the given path relative to the specified directory.
%%%
%%% If the path is not under the given directory, returns the path unchanged.
%%% @end
-spec relative_to(path(), path()) -> string().
relative_to(Path, Dir) ->
AbsPath = absname(Path),
AbsDir = absname(Dir),
case lists:prefix(filename:split(AbsDir), filename:split(AbsPath)) of
true ->
make_relative(filename:split(AbsPath), filename:split(AbsDir));
false ->
AbsPath
end.
%%% @doc
%%% Returns the given path relative to the current working directory.
%%%
%%% Converts an absolute path to a relative path from the current directory.
%%% @end
-spec relative_to_cwd(path()) -> string().
relative_to_cwd(Path) ->
{ok, Cwd} = file:get_cwd(),
relative_to(Path, Cwd).
%%% @doc
%%% Expands a path by resolving any environment variables and tildes.
%%%
%%% Environment variables are specified as $VAR or ${VAR}.
%%% Tilde (~) is expanded to the user's home directory.
%%% @end
-spec expand(path()) -> string().
expand(Path) ->
expand_path(to_list(Path)).
%%% @doc
%%% Expands a path relative to the specified directory.
%%%
%%% First expands the path, then makes it relative to the given directory.
%%% @end
-spec expand(path(), path()) -> string().
expand(Path, Dir) ->
ExpandedPath = expand(Path),
case type(ExpandedPath) of
absolute -> ExpandedPath;
_ -> absname(ExpandedPath, to_list(Dir))
end.
%%% @doc
%%% Returns the type of the given path.
%%%
%%% Returns absolute for absolute paths, relative for relative paths,
%%% and volumerelative for Windows volume-relative paths.
%%% @end
-spec type(path()) -> path_type().
type(Path) ->
case filename:pathtype(to_list(Path)) of
absolute -> absolute;
relative -> relative;
volumerelative -> volumerelative
end.
%%% @doc
%%% Returns a list of files matching the given wildcard pattern.
%%%
%%% Uses the current working directory as the base.
%%% @end
-spec wildcard(path()) -> [string()].
wildcard(Pattern) ->
filelib:wildcard(to_list(Pattern)).
%%% @doc
%%% Returns a list of files matching the given wildcard pattern in the specified directory.
%%%
%%% Uses the given directory as the base for pattern matching.
%%% @end
-spec wildcard(path(), path()) -> [string()].
wildcard(Pattern, Dir) ->
filelib:wildcard(to_list(Pattern), to_list(Dir)).
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%% @private
to_list(Path) when is_binary(Path) ->
binary_to_list(Path);
to_list(Path) when is_list(Path) ->
Path.
%% @private
make_relative(AbsPathComponents, DirComponents) ->
RelativeComponents = lists:nthtail(length(DirComponents), AbsPathComponents),
case RelativeComponents of
[] -> ".";
_ -> filename:join(RelativeComponents)
end.
%% @private
expand_path(Path) ->
% First expand environment variables
PathWithEnv = expand_env_vars(Path),
% Then expand tilde
expand_tilde(PathWithEnv).
%% @private
expand_env_vars(Path) ->
% Simple environment variable expansion
% This is a basic implementation - could be enhanced
case re:run(Path, "\\$\\{([^}]+)\\}|\\$([A-Za-z_][A-Za-z0-9_]*)",
[global, {capture, all, list}]) of
{match, Matches} ->
lists:foldl(fun expand_env_var/2, Path, Matches);
nomatch ->
Path
end.
%% @private
expand_env_var([FullMatch, "", VarName], Path) ->
% $VAR format
case os:getenv(VarName) of
false -> Path;
Value -> string:replace(Path, FullMatch, Value)
end;
expand_env_var([FullMatch, VarName, ""], Path) ->
% ${VAR} format
case os:getenv(VarName) of
false -> Path;
Value -> string:replace(Path, FullMatch, Value)
end;
expand_env_var(_, Path) ->
Path.
%% @private
expand_tilde("~" ++ Rest) ->
case os:getenv("HOME") of
false ->
% Try Windows-style home
case os:getenv("USERPROFILE") of
false -> "~" ++ Rest;
Home -> filename:join(Home, Rest)
end;
Home ->
case Rest of
"" -> Home;
"/" ++ RelPath -> filename:join(Home, RelPath);
"\\" ++ RelPath -> filename:join(Home, RelPath);
_ -> "~" ++ Rest
end
end;
expand_tilde(Path) ->
Path.