Skip to content

Commit c65b5dd

Browse files
committed
Merge environment variables
1 parent a8b5ca0 commit c65b5dd

17 files changed

+186
-112
lines changed

dune

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
(dirs (:standard \ node_modules \ _esy))
1+
(dirs
2+
(:standard \ node_modules \ _esy))

dune-project

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
(lang dune 1.6)
2-
(name reenv)
2+
(name reenv)
3+
(using fmt 1.1)

executable/ReenvApp.re

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ let () = {
6161
);
6262

6363
(term, info) |> Term.eval |> Term.exit;
64-
};
64+
};

executable/dune

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1-
(executable (name ReenvApp) (public_name reenv)
2-
(libraries reenv.lib cmdliner) (package reenv)) (env (_ (flags (:standard -w -39))))
1+
(executable
2+
(name ReenvApp)
3+
(public_name reenv)
4+
(libraries reenv.lib cmdliner)
5+
(package reenv))
6+
7+
(env
8+
(_
9+
(flags
10+
(:standard -w -39))))

lib/Dotenv.re

Whitespace-only changes.

lib/Env.re

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
let windows = Sys.os_type == "Win32";
2+
3+
let caseInsensitiveEqual = (i, j) =>
4+
String.lowercase_ascii(i) == String.lowercase_ascii(j);
5+
let caseInsensitiveHash = k => Hashtbl.hash(String.lowercase_ascii(k));
6+
7+
module EnvHash = {
8+
type t = string;
9+
let equal = if (windows) {caseInsensitiveEqual} else {(==)};
10+
let hash = if (windows) {caseInsensitiveHash} else {Hashtbl.hash};
11+
};
12+
13+
module EnvHashtbl = Hashtbl.Make(EnvHash);
14+
15+
type t = EnvHashtbl.t(string);
16+
17+
let list_of_in_channel = ic => {
18+
Util.readUntilEndOfFile(ic)
19+
|> List.filter(s => String.contains(s, '='))
20+
|> List.map(String.split_on_char('='))
21+
|> List.map(Util.escapeEquals);
22+
};
23+
24+
let make = envFiles => {
25+
let fileEnv =
26+
envFiles
27+
|> List.map(open_in_bin)
28+
|> List.map(list_of_in_channel)
29+
|> List.concat
30+
|> Array.of_list
31+
|> Array.map(((key, value)) => key ++ "=" ++ value);
32+
33+
let curEnv = Unix.environment() |> Array.append(fileEnv);
34+
35+
let table = EnvHashtbl.create(Array.length(curEnv));
36+
let f = item =>
37+
try (
38+
{
39+
let idx = String.index(item, '=');
40+
let name = String.sub(item, 0, idx);
41+
let value = String.sub(item, idx + 1, String.length(item) - idx - 1);
42+
43+
let nextValue =
44+
EnvHashtbl.find_opt(table, name)
45+
|> (
46+
fun
47+
| Some(prevValue) => String.concat(":", [prevValue, value])
48+
| None => value
49+
);
50+
51+
EnvHashtbl.replace(table, name, nextValue);
52+
}
53+
) {
54+
| Not_found => ()
55+
};
56+
57+
Array.iter(f, curEnv);
58+
table;
59+
};
60+
61+
let to_array = env => {
62+
let f = (name, value, items) => [name ++ "=" ++ value, ...items];
63+
let e = Array.of_list(EnvHashtbl.fold(f, env, []));
64+
65+
e;
66+
};
67+
68+
let get_env = (key, env) => {
69+
EnvHashtbl.find(env, key);
70+
};

lib/Env.rei

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type t;
2+
3+
let make: list(string) => t;
4+
5+
let to_array: t => array(string);
6+
7+
let get_env: (string, t) => string;

lib/EnvKeys.re

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module SS = Set.Make(String);
2+
3+
type t = SS.t;
4+
5+
let list_of_in_channel = ic => {
6+
Util.readUntilEndOfFile(ic)
7+
|> List.map(row =>
8+
switch (String.split_on_char('=', row)) {
9+
| [key, ..._] => key
10+
| [] => ""
11+
}
12+
)
13+
|> List.filter(a => a != "");
14+
};
15+
16+
let make = file => {
17+
let t = file |> open_in_bin |> list_of_in_channel |> SS.of_list;
18+
if (SS.is_empty(t)) {
19+
SS.empty;
20+
} else {
21+
t;
22+
};
23+
};
24+
25+
let concat = t_list => {
26+
t_list |> List.map(SS.elements) |> List.concat |> SS.of_list;
27+
};
28+
29+
let has_key = (t, key) => {
30+
SS.exists(elt => elt == key, t);
31+
};
32+
33+
let equal = SS.equal;
34+
35+
let missing_keys = (t1, t2) => {
36+
// TODO: is this right?
37+
SS.union(t1, t2) |> SS.elements;
38+
};

lib/EnvKeys.rei

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type t;
2+
3+
let make: string => t;
4+
5+
let concat: list(t) => t;
6+
7+
let has_key: (t, string) => bool;
8+
9+
let equal: (t, t) => bool;
10+
11+
let missing_keys: (t, t) => list(string);

lib/Reenv.re

+12-57
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,29 @@
11
exception Missing_keys(string);
22

3-
type t = Hashtbl.t(string, string);
3+
let checkSafe = (~safeFile, envFiles) => {
4+
let envKeys = envFiles |> List.map(EnvKeys.make) |> EnvKeys.concat;
45

5-
let t_of_in_channel = (t, ic) => {
6-
Util.readUntilEndOfFile(ic)
7-
|> List.filter(s => String.contains(s, '='))
8-
|> List.map(String.split_on_char('='))
9-
|> List.map(Util.escapeEquals)
10-
|> List.iter(((key, value)) => Hashtbl.replace(t, key, value));
6+
let safeKeys = EnvKeys.make(safeFile);
117

12-
t;
13-
};
14-
15-
let array_of_t: t => array((string, string)) =
16-
t => {
17-
Hashtbl.fold((key, value, l) => [(key, value), ...l], t, [])
18-
|> Array.of_list;
8+
if (EnvKeys.equal(envKeys, safeKeys)) {
9+
();
10+
} else {
11+
EnvKeys.missing_keys(envKeys, safeKeys)
12+
|> String.concat(", ")
13+
|> (keyString => raise(Missing_keys(keyString)));
1914
};
20-
21-
let make = (): t => Hashtbl.create(64);
22-
23-
let get_opt = (key, t) => Hashtbl.find_opt(t, key);
24-
let get_exn = (key, t) => Hashtbl.find(t, key);
25-
26-
let checkSafe = (~safeFile, t) => {
27-
let safeEnv = safeFile |> open_in_bin |> t_of_in_channel(make());
28-
29-
array_of_t(safeEnv)
30-
|> Array.fold_left(
31-
(lst, (key, _value)) =>
32-
switch (get_opt(key, t)) {
33-
| Some(_v) => lst
34-
| None => [key, ...lst]
35-
},
36-
[],
37-
)
38-
|> (
39-
keys =>
40-
switch (keys) {
41-
| [] => ()
42-
| lst => Missing_keys(String.concat(", ", lst)) |> raise
43-
}
44-
);
4515
};
4616

4717
let main = (~envFiles, ~safeFile, ~command, argv) => {
4818
let programArgs = Array.of_list([command, ...argv]);
4919

50-
let t: t = make();
51-
52-
Unix.environment()
53-
|> Array.map(s =>
54-
switch (String.split_on_char('=', s)) {
55-
| [key, ...value] => (key, String.concat("", value))
56-
| _ => exit(1)
57-
}
58-
)
59-
|> Array.iter(((key, value)) => Hashtbl.replace(t, key, value));
60-
61-
envFiles
62-
|> List.map(open_in_bin)
63-
|> List.iter(ic => t_of_in_channel(t, ic) |> ignore);
64-
6520
let () =
6621
switch (safeFile) {
67-
| Some(safeFile) => checkSafe(~safeFile, t)
22+
| Some(safeFile) => checkSafe(~safeFile, envFiles)
6823
| None => ()
6924
};
7025

71-
let environment =
72-
array_of_t(t) |> Array.map(((key, value)) => key ++ "=" ++ value);
26+
let environment = Env.make(envFiles) |> Env.to_array;
7327

7428
try (Unix.execvpe(command, programArgs, environment)) {
7529
| Unix.Unix_error(error, _method, _program) =>
@@ -79,3 +33,4 @@ let main = (~envFiles, ~safeFile, ~command, argv) => {
7933
};
8034

8135
module Util = Util;
36+
module Env = Env;

lib/Reenv.rei

+2-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
module Util = Util;
2+
module Env = Env;
23

34
exception Missing_keys(string);
45

5-
type t;
6-
7-
let make: unit => t;
8-
9-
let get_opt: (string, t) => option(string);
10-
let get_exn: (string, t) => string;
11-
12-
let array_of_t: t => array((string, string));
13-
14-
let checkSafe: (~safeFile: string, t) => unit;
6+
let checkSafe: (~safeFile: string, list(string)) => unit;
157

168
let main:
179
(
@@ -21,5 +13,3 @@ let main:
2113
list(string)
2214
) =>
2315
'a;
24-
25-
let t_of_in_channel: (t, in_channel) => t;

lib/dune

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
(library (name Reenv) (public_name reenv.lib) (libraries unix re))
1+
(library
2+
(name Reenv)
3+
(public_name reenv.lib)
4+
(libraries unix re))

test/DotenvCompliance.re

+15-27
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe("dotenv compliance", utils => {
88
expect.int(List.length(rows)).toBe(2);
99
});
1010

11-
utils.test("BASIC=basic is same as BASIC=\"basic\"", ({expect}) => {
11+
utils.test("Skips comments", ({expect}) => {
1212
let file = open_in_bin("./test/fixtures/.env.with_comment");
1313
let rows = Reenv.Util.readUntilEndOfFile(file);
1414

@@ -17,9 +17,8 @@ describe("dotenv compliance", utils => {
1717

1818
utils.test("BASIC=basic is same as BASIC=\"basic\"", ({expect}) => {
1919
let env =
20-
open_in_bin("./test/fixtures/.env.quotes")
21-
|> Reenv.t_of_in_channel(Reenv.make())
22-
|> Reenv.get_exn("BASIC");
20+
Reenv.Env.make(["./test/fixtures/.env.quotes"])
21+
|> Reenv.Env.get_env("BASIC");
2322

2423
expect.string(env).toEqual("basic");
2524
});
@@ -28,9 +27,8 @@ describe("dotenv compliance", utils => {
2827
"empty values become empty strings (EMPTY= becomes EMPTY=\"\")",
2928
({expect}) => {
3029
let env =
31-
open_in_bin("./test/fixtures/.env.empty")
32-
|> Reenv.t_of_in_channel(Reenv.make())
33-
|> Reenv.get_exn("EMPTY");
30+
Reenv.Env.make(["./test/fixtures/.env.empty"])
31+
|> Reenv.Env.get_env("EMPTY");
3432

3533
expect.string(env).toEqual("");
3634
});
@@ -39,19 +37,17 @@ describe("dotenv compliance", utils => {
3937
"single and double quoted values are escaped (SINGLE_QUOTE='quoted' == SINGLE_QUOTE=\"quoted\")",
4038
({expect}) => {
4139
let env =
42-
open_in_bin("./test/fixtures/.env.single_quotes")
43-
|> Reenv.t_of_in_channel(Reenv.make())
44-
|> Reenv.get_exn("SINGLE_QUOTE");
40+
Reenv.Env.make(["./test/fixtures/.env.single_quotes"])
41+
|> Reenv.Env.get_env("SINGLE_QUOTE");
4542

4643
expect.string(env).toEqual("quoted");
4744
},
4845
);
4946

5047
utils.test("whitespace is removed from both ends of the value", ({expect}) => {
5148
let env =
52-
open_in_bin("./test/fixtures/.env.trim")
53-
|> Reenv.t_of_in_channel(Reenv.make())
54-
|> Reenv.get_exn("TRIM");
49+
Reenv.Env.make(["./test/fixtures/.env.trim"])
50+
|> Reenv.Env.get_env("TRIM");
5551

5652
expect.string(env).toEqual("trim");
5753
});
@@ -60,27 +56,19 @@ describe("dotenv compliance", utils => {
6056
"inner quotes are maintained (think JSON) (JSON={\"foo\": \"bar\"} becomes {JSON:\"{\\\"foo\\\": \\\"bar\\\"}\")",
6157
({expect}) => {
6258
let env =
63-
open_in_bin("./test/fixtures/.env.json")
64-
|> Reenv.t_of_in_channel(Reenv.make())
65-
|> Reenv.array_of_t;
66-
67-
let (key, value) = env[0];
59+
Reenv.Env.make(["./test/fixtures/.env.json"])
60+
|> Reenv.Env.get_env("JSON");
6861

69-
expect.string(key).toEqual("JSON");
70-
expect.string(value).toEqual("{\"foo\": \"bar\"}");
62+
expect.string(env).toEqual("{\"foo\": \"bar\"}");
7163
},
7264
);
7365

7466
utils.test("new lines are expanded", ({expect}) => {
7567
let env =
76-
open_in_bin("./test/fixtures/.env.new_line")
77-
|> Reenv.t_of_in_channel(Reenv.make())
78-
|> Reenv.array_of_t;
79-
80-
let (key, value) = env[0];
68+
Reenv.Env.make(["./test/fixtures/.env.new_line"])
69+
|> Reenv.Env.get_env("NEW_LINE");
8170

82-
expect.string(key).toEqual("NEW_LINE");
83-
expect.string(value).toEqual({|new
71+
expect.string(env).toEqual({|new
8472
line|});
8573
});
8674
});

test/SafeTest.re

+5-8
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@ describe("Safe functionality", utils => {
44
let checkIfSafe = Reenv.checkSafe(~safeFile="./test/fixtures/.env");
55

66
utils.test("Should raise if not all keys are there", ({expect}) =>
7-
expect.fn(() => checkIfSafe(Reenv.make())).toThrowException(
8-
Reenv.Missing_keys("TEST2, TEST, TEST3"),
7+
expect.fn(() => checkIfSafe([])).toThrowException(
8+
Reenv.Missing_keys("TEST, TEST2, TEST3"),
99
)
1010
);
1111

12-
utils.test("is fine when all the keys are provided", ({expect}) => {
13-
let env =
14-
open_in_bin("./test/fixtures/.env") |> Reenv.t_of_in_channel(Reenv.make());
15-
16-
expect.same(checkIfSafe(env), ());
17-
});
12+
utils.test("is fine when all the keys are provided", ({expect}) =>
13+
expect.same(checkIfSafe(["./test/fixtures/.env"]), ())
14+
);
1815
});

0 commit comments

Comments
 (0)