(Fformat): Prevent a buffer overrun when the format
[bpt/emacs.git] / lisp / emacs-lisp / lisp-mnt.el
CommitLineData
0fc37e7d
ER
1;;; lisp-mnt.el --- minor mode for Emacs Lisp maintainers
2
2505742b 3;; Copyright (C) 1992, 1994, 1997, 2000 Free Software Foundation, Inc.
0fc37e7d
ER
4
5;; Author: Eric S. Raymond <esr@snark.thyrsus.com>
6;; Maintainer: Eric S. Raymond <esr@snark.thyrsus.com>
7;; Created: 14 Jul 1992
0fc37e7d 8;; Keywords: docs
1894df21 9;; X-Bogus-Bureaucratic-Cruft: Gruad will get you if you don't watch out!
0fc37e7d
ER
10
11;; This file is part of GNU Emacs.
12
13;; GNU Emacs is free software; you can redistribute it and/or modify
14;; it under the terms of the GNU General Public License as published by
7c938215 15;; the Free Software Foundation; either version 2, or (at your option)
0fc37e7d
ER
16;; any later version.
17
18;; GNU Emacs is distributed in the hope that it will be useful,
19;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;; GNU General Public License for more details.
22
23;; You should have received a copy of the GNU General Public License
d5eead69 24;; along with GNU Emacs; see the file COPYING. If not, write to
470c5afe
EN
25;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
26;; Boston, MA 02111-1307, USA.
0fc37e7d
ER
27
28;;; Commentary:
29
30;; This minor mode adds some services to Emacs-Lisp editing mode.
31;;
32;; First, it knows about the header conventions for library packages.
33;; One entry point supports generating synopses from a library directory.
34;; Another can be used to check for missing headers in library files.
35;;
36;; Another entry point automatically addresses bug mail to a package's
37;; maintainer or author.
38
39;; This file can be loaded by your lisp-mode-hook. Have it (require 'lisp-mnt)
40
41;; This file is an example of the header conventions. Note the following
42;; features:
43;;
44;; * Header line --- makes it possible to extract a one-line summary of
45;; the package's uses automatically for use in library synopses, KWIC
46;; indexes and the like.
47;;
48;; Format is three semicolons, followed by the filename, followed by
49;; three dashes, followed by the summary. All fields space-separated.
50;;
51;; * Author line --- contains the name and net address of at least
52;; the principal author.
53;;
68756f1f 54;; If there are multiple authors, they should be listed on continuation
0fc37e7d
ER
55;; lines led by ;;<TAB>, like this:
56;;
57;; ;; Author: Ashwin Ram <Ram-Ashwin@cs.yale.edu>
58;; ;; Dave Sill <de5@ornl.gov>
59;; ;; David Lawrence <tale@pawl.rpi.edu>
60;; ;; Noah Friedman <friedman@ai.mit.edu>
61;; ;; Joe Wells <jbw@maverick.uswest.com>
62;; ;; Dave Brennan <brennan@hal.com>
63;; ;; Eric Raymond <esr@snark.thyrsus.com>
64;;
65;; This field may have some special values; notably "FSF", meaning
66;; "Free Software Foundation".
67;;
68;; * Maintainer line --- should be a single name/address as in the Author
69;; line, or an address only, or the string "FSF". If there is no maintainer
70;; line, the person(s) in the Author field are presumed to be it. The example
71;; in this file is mildly bogus because the maintainer line is redundant.
ca4bd734 72;; The idea behind these two fields is to be able to write a Lisp function
0fc37e7d
ER
73;; that does "send mail to the author" without having to mine the name out by
74;; hand. Please be careful about surrounding the network address with <> if
75;; there's also a name in the field.
76;;
77;; * Created line --- optional, gives the original creation date of the
78;; file. For historical interest, basically.
79;;
80;; * Version line --- intended to give the reader a clue if they're looking
68756f1f
ER
81;; at a different version of the file than the one they're accustomed to. This
82;; may be an RCS or SCCS header.
0fc37e7d
ER
83;;
84;; * Adapted-By line --- this is for FSF's internal use. The person named
85;; in this field was the one responsible for installing and adapting the
86;; package for the distribution. (This file doesn't have one because the
87;; author *is* one of the maintainers.)
88;;
89;; * Keywords line --- used by the finder code (now under construction)
c0d79871 90;; for finding Emacs Lisp code related to a topic.
0fc37e7d 91;;
1894df21
NF
92;; * X-Bogus-Bureaucratic-Cruft line --- this is a joke and an example
93;; of a comment header. Headers starting with `X-' should never be used
94;; for any real purpose; this is the way to safely add random headers
95;; without invoking the wrath of any program.
0fc37e7d 96;;
ca4bd734 97;; * Commentary line --- enables Lisp code to find the developer's and
0fc37e7d
ER
98;; maintainers' explanations of the package internals.
99;;
100;; * Change log line --- optional, exists to terminate the commentary
101;; section and start a change-log part, if one exists.
102;;
c0d79871 103;; * Code line --- exists so Lisp can know where commentary and/or
0fc37e7d
ER
104;; change-log sections end.
105;;
106;; * Footer line --- marks end-of-file so it can be distinguished from
107;; an expanded formfeed or the results of truncation.
108
109;;; Change Log:
110
111;; Tue Jul 14 23:44:17 1992 ESR
112;; * Created.
113
114;;; Code:
115
d5eead69
RS
116;;; Variables:
117
666b9413
SE
118(defgroup lisp-mnt nil
119 "Minor mode for Emacs Lisp maintainers."
120 :prefix "lm-"
121 :group 'maint)
122
438cdcde 123(defcustom lm-header-prefix "^;+[ \t]+\\(@(#)\\)?[ \t]*\\$?"
d5eead69 124 "Prefix that is ignored before the tag.
ca4bd734
RS
125For example, you can write the 1st line synopsis string and headers like this
126in your Lisp package:
d5eead69
RS
127
128 ;; @(#) package.el -- pacakge description
129 ;;
130 ;; @(#) $Maintainer: Person Foo Bar $
131
132The @(#) construct is used by unix what(1) and
666b9413
SE
133then $identifier: doc string $ is used by GNU ident(1)"
134 :type 'regexp
135 :group 'lisp-mnt)
136
137(defcustom lm-comment-column 16
138 "Column used for placing formatted output."
139 :type 'integer
140 :group 'lisp-mnt)
141
142(defcustom lm-commentary-header "Commentary\\|Documentation"
143 "Regexp which matches start of documentation section."
144 :type 'regexp
145 :group 'lisp-mnt)
146
147(defcustom lm-history-header "Change Log\\|History"
148 "Regexp which matches the start of code log section."
149 :type 'regexp
150 :group 'lisp-mnt)
d5eead69
RS
151
152;;; Functions:
153
0fc37e7d
ER
154;; These functions all parse the headers of the current buffer
155
2505742b 156(defun lm-get-header-re (header &optional mode)
6e24ad22 157 "Return regexp for matching HEADER.
ca4bd734
RS
158If called with optional MODE and with value `section',
159return section regexp instead."
438cdcde
SM
160 (if (eq mode 'section)
161 (concat "^;;;;* " header ":[ \t]*$")
162 (concat lm-header-prefix header "[ \t]*:[ \t]*")))
d5eead69 163
2505742b 164(defun lm-get-package-name ()
6e24ad22 165 "Return package name by looking at the first line."
d5eead69
RS
166 (save-excursion
167 (goto-char (point-min))
168 (if (and (looking-at (concat lm-header-prefix))
169 (progn (goto-char (match-end 0))
170 (looking-at "\\([^\t ]+\\)")
171 (match-end 1)))
2505742b 172 (match-string-no-properties 1))))
d5eead69
RS
173
174(defun lm-section-mark (header &optional after)
175 "Return the buffer location of a given section start marker.
ca4bd734
RS
176The HEADER is the section mark string to search for.
177If AFTER is non-nil, return the location of the next line."
0fc37e7d
ER
178 (save-excursion
179 (let ((case-fold-search t))
180 (goto-char (point-min))
d5eead69 181 (if (re-search-forward (lm-get-header-re header 'section) nil t)
0fc37e7d
ER
182 (progn
183 (beginning-of-line)
64e07c7b 184 (if after (forward-line 1))
2505742b 185 (point))))))
0fc37e7d 186
d5eead69 187(defsubst lm-code-mark ()
ca4bd734 188 "Return the buffer location of the `Code' start marker."
0fc37e7d
ER
189 (lm-section-mark "Code"))
190
d5eead69 191(defsubst lm-commentary-mark ()
ca4bd734 192 "Return the buffer location of the `Commentary' start marker."
d5eead69
RS
193 (lm-section-mark lm-commentary-header))
194
195(defsubst lm-history-mark ()
ca4bd734 196 "Return the buffer location of the `History' start marker."
d5eead69
RS
197 (lm-section-mark lm-history-header))
198
199(defun lm-header (header)
ca4bd734
RS
200 "Return the contents of the header named HEADER."
201 (goto-char (point-min))
202 (let ((case-fold-search t))
438cdcde
SM
203 (when (and (re-search-forward (lm-get-header-re header) (lm-code-mark) t)
204 ;; RCS ident likes format "$identifier: data$"
205 (looking-at
206 (if (save-excursion
207 (skip-chars-backward "^$" (match-beginning 0))
208 (= (point) (match-beginning 0)))
209 "[^\n]+" "[^$\n]+")))
210 (match-string-no-properties 0))))
0fc37e7d 211
d5eead69 212(defun lm-header-multiline (header)
ca4bd734 213 "Return the contents of the header named HEADER, with continuation lines.
d5eead69 214The returned value is a list of strings, one per line."
0fc37e7d
ER
215 (save-excursion
216 (goto-char (point-min))
d5eead69 217 (let ((res (lm-header header)))
be961cd5 218 (when res
d5eead69
RS
219 (setq res (list res))
220 (forward-line 1)
438cdcde
SM
221 (while (and (or (looking-at (concat lm-header-prefix "[\t ]+"))
222 (and (not (looking-at
223 (lm-get-header-re "\\sw\\(\\sw\\|\\s_\\)*")))
224 (looking-at lm-header-prefix)))
225 (goto-char (match-end 0))
226 (looking-at ".+"))
227 (setq res (cons (match-string-no-properties 0) res))
2505742b 228 (forward-line 1)))
438cdcde 229 (nreverse res))))
0fc37e7d
ER
230
231;; These give us smart access to the header fields and commentary
232
be961cd5
SM
233(defmacro lm-with-file (file &rest body)
234 (let ((filesym (make-symbol "file")))
235 `(save-excursion
236 (let ((,filesym ,file))
237 (if ,filesym (set-buffer (find-file-noselect ,filesym)))
238 (prog1 (progn ,@body)
239 (if (and ,filesym (not (get-buffer-window (current-buffer) t)))
240 (kill-buffer (current-buffer))))))))
241(put 'lm-with-file 'lisp-indent-function 1)
242(put 'lm-with-file 'edebug-form-spec t)
243
0fc37e7d 244(defun lm-summary (&optional file)
ca4bd734 245 "Return the one-line summary of file FILE, or current buffer if FILE is nil."
be961cd5 246 (lm-with-file file
0fc37e7d 247 (goto-char (point-min))
2505742b
DL
248 (if (and (looking-at lm-header-prefix)
249 (progn (goto-char (match-end 0))
250 (looking-at "[^ ]+[ \t]+--+[ \t]+\\(.*\\)")))
251 (let ((summary (match-string-no-properties 1)))
be961cd5
SM
252 ;; Strip off -*- specifications.
253 (if (string-match "[ \t]*-\\*-.*-\\*-" summary)
254 (substring summary 0 (match-beginning 0))
255 summary)))))
0fc37e7d 256
68756f1f 257(defun lm-crack-address (x)
6e24ad22 258 "Split up an email address X into full name and real email address.
ca4bd734 259The value is a cons of the form (FULLNAME . ADDRESS)."
68756f1f 260 (cond ((string-match "\\(.+\\) [(<]\\(\\S-+@\\S-+\\)[>)]" x)
2505742b
DL
261 (cons (match-string 1 x)
262 (match-string 2 x)))
68756f1f 263 ((string-match "\\(\\S-+@\\S-+\\) [(<]\\(.*\\)[>)]" x)
2505742b
DL
264 (cons (match-string 2 x)
265 (match-string 1 x)))
68756f1f
ER
266 ((string-match "\\S-+@\\S-+" x)
267 (cons nil x))
268 (t
269 (cons x nil))))
270
0fc37e7d 271(defun lm-authors (&optional file)
ca4bd734
RS
272 "Return the author list of file FILE, or current buffer if FILE is nil.
273Each element of the list is a cons; the car is the full name,
274the cdr is an email address."
be961cd5 275 (lm-with-file file
68756f1f 276 (let ((authorlist (lm-header-multiline "author")))
be961cd5 277 (mapcar 'lm-crack-address authorlist))))
0fc37e7d
ER
278
279(defun lm-maintainer (&optional file)
ca4bd734
RS
280 "Return the maintainer of file FILE, or current buffer if FILE is nil.
281The return value has the form (NAME . ADDRESS)."
be961cd5
SM
282 (lm-with-file file
283 (let ((maint (lm-header "maintainer")))
284 (if maint
285 (lm-crack-address maint)
286 (car (lm-authors))))))
0fc37e7d
ER
287
288(defun lm-creation-date (&optional file)
ca4bd734 289 "Return the created date given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
290 (lm-with-file file
291 (lm-header "created")))
0fc37e7d 292
07d505c1
GM
293(defun lm-last-modified-date (&optional file iso-date)
294 "Return the modify-date given in file FILE, or current buffer if FILE is nil.
295ISO-DATE non-nil means return the date in ISO 8601 format."
be961cd5 296 (lm-with-file file
07d505c1
GM
297 (when (progn (goto-char (point-min))
298 (re-search-forward
299 "\\$[I]d: [^ ]+ [^ ]+ \\([^/]+\\)/\\([^/]+\\)/\\([^ ]+\\) "
300 (lm-code-mark) t))
301 (let ((dd (match-string 3))
302 (mm (match-string 2))
303 (yyyy (match-string 1)))
304 (if iso-date
305 (format "%s-%s-%s" yyyy mm dd)
306 (format "%s %s %s"
307 dd
308 (nth (string-to-int mm)
309 '("" "Jan" "Feb" "Mar" "Apr" "May" "Jun"
310 "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))
311 yyyy))))))
0fc37e7d
ER
312
313(defun lm-version (&optional file)
ca4bd734 314 "Return the version listed in file FILE, or current buffer if FILE is nil.
2505742b 315This can be found in an RCS or SCCS header."
be961cd5 316 (lm-with-file file
2505742b
DL
317 (or (lm-header "version")
318 (let ((header-max (lm-code-mark)))
319 (goto-char (point-min))
320 (cond
321 ;; Look for an RCS header
322 ((re-search-forward "\\$[I]d: [^ ]+ \\([^ ]+\\) " header-max t)
323 (match-string-no-properties 1))
324 ((re-search-forward "\\$Revision: +\\([^ ]+\\) " header-max t)
325 (match-string-no-properties 1))
326 ;; Look for an SCCS header
327 ((re-search-forward
328 (concat
329 (regexp-quote "@(#)")
330 (regexp-quote (file-name-nondirectory (buffer-file-name)))
331 "\t\\([012345679.]*\\)")
332 header-max t)
333 (match-string-no-properties 1)))))))
0fc37e7d
ER
334
335(defun lm-keywords (&optional file)
ca4bd734 336 "Return the keywords given in file FILE, or current buffer if FILE is nil."
be961cd5
SM
337 (lm-with-file file
338 (let ((keywords (lm-header "keywords")))
339 (and keywords (downcase keywords)))))
0fc37e7d
ER
340
341(defun lm-adapted-by (&optional file)
ca4bd734
RS
342 "Return the adapted-by names in file FILE, or current buffer if FILE is nil.
343This is the name of the person who cleaned up this package for
344distribution."
be961cd5
SM
345 (lm-with-file file
346 (lm-header "adapted-by")))
0fc37e7d 347
64e07c7b 348(defun lm-commentary (&optional file)
ca4bd734 349 "Return the commentary in file FILE, or current buffer if FILE is nil.
e7b377f0
DL
350The value is returned as a string. In the file, the commentary starts
351with the tag `Commentary' or `Documentation' and ends with one of the
352tags `Code', `Change Log' or `History'."
be961cd5 353 (lm-with-file file
2505742b
DL
354 (let ((commentary (lm-commentary-mark))
355 (change-log (lm-history-mark))
356 (code (lm-code-mark)))
357 (cond
358 ((and commentary change-log)
359 (buffer-substring-no-properties commentary change-log))
360 ((and commentary code)
361 (buffer-substring-no-properties commentary code))))))
0fc37e7d
ER
362
363;;; Verification and synopses
364
d5eead69 365(defun lm-insert-at-column (col &rest strings)
6e24ad22 366 "Insert, at column COL, list of STRINGS."
d5eead69 367 (if (> (current-column) col) (insert "\n"))
b4d73c71 368 (move-to-column col t)
d5eead69 369 (apply 'insert strings))
0fc37e7d 370
e7b377f0 371(defun lm-verify (&optional file showok verb)
0fc37e7d 372 "Check that the current buffer (or FILE if given) is in proper format.
ca4bd734 373If FILE is a directory, recurse on its files and generate a report in
0fc37e7d 374a temporary buffer."
d5eead69 375 (interactive)
2505742b
DL
376 (let* ((verb (or verb (interactive-p)))
377 (ret (and verb "Ok."))
378 name)
d5eead69 379 (if (and file (file-directory-p file))
2505742b
DL
380 (setq ret
381 (with-temp-buffer
382 (mapcar
383 (lambda (f)
384 (if (string-match ".*\\.el\\'" f)
385 (let ((status (lm-verify f)))
386 (insert f ":")
387 (if status
388 (lm-insert-at-column lm-comment-column status
389 "\n")
390 (if showok
391 (lm-insert-at-column lm-comment-column
392 "OK\n"))))))
393 (directory-files file))))
be961cd5 394 (lm-with-file file
d5eead69 395 (setq name (lm-get-package-name))
2505742b
DL
396 (setq ret
397 (cond
398 ((null name)
399 "Can't find a package NAME")
400 ((not (lm-authors))
401 "Author: tag missing.")
402 ((not (lm-maintainer))
403 "Maintainer: tag missing.")
404 ((not (lm-summary))
405 "Can't find a one-line 'Summary' description")
406 ((not (lm-keywords))
407 "Keywords: tag missing.")
408 ((not (lm-commentary-mark))
409 "Can't find a 'Commentary' section marker.")
410 ((not (lm-history-mark))
411 "Can't find a 'History' section marker.")
412 ((not (lm-code-mark))
413 "Can't find a 'Code' section marker")
414 ((progn
415 (goto-char (point-max))
416 (not
417 (re-search-backward
418 (concat "^;;;[ \t]+" name "[ \t]+ends here[ \t]*$"
419 "\\|^;;;[ \t]+ End of file[ \t]+" name)
420 nil t)))
421 (format "Can't find a footer line for [%s]" name))
422 (t
423 ret)))))
d5eead69
RS
424 (if verb
425 (message ret))
2505742b 426 ret))
0fc37e7d
ER
427
428(defun lm-synopsis (&optional file showall)
429 "Generate a synopsis listing for the buffer or the given FILE if given.
ca4bd734
RS
430If FILE is a directory, recurse on its files and generate a report in
431a temporary buffer. If SHOWALL is non-nil, also generate a line for files
0fc37e7d 432which do not include a recognizable synopsis."
d5eead69
RS
433 (interactive
434 (list
435 (read-file-name "Synopsis for (file or dir): ")))
436
0fc37e7d 437 (if (and file (file-directory-p file))
2505742b 438 (with-temp-buffer
0fc37e7d 439 (mapcar
2505742b
DL
440 (lambda (f)
441 (if (string-match "\\.el\\'" f)
442 (let ((syn (lm-synopsis f)))
443 (if syn
444 (progn
445 (insert f ":")
446 (lm-insert-at-column lm-comment-column syn "\n"))
447 (when showall
448 (insert f ":")
449 (lm-insert-at-column lm-comment-column "NA\n"))))))
450 (directory-files file)))
451 (save-excursion
452 (if file
453 (find-file file))
454 (prog1
455 (lm-summary)
456 (if file
457 (kill-buffer (current-buffer)))))))
458
459(eval-when-compile (defvar report-emacs-bug-address))
0fc37e7d
ER
460
461(defun lm-report-bug (topic)
462 "Report a bug in the package currently being visited to its maintainer.
6e24ad22 463Prompts for bug subject TOPIC. Leaves you in a mail buffer."
68756f1f 464 (interactive "sBug Subject: ")
2505742b
DL
465 (require 'emacsbug)
466 (let ((package (lm-get-package-name))
467 (addr (lm-maintainer))
468 (version (lm-version)))
469 (compose-mail (if addr
470 (concat (car addr) " <" (cdr addr) ">")
471 report-emacs-bug-address)
472 topic)
0fc37e7d 473 (goto-char (point-max))
2505742b
DL
474 (insert "\nIn " package)
475 (if version
476 (insert " version " version))
477 (newline 2)
d5eead69 478 (message
0fc37e7d
ER
479 (substitute-command-keys "Type \\[mail-send] to send bug report."))))
480
481(provide 'lisp-mnt)
482
483;;; lisp-mnt.el ends here