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
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 );
5464do_join (_Left , Right ) when hd (Right ) =:= $/ ->
5565 normalize (Right );
5666do_join (Left , Right ) ->
@@ -104,10 +114,195 @@ skip_slashes([$/ | Rest]) -> skip_slashes(Rest);
104114skip_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 ).
0 commit comments