Skip to content

Commit 68d4025

Browse files
committed
estdlib: add filename functions and init:get_arguments/0
Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent cbe3013 commit 68d4025

7 files changed

Lines changed: 352 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [0.7.0-beta.0] - Unreleased
88

99
### Added
10+
- Added `filename:dirname/1`, `filename:basename/1,2`, `filename:extension/1`, `filename:rootname/1,2` and `filename:join/2`
11+
- Added `init:get_arguments/0`
1012
- Added Erlang distribution over serial (uart)
1113
- Added WASM32 JIT backend for Emscripten platform
1214
- Added `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in sta or sta+ap mode.

libs/estdlib/src/filename.erl

Lines changed: 203 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@
2828
-module(filename).
2929

3030
-export([
31+
basename/1,
32+
basename/2,
33+
dirname/1,
34+
extension/1,
3135
join/1,
36+
join/2,
37+
rootname/1,
38+
rootname/2,
3239
split/1
3340
]).
3441

@@ -38,8 +45,9 @@
3845
%% @doc Join a list of path components.
3946
%%
4047
%% If a component is absolute (starts with "/"), all preceding
41-
%% components are discarded. Redundant directory separators are
42-
%% removed from the result.
48+
%% components are discarded. Redundant directory separators and
49+
%% "." components followed by a separator are removed from the
50+
%% result.
4351
%% @end
4452
%%-----------------------------------------------------------------------------
4553
-spec join(Components :: [string()]) -> string().
@@ -51,6 +59,8 @@ join([Name | Rest]) ->
5159
join([do_join(Name, hd(Rest)) | tl(Rest)]).
5260

5361
%% @private
62+
do_join(Left, []) ->
63+
normalize(Left);
5464
do_join(_Left, Right) when hd(Right) =:= $/ ->
5565
normalize(Right);
5666
do_join(Left, Right) ->
@@ -104,10 +114,195 @@ skip_slashes([$/ | Rest]) -> skip_slashes(Rest);
104114
skip_slashes(Rest) -> Rest.
105115

106116
%% @private
107-
%% Collapse every run of consecutive "/" into a single "/".
108-
normalize([]) ->
117+
normalize(Name) ->
118+
strip_join_trailing_slash(collapse_separators(Name)).
119+
120+
%% @private
121+
%% Collapse every run of consecutive "/" into a single "/", and drop any
122+
%% "." component followed by a separator ("/./" -> "/"), as OTP join does.
123+
%% A leading "./" and a trailing "/." are kept.
124+
collapse_separators([]) ->
109125
[];
110-
normalize([$/, $/ | Rest]) ->
111-
normalize([$/ | Rest]);
112-
normalize([C | Rest]) ->
113-
[C | normalize(Rest)].
126+
collapse_separators([$/, $/ | Rest]) ->
127+
collapse_separators([$/ | Rest]);
128+
collapse_separators([$/, $., $/ | Rest]) ->
129+
collapse_separators([$/ | Rest]);
130+
collapse_separators([C | Rest]) ->
131+
[C | collapse_separators(Rest)].
132+
133+
%% @private
134+
%% Remove a single trailing "/", except for the root "/" itself.
135+
strip_join_trailing_slash([$/]) -> [$/];
136+
strip_join_trailing_slash(Name) -> strip_trailing_slash(Name).
137+
138+
%% @private
139+
%% Remove a single trailing "/", if present.
140+
strip_trailing_slash([]) -> [];
141+
strip_trailing_slash([$/]) -> [];
142+
strip_trailing_slash([C | Rest]) -> [C | strip_trailing_slash(Rest)].
143+
144+
%%-----------------------------------------------------------------------------
145+
%% @param Name1 first path component
146+
%% @param Name2 second path component
147+
%% @returns the path formed by joining Name1 and Name2
148+
%% @doc Join two path components, equivalent to `join([Name1, Name2])'.
149+
%% @end
150+
%%-----------------------------------------------------------------------------
151+
-spec join(Name1 :: string(), Name2 :: string()) -> string().
152+
join(Name1, []) ->
153+
join([Name1]);
154+
join(Name1, Name2) ->
155+
join([Name1, Name2]).
156+
157+
%%-----------------------------------------------------------------------------
158+
%% @param Name a file name
159+
%% @returns the directory part of Name
160+
%% @doc Return the directory part of a file name, "." if there is none.
161+
%% @end
162+
%%-----------------------------------------------------------------------------
163+
-spec dirname(Name :: string()) -> string().
164+
dirname(Name) ->
165+
%% Drop the last component (the characters after the last slash, which
166+
%% may be empty for names with a trailing slash), then the separating
167+
%% slash run; what remains, reversed, is the directory.
168+
case drop_component(lists:reverse(Name)) of
169+
no_slash ->
170+
".";
171+
[] ->
172+
"/";
173+
DirReversed ->
174+
lists:reverse(DirReversed)
175+
end.
176+
177+
%% @private
178+
%% Drop the reversed last component and the slash run separating it; return
179+
%% the reversed directory part, or no_slash if the name has no slash at all.
180+
drop_component([]) -> no_slash;
181+
drop_component([$/ | _] = Rest) -> skip_slashes(Rest);
182+
drop_component([_ | Rest]) -> drop_component(Rest).
183+
184+
%%-----------------------------------------------------------------------------
185+
%% @param Name a file name
186+
%% @returns the last component of Name
187+
%% @doc Return the last component of a file name, ignoring a single
188+
%% trailing slash (further ones make the last component empty).
189+
%% @end
190+
%%-----------------------------------------------------------------------------
191+
-spec basename(Name :: string()) -> string().
192+
basename(Name) ->
193+
basename(Name, []).
194+
195+
%%-----------------------------------------------------------------------------
196+
%% @param Name a file name
197+
%% @param Ext an extension
198+
%% @returns the last component of Name, with Ext stripped if it matches
199+
%% @doc Like `basename/1', but also removes the extension Ext when the
200+
%% base name ends with it.
201+
%% @end
202+
%%-----------------------------------------------------------------------------
203+
-spec basename(Name :: string(), Ext :: string()) -> string().
204+
basename(Name, Ext) ->
205+
basename(Name, Ext, []).
206+
207+
%% @private
208+
%% Scan Name, accumulating the current component (reversed) in Tail and
209+
%% resetting it at each "/"; when the remainder equals Ext, the component
210+
%% accumulated so far is the base name. A single trailing "/" is ignored.
211+
%% This mirrors OTP basename/2; basename/1 is the Ext = "" case.
212+
basename(Ext, Ext, Tail) ->
213+
lists:reverse(Tail);
214+
basename([$/], Ext, Tail) ->
215+
basename([], Ext, Tail);
216+
basename([$/ | Rest], Ext, _Tail) ->
217+
basename(Rest, Ext, []);
218+
basename([C | Rest], Ext, Tail) ->
219+
basename(Rest, Ext, [C | Tail]);
220+
basename([], _Ext, Tail) ->
221+
lists:reverse(Tail).
222+
223+
%%-----------------------------------------------------------------------------
224+
%% @param Name a file name
225+
%% @returns the extension of Name, including the dot, or "" if there is none
226+
%% @doc Return the file extension of the last path component, "" if the
227+
%% last component has no dot.
228+
%% @end
229+
%%-----------------------------------------------------------------------------
230+
-spec extension(Name :: string()) -> string().
231+
extension(Name) ->
232+
case extension_split(Name) of
233+
{_Root, Ext} -> Ext
234+
end.
235+
236+
%%-----------------------------------------------------------------------------
237+
%% @param Name a file name
238+
%% @returns Name with its extension removed
239+
%% @doc Remove the extension of the last path component, if any.
240+
%% @end
241+
%%-----------------------------------------------------------------------------
242+
-spec rootname(Name :: string()) -> string().
243+
rootname(Name) ->
244+
case leading_dot_only(Name) of
245+
true ->
246+
[];
247+
false ->
248+
case extension_split(Name) of
249+
{Root, _Ext} -> Root
250+
end
251+
end.
252+
253+
%% @private
254+
%% True if Name is a leading-dot dotfile with no directory and no further
255+
%% dot (e.g. ".bashrc"), which OTP treats as having no root name.
256+
leading_dot_only([$. | Rest]) ->
257+
not lists:member($/, Rest) andalso not lists:member($., Rest);
258+
leading_dot_only(_) ->
259+
false.
260+
261+
%%-----------------------------------------------------------------------------
262+
%% @param Name a file name
263+
%% @param Ext an extension
264+
%% @returns Name with Ext removed if Name ends with Ext
265+
%% @doc Remove Ext from Name when it is its extension.
266+
%% @end
267+
%%-----------------------------------------------------------------------------
268+
-spec rootname(Name :: string(), Ext :: string()) -> string().
269+
rootname(Name, Ext) ->
270+
strip_root_suffix(Name, Ext, [], Name).
271+
272+
%% @private
273+
%% Remove Ext from the end of Name if present, except when the character
274+
%% before it is a "/" (i.e. Ext is the whole last component), matching
275+
%% OTP rootname/2.
276+
strip_root_suffix(Ext, Ext, [$/ | _], Name) -> Name;
277+
strip_root_suffix(Ext, Ext, Acc, _Name) -> lists:reverse(Acc);
278+
strip_root_suffix([], _Ext, _Acc, Name) -> Name;
279+
strip_root_suffix([C | Rest], Ext, Acc, Name) -> strip_root_suffix(Rest, Ext, [C | Acc], Name).
280+
281+
%% @private
282+
%% Split Name into {Root, Extension}: the extension starts at the last "." of
283+
%% the last path component, unless that dot is the component's first character
284+
%% (hidden files such as ".bashrc" have no extension).
285+
extension_split(Name) ->
286+
case extension_length(lists:reverse(Name), 0) of
287+
0 ->
288+
{Name, []};
289+
ExtLen ->
290+
NameLen = length(Name),
291+
{lists:sublist(Name, NameLen - ExtLen), lists:nthtail(NameLen - ExtLen, Name)}
292+
end.
293+
294+
%% @private
295+
%% Scan the reversed name for the dot starting the extension; return the
296+
%% extension length (including the dot), or 0 if there is none.
297+
extension_length([], _Consumed) ->
298+
0;
299+
extension_length([$/ | _], _Consumed) ->
300+
0;
301+
extension_length([$., Next | _], Consumed) when Next =/= $/ ->
302+
Consumed + 1;
303+
extension_length([$.], _Consumed) ->
304+
0;
305+
extension_length([$., $/ | _], _Consumed) ->
306+
0;
307+
extension_length([_ | Rest], Consumed) ->
308+
extension_length(Rest, Consumed + 1).

libs/estdlib/src/init.erl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
boot/1,
3232
get_argument/1,
3333
get_plain_arguments/0,
34+
get_arguments/0,
3435
notify_when_started/1
3536
]).
3637

@@ -96,6 +97,16 @@ get_argument(_Flag) ->
9697
get_plain_arguments() ->
9798
[].
9899

100+
%%-----------------------------------------------------------------------------
101+
%% @return all command-line user flags with their values.
102+
%% @doc Returns all command-line flags, as `{Flag, Values}' tuples.
103+
%% AtomVM has no emulator flags, so this always returns `[]'.
104+
%% @end
105+
%%-----------------------------------------------------------------------------
106+
-spec get_arguments() -> [{atom(), [string()]}].
107+
get_arguments() ->
108+
[].
109+
99110
%% @private
100111
-spec notify_when_started(Pid :: pid()) -> ok | started.
101112
notify_when_started(_Pid) ->

tests/libs/estdlib/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ set(ERLANG_MODULES
6060
test_supervisor
6161
test_lists_subtraction
6262
test_file
63+
test_init
6364
test_filename
6465
test_tcp_socket
6566
test_udp_socket

tests/libs/estdlib/test_filename.erl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,76 @@
2727
test() ->
2828
ok = test_join(),
2929
ok = test_split(),
30+
ok = test_join2(),
31+
ok = test_dirname(),
32+
ok = test_basename(),
33+
ok = test_extension(),
34+
ok = test_rootname(),
35+
ok.
36+
37+
test_join2() ->
38+
?ASSERT_MATCH(filename:join("/usr", "local"), "/usr/local"),
39+
?ASSERT_MATCH(filename:join("a", ""), "a"),
40+
?ASSERT_MATCH(filename:join("", "b"), "/b"),
41+
?ASSERT_MATCH(filename:join("a/", "/b"), "/b"),
42+
?ASSERT_MATCH(filename:join("a/", "b/"), "a/b"),
43+
ok.
44+
45+
test_dirname() ->
46+
?ASSERT_MATCH(filename:dirname("/usr/src/kalle.erl"), "/usr/src"),
47+
?ASSERT_MATCH(filename:dirname("kalle.erl"), "."),
48+
?ASSERT_MATCH(filename:dirname("/"), "/"),
49+
?ASSERT_MATCH(filename:dirname("/usr/"), "/usr"),
50+
?ASSERT_MATCH(filename:dirname("usr/src/"), "usr/src"),
51+
?ASSERT_MATCH(filename:dirname("a/b/c"), "a/b"),
52+
?ASSERT_MATCH(filename:dirname(""), "."),
53+
?ASSERT_MATCH(filename:dirname("."), "."),
54+
ok.
55+
56+
test_basename() ->
57+
?ASSERT_MATCH(filename:basename("/usr/src/kalle.erl"), "kalle.erl"),
58+
?ASSERT_MATCH(filename:basename("kalle.erl"), "kalle.erl"),
59+
?ASSERT_MATCH(filename:basename("/"), ""),
60+
?ASSERT_MATCH(filename:basename("/usr/"), "usr"),
61+
?ASSERT_MATCH(filename:basename("a/b/c"), "c"),
62+
?ASSERT_MATCH(filename:basename(""), ""),
63+
?ASSERT_MATCH(filename:basename("src/kalle.erl", ".erl"), "kalle"),
64+
?ASSERT_MATCH(filename:basename("src/kalle.beam", ".erl"), "kalle.beam"),
65+
?ASSERT_MATCH(filename:basename("kalle.erl", ".erl"), "kalle"),
66+
%% Only a single trailing separator marks the "directory" case
67+
?ASSERT_MATCH(filename:basename("a//"), ""),
68+
?ASSERT_MATCH(filename:basename("a/b//"), ""),
69+
?ASSERT_MATCH(filename:basename("trailing/slash///"), ""),
70+
?ASSERT_MATCH(filename:basename("a/b.erl/", ".erl"), "b.erl"),
71+
?ASSERT_MATCH(filename:basename("a/", "a"), "a"),
72+
%% An Ext containing "/" can match across components
73+
?ASSERT_MATCH(filename:basename("/a/b", "/b"), "a"),
74+
?ASSERT_MATCH(filename:basename("a//b", "/b"), ""),
75+
ok.
76+
77+
test_extension() ->
78+
?ASSERT_MATCH(filename:extension("foo.erl"), ".erl"),
79+
?ASSERT_MATCH(filename:extension("a/b.c/foo"), ""),
80+
?ASSERT_MATCH(filename:extension("foo"), ""),
81+
?ASSERT_MATCH(filename:extension("a.b/c.d"), ".d"),
82+
?ASSERT_MATCH(filename:extension("/x.y/z.erl"), ".erl"),
83+
ok.
84+
85+
test_rootname() ->
86+
?ASSERT_MATCH(filename:rootname("foo.erl"), "foo"),
87+
?ASSERT_MATCH(filename:rootname("a/b.c/foo"), "a/b.c/foo"),
88+
?ASSERT_MATCH(filename:rootname("/x.y/z.erl"), "/x.y/z"),
89+
?ASSERT_MATCH(filename:rootname("foo"), "foo"),
90+
?ASSERT_MATCH(filename:rootname("foo.erl", ".erl"), "foo"),
91+
?ASSERT_MATCH(filename:rootname("foo.beam", ".erl"), "foo.beam"),
92+
%% A dot extension that is the whole basename is not stripped
93+
?ASSERT_MATCH(filename:rootname("a/.erl", ".erl"), "a/.erl"),
94+
?ASSERT_MATCH(filename:rootname("a/.bashrc", ".bashrc"), "a/.bashrc"),
95+
%% A leading-dot dotfile with no directory has no root name
96+
?ASSERT_MATCH(filename:rootname("."), []),
97+
?ASSERT_MATCH(filename:rootname(".bashrc"), []),
98+
?ASSERT_MATCH(filename:rootname(".."), "."),
99+
?ASSERT_MATCH(filename:rootname(".a.b"), ".a"),
30100
ok.
31101

32102
test_join() ->
@@ -69,6 +139,28 @@ test_join() ->
69139
?ASSERT_MATCH(filename:join(["foo", "."]), "foo/."),
70140
?ASSERT_MATCH(filename:join(["foo", ".."]), "foo/.."),
71141

142+
%% A trailing separator in the final component is stripped
143+
?ASSERT_MATCH(filename:join(["a/"]), "a"),
144+
?ASSERT_MATCH(filename:join(["a", ""]), "a"),
145+
?ASSERT_MATCH(filename:join(["", ""]), ""),
146+
?ASSERT_MATCH(filename:join(["a/b///c/"]), "a/b/c"),
147+
148+
%% A "." component followed by a separator is dropped ("/./" -> "/"),
149+
%% but a leading "./" and a trailing "/." are kept
150+
?ASSERT_MATCH(filename:join("a/.", "b"), "a/b"),
151+
?ASSERT_MATCH(filename:join("a", "./b"), "a/b"),
152+
?ASSERT_MATCH(filename:join(["a", ".", "b"]), "a/b"),
153+
?ASSERT_MATCH(filename:join(["a/./b"]), "a/b"),
154+
?ASSERT_MATCH(filename:join(["a/.//./b"]), "a/b"),
155+
?ASSERT_MATCH(filename:join(["a/./."]), "a/."),
156+
?ASSERT_MATCH(filename:join(["a/../b"]), "a/../b"),
157+
?ASSERT_MATCH(filename:join(["./a"]), "./a"),
158+
?ASSERT_MATCH(filename:join(["./"]), "."),
159+
?ASSERT_MATCH(filename:join(["./."]), "./."),
160+
?ASSERT_MATCH(filename:join([".././"]), ".."),
161+
?ASSERT_MATCH(filename:join(["/./"]), "/"),
162+
?ASSERT_MATCH(filename:join(["/."]), "/."),
163+
72164
ok.
73165

74166
test_split() ->

0 commit comments

Comments
 (0)