A structured-data shell in Gleam, inspired by Nushell
1%% Erlang FFI for gleshell: REPL I/O, cwd, env, external processes.
2-module(gleshell_ffi).
3-export([
4 get_line/1,
5 set_cwd/1,
6 get_cwd/0,
7 getenv/1,
8 setenv/2,
9 run_cmd/2,
10 which/1,
11 home_dir/0
12]).
13
14-spec get_line(binary()) -> {ok, binary()} | {error, binary()}.
15get_line(Prompt) when is_binary(Prompt) ->
16 %% OTP may return a charlist or a UTF-8 binary depending on the
17 %% standard_io encoding / binary options — accept both.
18 case io:get_line(unicode:characters_to_list(Prompt)) of
19 eof ->
20 {error, <<"eof">>};
21 {error, _} ->
22 {error, <<"io_error">>};
23 Line when is_list(Line); is_binary(Line) ->
24 Bin = unicode:characters_to_binary(Line),
25 Stripped = string:trim(Bin, trailing, [$\n, $\r]),
26 {ok, Stripped}
27 end.
28
29-spec set_cwd(binary()) -> {ok, nil} | {error, binary()}.
30set_cwd(Path) when is_binary(Path) ->
31 case file:set_cwd(unicode:characters_to_list(Path)) of
32 ok ->
33 {ok, nil};
34 {error, Reason} ->
35 {error, reason_to_bin(Reason)}
36 end.
37
38-spec get_cwd() -> {ok, binary()} | {error, binary()}.
39get_cwd() ->
40 case file:get_cwd() of
41 {ok, Dir} ->
42 {ok, unicode:characters_to_binary(Dir)};
43 {error, Reason} ->
44 {error, reason_to_bin(Reason)}
45 end.
46
47-spec getenv(binary()) -> {ok, binary()} | {error, nil}.
48getenv(Name) when is_binary(Name) ->
49 case os:getenv(unicode:characters_to_list(Name)) of
50 false ->
51 {error, nil};
52 Value ->
53 {ok, unicode:characters_to_binary(Value)}
54 end.
55
56-spec setenv(binary(), binary()) -> {ok, nil}.
57setenv(Name, Value) when is_binary(Name), is_binary(Value) ->
58 os:putenv(unicode:characters_to_list(Name), unicode:characters_to_list(Value)),
59 {ok, nil}.
60
61-spec which(binary()) -> {ok, binary()} | {error, nil}.
62which(Command) when is_binary(Command) ->
63 case os:find_executable(unicode:characters_to_list(Command)) of
64 false ->
65 {error, nil};
66 Path ->
67 {ok, unicode:characters_to_binary(Path)}
68 end.
69
70-spec home_dir() -> {ok, binary()} | {error, binary()}.
71home_dir() ->
72 case os:getenv("HOME") of
73 false ->
74 {error, <<"HOME not set">>};
75 Home ->
76 {ok, unicode:characters_to_binary(Home)}
77 end.
78
79%% Run an executable with args; capture stdout+stderr and exit status.
80%% Returns {ok, {Status :: integer(), Output :: binary()}} | {error, binary()}.
81-spec run_cmd(binary(), [binary()]) -> {ok, {integer(), binary()}} | {error, binary()}.
82run_cmd(Command, Args) when is_binary(Command), is_list(Args) ->
83 case os:find_executable(unicode:characters_to_list(Command)) of
84 false ->
85 {error, <<"command not found: ", Command/binary>>};
86 Path ->
87 PortArgs = [unicode:characters_to_list(A) || A <- Args],
88 try
89 Port = open_port(
90 {spawn_executable, Path},
91 [
92 binary,
93 exit_status,
94 stderr_to_stdout,
95 use_stdio,
96 stream,
97 {args, PortArgs}
98 ]
99 ),
100 collect_output(Port, <<>>)
101 catch
102 _:Reason ->
103 {error, reason_to_bin(Reason)}
104 end
105 end.
106
107collect_output(Port, Acc) ->
108 receive
109 {Port, {data, Data}} when is_binary(Data) ->
110 collect_output(Port, <<Acc/binary, Data/binary>>);
111 {Port, {data, Data}} when is_list(Data) ->
112 Bin = unicode:characters_to_binary(Data),
113 collect_output(Port, <<Acc/binary, Bin/binary>>);
114 {Port, {exit_status, Status}} ->
115 {ok, {Status, Acc}}
116 after 120_000 ->
117 catch port_close(Port),
118 {error, <<"command timed out after 120s">>}
119 end.
120
121reason_to_bin(Reason) when is_atom(Reason) ->
122 atom_to_binary(Reason, utf8);
123reason_to_bin(Reason) when is_binary(Reason) ->
124 Reason;
125reason_to_bin(Reason) ->
126 iolist_to_binary(io_lib:format("~p", [Reason])).