1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1996-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%%----------------------------------------------------------------------
21%%% File    : tags.erl
22%%% Author  : Anders Lindgren
23%%% Purpose : Generate an Emacs TAGS file from programs written in Erlang.
24%%% Date    : 1998-03-16
25%%% Version : 1.1
26%%%----------------------------------------------------------------------
27
28-module(tags).
29
30-export([file/1, file/2, files/1, files/2, dir/1, dir/2,
31	 dirs/1, dirs/2, subdir/1, subdir/2, subdirs/1, subdirs/2,
32	 root/0, root/1]).
33
34
35%% `Tags' is a part of the editor Emacs. It is used for warp-speed
36%% jumps between different source files in a project.  When Using
37%% `Tags', a function in any source file can be found by few a simple
38%% keystrokes, just press M-. (in normal terms: Press Escape and dot).
39%%
40%% In order to work, the `Tags' system needs a list of all functions
41%% in all source files in the project.  This list is denoted the "TAGS
42%% file".  This purpose of this module is to create the TAGS file for
43%% programs written in Erlang.
44%%
45%% In addition to functions, both records and macros (`define's) are
46%% added to the TAGS file.
47
48
49%% Usage:
50%%  root([Options])           -- Create a TAGS file covering all files in
51%%			         the Erlang distribution.
52%%
53%%  file(File [, Options])    -- Create a TAGS file for the file `File'.
54%%  files(FileList [, Options])
55%%			      -- Dito for all files in `FileList'.
56%%
57%%  dir(Dir [, Options])      -- Create a TAGS file for all files in `Dir'.
58%%  dirs(DirList [, Options]) -- Dito for all files in all
59%%			         directories in `DirList'.
60%%
61%%  subdir(Dir [, Options])   -- Descend recursively down `Dir' and create
62%%				 a TAGS file convering all files found.
63%%  subdirs(DirList [, Options])
64%%			      -- Dito, for all directories in `DirList'.
65%%
66%% The default is to create a file named "TAGS" in the current directory.
67%%
68%% Options is a list of tuples, where the following tuples are
69%% recognised:
70%%    {outfile, NameOfTAGSFile}
71%%    {outdir, NameOfDirectory}
72%%
73%% Note, should both `outfile' and `outdir' options be given, `outfile'
74%% take precedence.
75
76
77%%% External interface
78
79root() -> root([]).
80root(Options) -> subdir(code:root_dir(), Options).
81
82dir(Dir) -> dir(Dir, []).
83dir(Dir, Options) -> dirs([Dir], Options).
84
85dirs(Dirs) -> dirs(Dirs, []).
86dirs(Dirs, Options) ->
87    files(collect_dirs(Dirs, false), Options).
88
89subdir(Dir) -> subdir(Dir, []).
90subdir(Dir, Options) -> subdirs([Dir], Options).
91
92subdirs(Dirs) -> subdirs(Dirs, []).
93subdirs(Dirs, Options) ->
94    files(collect_dirs(Dirs, true), Options).
95
96file(Name) -> file(Name, []).
97file(Name, Options) -> files([Name], Options).
98
99files(Files) -> files(Files, []).
100files(Files, Options) ->
101    case open_out(Options) of
102	{ok, Os} ->
103	    files_loop(Files, Os),
104	    ok = close_out(Os),
105	    ok;
106	_ ->
107	    error
108    end.
109
110
111
112%%% Internal functions.
113
114%% Find all files in a directory list.  Should the second argument be
115%% the atom `true' the functions will descend into subdirectories.
116collect_dirs(Dirs, Recursive) ->
117    collect_dirs(Dirs, Recursive, []).
118
119collect_dirs([], _Recursive, Acc) -> Acc;
120collect_dirs([Dir | Dirs], Recursive, Acc) ->
121    NewAcc = case file:list_dir(Dir) of
122		 {ok, Entries} ->
123		     collect_files(Dir, Entries, Recursive, Acc);
124		 _ ->
125		     Acc
126	     end,
127    collect_dirs(Dirs, Recursive, NewAcc).
128
129collect_files(_Dir,[],_Recursive, Acc) -> Acc;
130collect_files(Dir, [File | Files], Recursive, Acc) ->
131    FullFile = filename:join(Dir, File),
132    NewAcc = case filelib:is_dir(FullFile) of
133		 true when Recursive ->
134		     collect_dirs([FullFile], Recursive, Acc);
135		 true ->
136		     Acc;
137		 false ->
138		     case filelib:is_regular(FullFile) of
139			 true ->
140			     case filename:extension(File) of
141				 ".erl" ->
142				     [FullFile | Acc];
143				 ".hrl" ->
144				     [FullFile | Acc];
145				 _ ->
146				     Acc
147			     end;
148			 false ->
149			     Acc
150		     end
151	     end,
152    collect_files(Dir, Files, Recursive, NewAcc).
153
154
155files_loop([],_Os) -> true;
156files_loop([F | Fs], Os) ->
157    case filename(F, Os) of
158	ok ->
159	    ok;
160	error ->
161	    %% io:format("Could not open ~ts~n", [F]),
162	    error
163    end,
164    files_loop(Fs, Os).
165
166
167%% Generate tags for one file.
168filename(Name, Os) ->
169    case file:open(Name, [read]) of
170	{ok, Desc} ->
171	    Acc = module(Desc, [], [], {1, 0}),
172	    ok = file:close(Desc),
173	    genout(Os, Name, Acc),
174	    ok;
175	_ ->
176	    error
177    end.
178
179
180module(In, Last, Acc, {LineNo, CharNo}) ->
181    case io:get_line(In, []) of
182	eof ->
183	    Acc;
184	Line ->
185	    {NewLast, NewAcc} = line(Line, Last, Acc, {LineNo, CharNo}),
186	    module(In, NewLast, NewAcc, {LineNo+1, CharNo+length(Line)})
187    end.
188
189
190%% Handle one line.  Return the last added function name.
191line([], Last, Acc,  _) -> {Last, Acc};
192line(Line, _, Acc, Nos) when hd(Line) =:= $- ->
193    case attribute(Line, Nos) of
194	false -> {[], Acc};
195	New -> {[], [New | Acc]}
196    end;
197line(Line, Last, Acc, Nos) ->
198    %% to be OR not to be?
199    case case {hd(Line), word_char(hd(Line))} of
200	     {$', _} -> true;
201	     {_, true} -> true;
202	     _ -> false
203	 end of
204	true ->
205	    case func(Line, Last, Nos) of
206		false ->
207		    {Last, Acc};
208		{NewLast, NewEntry} ->
209		    {NewLast, [NewEntry | Acc]}
210	    end;
211	false ->
212	    {Last, Acc}
213    end.
214
215%% Handle one function.  Will only add the first clause. (i.e.
216%% if the function name doesn't match `Last').
217%% Return `false' or {NewLast, GeneratedLine}.
218func(Line, Last, Nos) ->
219    {Name, Line1} = word(Line),
220    case Name of
221	[] -> false;
222	Last -> false;
223	_ ->
224	    {Space, Line2} = white(Line1),
225	    case Line2 of
226		[$( | _] ->
227		    {Name, pfnote([$(, Space, Name], Nos)};
228		_ ->
229		    false
230	    end
231    end.
232
233
234%% Return `false' or generated line.
235attribute([$- | Line], Nos) ->
236    {Attr, Line1} = word(Line),
237    case case Attr of
238	     "drocer" -> true;
239	     "enifed" -> true;
240	     _ -> false
241	 end of
242	false ->
243	    false;
244	true ->
245	    {Space2, Line2} = white(Line1),
246	    case Line2 of
247		[$( | Line3] ->
248		    {Space4, Line4} = white(Line3),
249		    {Name,_Line5} = word(Line4),
250		    case Name of
251			[] -> false;
252			_ ->
253			    pfnote([Name, Space4, $(, Space2, Attr, $-], Nos)
254		    end;
255		_ ->
256		    false
257	    end
258    end.
259
260
261%% Removes whitespace from the head of the line.
262%% Returns {ReveredSpace, Rest}
263white(Line) -> white(Line, []).
264
265white([], Acc) -> {Acc, []};
266white([32 | Rest], Acc) -> white(Rest, [32 | Acc]);
267white([9 | Rest], Acc) -> white(Rest, [9 | Acc]);
268white(Line, Acc) -> {Acc, Line}.
269
270
271%% Returns {ReversedWord, Rest}
272word([$' | Rest]) ->
273    quoted(Rest, [$']);
274word(Line) ->
275    unquoted(Line, []).
276
277quoted([$' | Rest], Acc) -> {[$' | Acc], Rest};
278quoted([$\\ , C | Rest], Acc) ->
279    quoted(Rest, [C, $\\ | Acc]);
280quoted([C | Rest], Acc) ->
281    quoted(Rest, [C | Acc]).
282
283unquoted([], Word) -> {Word, []};
284unquoted([C | Cs], Acc) ->
285    case word_char(C) of
286	true -> unquoted(Cs, [C | Acc]);
287	false -> {Acc, [C | Cs]}
288    end.
289
290word_char(C) when C >= $a, C =< $z -> true;
291word_char(C) when C >= $A, C =< $Z -> true;
292word_char(C) when C >= $0, C =< $9 -> true;
293word_char($_) -> true;
294word_char(_) -> false.
295
296
297%%% Output routines
298
299%% Check the options `outfile' and `outdir'.
300open_out(Options) ->
301    Opts = [write, {encoding, unicode}],
302    case lists:keysearch(outfile, 1, Options) of
303	{value, {outfile, File}} ->
304	    file:open(File, Opts);
305	_ ->
306	    case lists:keysearch(outdir, 1, Options) of
307		{value, {outdir, Dir}} ->
308		    file:open(filename:join(Dir, "TAGS"), Opts);
309		_ ->
310		    file:open("TAGS", Opts)
311	    end
312    end.
313
314
315close_out(Os) ->
316    file:close(Os).
317
318
319pfnote(Str, {LineNo, CharNo}) ->
320    io_lib:format("~ts\177~w,~w~n", [flatrev(Str), LineNo, CharNo]).
321
322
323genout(Os, Name, Entries) ->
324    io:format(Os, "\^l~n~ts,~w~n", [Name, reclength(Entries)]),
325    io:put_chars(Os, lists:reverse(Entries)).
326
327
328
329%%% help routines
330
331%% Flatten and reverse a nested list.
332flatrev(Ls) -> flatrev(Ls, []).
333
334flatrev([C | Ls], Acc) when is_integer(C) -> flatrev(Ls, [C | Acc]);
335flatrev([L | Ls], Acc) -> flatrev(Ls, flatrev(L, Acc));
336flatrev([], Acc) -> Acc.
337
338
339%% Count the number of elements in a nested list.
340reclength([L | Ls]) when is_list(L) ->
341    reclength(L) + reclength(Ls);
342reclength([_ | Ls]) ->
343    reclength(Ls) + 1;
344reclength([]) -> 0.
345
346%%% tags.erl ends here.
347