%%% @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.