src/env.erl

%%%----------------------------------------------------------------------------
%%% @doc Environment utils
%%% @author Serge Aleynikov <saleyn@gmail.com>
%%% @copyright 2012 Serge Aleynikov
%%% @end
%%%----------------------------------------------------------------------------
%%% Created: 2012-09-21
%%%----------------------------------------------------------------------------
-module(env).
-author('saleyn@gmail.com').

%% API
-export([subst_env_path/1,   subst_env_path/2, 
         replace_env_vars/1, replace_env_vars/2,
         get_env/3, home_dir/0,
         normalize_path/1]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

-deprecated([{subst_env_path,1,"use replace_env_vars/1 instead"}]).
-deprecated([{subst_env_path,2,"use replace_env_vars/2 instead"}]).

%%%----------------------------------------------------------------------------
%%% External API
%%%----------------------------------------------------------------------------

-spec subst_env_path(list() | binary()) -> list() | binary().
subst_env_path(OsPath) ->
    replace_env_vars(OsPath, []).

-spec subst_env_path(list() | binary(), [{atom() | string(), string()}]) ->
    list() | binary().
subst_env_path(OsPath, Bindings) when is_list(Bindings) ->
    replace_env_vars(OsPath, Bindings).

%%------------------------------------------------------------------------
%% @doc Perform replacement of environment variable values in the OsPath.
%% ```
%% Example:
%%   env:replace_env_vars("~/app")       -> "/home/cuser/app"
%%   env:replace_env_vars("${HOME}/app") -> "/home/cuser/app"
%%   env:replace_env_vars("$USER/app")   -> "cuser/app"
%% '''
%% @see os:getenv/1
%% @end
%%------------------------------------------------------------------------
-spec replace_env_vars(list() | binary()) -> list() | binary().
replace_env_vars(OsPath) ->
    replace_env_vars(OsPath, []).


%%------------------------------------------------------------------------
%% @doc Perform replacement of environment variable values in the OsPath.
%%      This function also allows to provide a list of `Bindings' that
%%      override the environment (they are checked before environment
%%      variables are looked up).
%% ```
%% Example:
%%   env:replace_env_vars("~/",   [{"HOME", "/home/cu"}]) -> "/home/cu/"
%%   env:replace_env_vars("~/",   [{home,   "/home/cu"}]) -> "/home/cu/"
%%   env:replace_env_vars("$A/",  [{a, "/aaa"}]) -> "/aaa/"
%%   env:replace_env_vars("${A}/",[{a, "/aaa"}]) -> "/aaa/"
%% '''
%% @see os:getenv/1
%% @end
%%------------------------------------------------------------------------
-spec replace_env_vars(list() | binary(), [{atom() | string(), string()}]) ->
    list() | binary().
replace_env_vars(OsPath, Bindings) when is_list(Bindings) ->
    element(2, env_subst(OsPath, Bindings)).

%%-----------------------------------------------------------------------------
%% @doc Get application configuration
%% @end
%%-----------------------------------------------------------------------------
-spec get_env(atom(), atom(), any()) -> any().
get_env(App, Key, Default) ->
    case application:get_env(App, Key) of
    {ok, Val} ->
        Val;
    _ ->
        Default
    end.

%%%----------------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------------

internal_var("RELEASES")    -> {ok, get_rels_dir()};
internal_var("ROOTDIR")     -> {ok, get_root_dir()};
internal_var("RELPATH")     -> {ok, get_rel_path()};     % Releases/Version
internal_var(_)             -> undefined.

env_var(OS, "$$", _) when OS==unix; OS==win ->
    "$";
env_var(OS, [$~] = Key, Bindings) when OS==unix; OS==win ->
    case get_var(OS, "HOME", Bindings) of
    {ok, Value} -> {Key, Value};
    _           -> Key
    end;
env_var(OS, [$~ | User] = Word, Bindings) when OS==unix; OS==win ->
    case get_var(OS, "HOME", Bindings) of
    {ok, Value} -> {[$~], filename:join(filename:dirname(Value), User)};
    _           -> Word
    end;
env_var(OS, [$$, ${ | Env] = Word, Bindings) when OS==unix; OS==win ->
    Key = string:strip(Env, right, $}),
    case get_var(OS, Key, Bindings) of
    {ok, Value} -> {Key, Value};
    _           -> Word
    end;
env_var(OS, [$$ | Key] = Word, Bindings) when OS==unix; OS==win ->
    case get_var(OS, Key, Bindings) of
    {ok, Value} -> {Key, Value};
    _           -> Word
    end;
env_var(win, "%%", _) ->
    "%";
env_var(win, [$% | Env] = Word, Bindings) ->
    Key = string:strip(Env, right, $%),
    case get_var(win, Key, Bindings) of
    {ok, Value} -> {Key, Value};
    _           -> Word
    end;
env_var(_, Other, _Bindings) ->
    Other.

get_var(OS, Name, Bindings) ->
    case try_get_binding(Name, Bindings) of
    undefined when Name == "HOME" ->
        {ok, home_dir(OS)};
    undefined ->
        case os:getenv(Name) of
        false -> internal_var(Name);
        Value -> {ok, Value}
        end;
    Value ->
        {ok, Value}
    end.

try_get_binding(Name, Bindings) ->
    case proplists:get_value(Name, Bindings) of
    undefined when is_list(Name) ->
        try
            A = list_to_existing_atom(string:to_lower(Name)),
            try_get_binding(A, Bindings)
        catch _:_ ->
            undefined
        end;
    Value ->
        Value
    end.

env_subst(Bin, Bindings) when is_binary(Bin) ->
    {VarList, NewText} = env_subst(binary_to_list(Bin), Bindings),
    {VarList, list_to_binary(NewText)};
env_subst(Text, Bindings) when is_list(Text) ->
    env_subst(Text, os:type(), Bindings).

env_subst(Text, {unix, _}, Bindings) ->
    env_subst(Text, unix,
        "(?|(?:\\$\\$)|(?:~[^/$]*)|(?:\\${[A-Za-z][A-Za-z_0-9]*})|(?:\\$[A-Za-z][A-Za-z_0-9]*))",
        Bindings);
env_subst(Text, {win32, _}, Bindings) ->
    env_subst(Text, win, "(?|(?:\\%\\%)|(?:%[A-Za-z][A-Za-z_0-9]*%)|(?:~[^/$]*)|(?:\\${[A-Za-z][A-Za-z_0-9]*})|(?:\\$[A-Za-z][A-Za-z_0-9]*))", Bindings).

env_subst(Text, OsType, Pattern, Bindings) ->
    case re:run(Text, Pattern, [global,{capture, all}]) of
    {match, Matches} ->
        {Vars, TextList, Last} =
            lists:foldl(fun
                ([{Start, Length}], {Dict, List, Prev}) when Start >= 0 ->
                    Pos = Start+1,
                    Match = string:substr(Text, Pos, Length),
                    case env_var(OsType, Match, Bindings) of
                    {Key, Val} ->
                        case orddict:is_key(Key, Dict) of
                        true ->
                            NewDict = Dict;
                        false ->
                            NewDict = orddict:append(Key, Val, Dict)
                        end;
                    Val ->
                        NewDict = Dict
                    end,
                    NewList = [Val, string:substr(Text, Prev, Pos - Prev) | List],
                    NewPrev = Pos + Length,
                    {NewDict, NewList, NewPrev};
                (_, Acc) -> Acc
            end, {orddict:new(), [], 1}, Matches),
        VarList = [{K, V} || {K, [V|_]} <- orddict:to_list(Vars)],
        NewText = lists:concat(lists:reverse([string:substr(Text, Last) | TextList])),
        {VarList, NewText};
    nomatch ->
        {[], Text}
    end.

get_rel_ver() ->
    % release and version
    try [T || T <- release_handler:which_releases(), element(4, T) =:= permanent] of
    [{Name, Vsn, _Apps, permanent} | _] ->
        {Name, Vsn};
    _Other ->
        throw
    catch _:_ ->
        false
    end.

get_root_dir() ->
    case os:getenv("ROOTDIR") of
    Str when is_list(Str) -> Str;
    _ -> ""
    end.

get_rels_dir() ->
    case application:get_env(sasl, releases_dir) of
    {ok, Path} ->
        Path;
    _ ->
        filename:join(get_root_dir(), "releases")
    end.

get_rel_path() ->
    % version
    {_, Ver} = get_rel_ver(),
    % current release dir
    filename:join(get_rels_dir(), Ver).

home_dir() ->
  case os:type() of
    {win32,_} -> home_dir(win);
    {_,_}     -> home_dir(linux)
  end.

home_dir(win) ->
  normalize_path(os:getenv("USERPROFILE"));
home_dir(_) ->
  os:getenv("HOME").

normalize_path(Path) ->
  case os:type() of
    {win32,_} ->
 			F = fun Norm([$\\ | T]) -> [$/ | Norm(T)];
              Norm([H   | T]) -> [H  | Norm(T)];
              Norm([]       ) -> []
          end,
      F(Path);
    {_,_} ->
      Path
  end.

  
%%%----------------------------------------------------------------------------
%%% Test Cases
%%%----------------------------------------------------------------------------

-ifdef(EUNIT).

run_test_() -> 
    [
        ?_assertEqual("/abc/$/efg", replace_env_vars("/abc/$$/efg")),
        ?_assertEqual(true, os:putenv("X", "x")),
        ?_assertEqual("/" ++ os:getenv("X") ++ "/dir", replace_env_vars("/$X/dir")),
        ?_assertEqual(os:getenv("X") ++ "/dir", replace_env_vars("${X}/dir")),
        ?_assertEqual(os:getenv("HOME") ++ "/dir", replace_env_vars("~/dir")),
        ?_assertEqual("/aaa/dir", replace_env_vars("/$X/dir", [{"X", "aaa"}])),
        ?_assertEqual("/aaa/dir", replace_env_vars("/$X/dir", [{x, "aaa"}])),
        ?_assertEqual("/xxx/dir", replace_env_vars("$HOME/dir",  [{"HOME", "/xxx"}])),
        ?_assertEqual("/xxx/dir", replace_env_vars("~/dir", [{"HOME", "/xxx"}])),
        ?_assertEqual("/xxx/dir", replace_env_vars("~/dir", [{home, "/xxx"}]))
    ].

-endif.