1 module tagion.utils.envexpand;
2 
3 import std.algorithm;
4 import std.range;
5 import std.typecons;
6 
7 enum ignore_env_start = "!";
8 enum bracket_pairs = [
9         ["$$", "$", ignore_env_start], // "!" ignored as environment start
10         ["$(", ")"],
11         ["${", "}"],
12         ["$", ""],
13 
14     ];
15 
16 @safe
17 string envExpand(string text, string[string] env, void delegate(string msg) error = null) pure {
18     alias BracketState = Tuple!(string[], "bracket", ptrdiff_t, "index");
19     static long envName(string str, string end_sym) pure {
20         import std.uni;
21 
22         if (end_sym.length) {
23             return str.countUntil(end_sym);
24         }
25         if ((str.length > 0) && !str[0].isAlpha) {
26             return -1;
27         }
28         return (str ~ '!').countUntil!(c => !c.isAlphaNum);
29     }
30 
31     string innerExpand(string str) {
32         auto begin = bracket_pairs
33             .map!(bracket => BracketState(bracket, str.countUntil(bracket[0])))
34             .filter!(state => state.index >= 0);
35         //  .take(1);
36         if (!begin.empty) {
37             const state = begin.front;
38             if (state.bracket.length > 2) {
39                 const next_index = state.index + state.bracket[0].length;
40                 return str[0 .. state.index] ~ state.bracket[1] ~ innerExpand(str[next_index .. $]);
41             }
42             const env_start_index = state.index + state.bracket[0].length;
43             auto end_str = innerExpand(str[env_start_index .. $]);
44             const env_end_index = envName(end_str, state.bracket[1]);
45             string env_value;
46             if (env_end_index > 0) {
47                 const env_name = end_str[0 .. env_end_index];
48                 end_str = end_str[env_end_index + state.bracket[1].length .. $];
49                 env_value = env.get(env_name, null);
50             }
51             return str[0 .. state.index] ~
52                 env_value ~
53                 innerExpand(end_str);
54         }
55         return str;
56 
57     }
58 
59     return innerExpand(text);
60 }
61 
62 @safe
63 unittest {
64     // Simple text without env expansion
65     assert("text".envExpand(null) == "text");
66     // Expansion with undefined env
67     assert("text$(NAME)".envExpand(null) == "text");
68     // Expansion where the env is defined
69     assert("text$(NAME)".envExpand(["NAME": "hugo"]) == "texthugo");
70     // Full expansion 
71     assert("text${NAME}end".envExpand(["NAME": "hugo"]) == "texthugoend");
72     // Environment without brackets
73     assert("text$NAME".envExpand(["NAME": "hugo"]) == "texthugo");
74     // Undefined env without brackets expansion
75     assert("text$NAMEend".envExpand(["NAME": "hugo"]) == "text");
76     // Expansion of undefined environment of environment
77     assert("text$(OF${NAME})".envExpand(["NAME": "hugo"]) == "text");
78     // Expansion of defined environment of environment
79     assert("text$(OF${NAME})".envExpand(["NAME": "hugo", "OFhugo": "_extra_"]) == "text_extra_");
80     // Expansion of defined environment of environment
81     assert("text$(OF$(NAME)_end)".envExpand([
82         "NAME": "hugo",
83         "OFhugo": "_extra_",
84         "OFhugo_end": "_other_extra_"
85     ]) == "text_other_extra_");
86     // Double dollar ignored as an environment
87     assert("text$$(NAME)".envExpand(["NAME": "not-replaced"]) == "text$(NAME)");
88 
89 }