Let ‘cl-defun’ handle keyword arguments.
[apt-sources-list.git] / apt-sources-list.el
1 ;;; apt-sources-list.el --- Mode for editing APT source.list files
2 ;;
3 ;; Copyright (C) 2001-2003, Dr. Rafael Sepúlveda <drs@gnulinux.org.mx>
4 ;; 2009 Peter S. Galbraith <psg@debian.org>
5 ;; 2017 Joe Wreschnig
6 ;;
7 ;; Author: Dr. Rafael Sepúlveda <drs@gnulinux.org.mx>
8 ;; Maintainer: Joe Wreschnig <joe.wreschnig@gmail.com>
9 ;; URL: https://git.korewanetadesu.com/apt-sources-list.git
10 ;; Package-Requires: ((emacs "24.4"))
11 ;; Package-Version: 0
12 ;;
13 ;; This program is free software; you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or (at
16 ;; your option) any later version.
17 ;;
18 ;; This program is distributed in the hope that it will be useful, but
19 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 ;; General Public License for more details.
22 ;;
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26
27 ;;; Commentary:
28 ;;
29 ;; This package contains a major mode for editing APT’s “.list” files.
30 ;;
31 ;; The “/etc/apt/sources.list” file and other files in
32 ;; “/etc/apt/sources.list.d” tell APT, found on Debian-based systems and
33 ;; others, where to find packages for installation.
34 ;;
35 ;; This format specifies a package source with a single line, e.g.:
36 ;;
37 ;; deb http://deb.debian.org/debian stable main contrib
38 ;;
39 ;; For more information about the format you can read the manual
40 ;; pages “apt(8)” and “sources.list(5)”, also on the web at URL
41 ;; ‘https://manpages.debian.org/stable/apt/sources.list.5.en.html’
42 ;; and URL ‘https://manpages.debian.org/stable/apt/apt.8.en.html’.
43
44
45 ;;; Code:
46
47 (require 'cl-lib)
48 (require 'subr-x)
49 (eval-when-compile
50 (require 'rx))
51
52 (defgroup apt-sources-list nil
53 "Mode for editing APT sources.list file."
54 :group 'tools)
55
56 (defface apt-sources-list-type
57 '((t (:inherit font-lock-constant-face)))
58 "Face for a source’s type (i.e. “deb” or “deb-src”).")
59
60 (defface apt-sources-list-uri
61 '((t (:inherit font-lock-variable-name-face)))
62 "Face for a source’s URI.")
63
64 (defface apt-sources-list-suite
65 '((t (:inherit font-lock-type-face)))
66 "Face for a source’s suite (e.g. “unstable”, “stretch/updates”).")
67
68 (defface apt-sources-list-options
69 '((t (:inherit font-lock-builtin-face)))
70 "Face for a package source’s options (e.g. “[arch=amd64]”).")
71
72 (defface apt-sources-list-components
73 '((t (:inherit font-lock-keyword-face)))
74 "Face for a package source’s components (e.g. “main”, “non-free”).")
75
76 (defcustom apt-sources-list-suites
77 '("stable" "testing" "unstable" "oldstable" "jessie" "stretch" "sid")
78 "Suites to offer for completion.
79
80 The first item in this list is used as the default value when
81 editing sources."
82 :type '(repeat string))
83
84 (defcustom apt-sources-list-components
85 '("main" "contrib" "non-free")
86 "Components to offer for completion.
87
88 The first item in this list is used as the default value when
89 editing sources."
90 :type '(repeat string))
91
92 (defcustom apt-sources-list-name-format "# %s"
93 "Format used in the name of a new source line.
94
95 This is used by ‘apt-sources-list-insert’. It should contain a
96 single “%s” which will be replaced with the source name."
97 :type 'string
98 :group 'apt-sources-list)
99
100 (defconst apt-sources-list-one-line
101 (rx line-start
102 (zero-or-more blank)
103 (group (or "deb" "deb-src"))
104 (one-or-more blank)
105 (optional
106 ;; TODO: This matches malformed options.
107 "[" (group (one-or-more (not (any "]\n#")))) "]"
108 (one-or-more blank))
109 (group
110 (one-or-more (any "-A-Za-z0-9._"))
111 ":"
112 (one-or-more (not (any " \t\n#"))))
113 (one-or-more blank)
114 (group
115 (or (and (one-or-more (not (any " \t\n#"))) "/")
116 (and (zero-or-more (not (any " \t\n#")))
117 (not (any " \t\n/#"))
118 (one-or-more blank)
119 (group
120 (one-or-more (not (any " \t\n#")))
121 (zero-or-more
122 (one-or-more blank)
123 (one-or-more (not (any " \t\n#"))))))))
124 (zero-or-more blank)
125 (or line-end "#"))
126 "Regex to match a valid APT source in one-line format.")
127
128 (defconst apt-sources-list-font-lock-keywords
129 `((,apt-sources-list-one-line
130 (1 'apt-sources-list-type)
131 (2 'apt-sources-list-options nil t)
132 (3 'apt-sources-list-uri)
133 (4 'apt-sources-list-suite)
134 (5 'apt-sources-list-components t t)))
135 "Faces for parts of sources.list lines.")
136
137 (cl-defun apt-sources-list-insert
138 (uri &key name (type "deb") options
139 (suite (car apt-sources-list-suites))
140 (components (car apt-sources-list-components)))
141 "Insert a new package source at URI, with extra PROPERTIES.
142
143 When called interactively without a prefix argument, assume
144 the type is “deb” and no special options.
145
146 When called from Lisp, optional properties include:
147
148 ‘:name’ - a source name to include in a leading comment
149 ‘:type’ - “deb” or “deb-src”, defaulting to “deb”
150 ‘:options’ - an options string, without […] delimiters
151 ‘:suite’ - defaults to the first of ‘apt-sources-list-suites’
152 ‘:components’ - defaults to the first of ‘apt-sources-list-components’
153
154 You should read the official APT documentation for further
155 explanation of the format."
156 (interactive
157 (let* ((_ (barf-if-buffer-read-only))
158 (name (read-string "Source name: "))
159 (type (if current-prefix-arg
160 (completing-read "Type: " '("deb" "deb-src") nil t "deb")
161 "deb"))
162 (options (if current-prefix-arg (read-string "Options: ") ""))
163 (uri (read-string "URI: " "https://"))
164 (suite (completing-read "Suite: "
165 apt-sources-list-suites nil nil
166 (car apt-sources-list-suites)))
167 (components
168 (unless (string-suffix-p "/" suite)
169 (apt-sources-list--read-components))))
170 (list uri
171 :name (unless (string-blank-p name) name)
172 :type type
173 :options (unless (string-blank-p options) options)
174 :suite suite :components components)))
175
176 (when name
177 (insert (format apt-sources-list-name-format name) "\n"))
178 (insert type (if options (format " [%s] " options) " ") uri " "
179 suite (if (string-suffix-p "/" suite) ""
180 (format " %s" components))))
181
182 (defun apt-sources-list-forward-source (&optional n)
183 "Go N source lines forward (backward if N is negative)."
184 (interactive "p")
185 (save-excursion
186 (if (> (or n 1) 0)
187 (end-of-line)
188 (beginning-of-line))
189 (condition-case nil
190 (re-search-forward apt-sources-list-one-line nil nil n)
191 (search-failed
192 (error "No further repositories found buffer"))))
193 (goto-char (match-beginning 0)))
194
195 (defun apt-sources-list-backward-source (&optional n)
196 "Go N source lines backward (forward if N is negative)."
197 (interactive "p")
198 (apt-sources-list-forward-source (- (or n 1))))
199
200 (defun apt-sources-list-source-p ()
201 "Return non-nil if the line at point is a source."
202 (string-match-p apt-sources-list-one-line (thing-at-point 'line)))
203
204 (define-error 'apt-sources-list-not-found
205 "The point is not on an APT source line")
206
207 (define-error 'apt-sources-list-suite-component-mismatch
208 "Exact suite paths (ending with “/”) may not specify components")
209
210 (defun apt-sources-list-match-source ()
211 "Fill the match data with the source at point.
212
213 If there is no source, error."
214 (save-mark-and-excursion
215 (beginning-of-line)
216 (or (looking-at apt-sources-list-one-line)
217 (signal 'apt-sources-list-not-found nil))))
218
219 (defun apt-sources-list-change-type (&optional type)
220 "Change the type of the source at point to TYPE.
221
222 Interactively or when TYPE is nil, toggle the type between “deb”
223 and “deb-src”."
224 (interactive "*")
225 (save-match-data
226 (apt-sources-list-match-source)
227 (unless type
228 (setq type (if (equal (match-string 1) "deb") "deb-src" "deb")))
229 (save-mark-and-excursion
230 (replace-match type t t nil 1))))
231
232 (defun apt-sources-list-change-options (options)
233 "Change the options of the source at point to OPTIONS (excluding []s)."
234 (interactive
235 (list (save-match-data
236 (barf-if-buffer-read-only)
237 (apt-sources-list-match-source)
238 (read-string "Options: " (match-string 2)))))
239 (save-match-data
240 (apt-sources-list-match-source)
241 (when (= 0 (length options))
242 (setq options nil))
243 (save-mark-and-excursion
244 (cond ((and (match-string 2) options)
245 (replace-match options t t nil 2))
246 ((match-string 2)
247 (delete-region (- (match-beginning 2) 2) (1+ (match-end 2))))
248 (options
249 (replace-match (concat (match-string 1) " [" options "]")
250 nil t nil 1))))))
251
252 (defun apt-sources-list-change-uri (uri)
253 "Change the URI of the source at point to URI."
254 (interactive
255 (list (save-match-data
256 (barf-if-buffer-read-only)
257 (apt-sources-list-match-source)
258 (read-string "URI: " (match-string 3)))))
259 (save-match-data
260 (apt-sources-list-match-source)
261 (save-mark-and-excursion
262 (replace-match uri t t nil 3))))
263
264 (defun apt-sources-list--read-components (&optional initial)
265 "Read a components string, defaulting to INITIAL."
266 (save-match-data
267 (let ((minibuffer-local-completion-map
268 (copy-keymap minibuffer-local-completion-map)))
269 (define-key minibuffer-local-completion-map (kbd "<SPC>") nil)
270 (completing-read "Components: "
271 apt-sources-list-components
272 nil nil
273 (or initial (car apt-sources-list-components))))))
274
275 (defun apt-sources-list-change-suite (suite &optional default-components)
276 "Change the suite of the source at point to SUITE.
277
278 If the new suite requires components and the old one did not,
279 DEFAULT-COMPONENTS is used. If none are provided, the first item
280 in ‘apt-sources-list-components’ is used."
281 (interactive
282 (save-match-data
283 (barf-if-buffer-read-only)
284 (apt-sources-list-match-source)
285 (let ((components (match-string 5))
286 (suite (completing-read "Suite: "
287 apt-sources-list-suites)))
288 (if (not (string-suffix-p "/" suite))
289 (list suite (apt-sources-list--read-components))
290 (list suite)))))
291
292 (save-mark-and-excursion
293 (save-match-data
294 (apt-sources-list-match-source)
295 (if (string-suffix-p "/" suite)
296 (when (match-string 5)
297 (replace-match "" t t nil 5))
298 (setq suite (concat suite " "
299 (or (match-string 5)
300 default-components
301 (car apt-sources-list-components)
302 "main"))))
303 (replace-match suite t t nil 4))))
304
305 (defun apt-sources-list-change-components (components)
306 "Change the components of the source at point to COMPONENTS."
307 (interactive
308 (save-match-data
309 (barf-if-buffer-read-only)
310 (apt-sources-list-match-source)
311 (when (string-suffix-p "/" (match-string 4))
312 (signal 'apt-sources-list-suite-component-mismatch nil))
313 (list (apt-sources-list--read-components
314 (substring-no-properties (match-string 5))))))
315
316 (save-match-data
317 (apt-sources-list-match-source)
318 (when (string-suffix-p "/" (match-string 4))
319 (signal 'apt-sources-list-suite-component-mismatch nil))
320 (save-mark-and-excursion
321 (replace-match components t t nil 5))))
322
323 (defun apt-sources-list-replicate ()
324 "Copy the source line, toggling the type."
325 (interactive "*")
326 (apt-sources-list-match-source)
327 (let ((copy (buffer-substring (line-beginning-position)
328 (line-end-position))))
329 (save-excursion
330 (end-of-line)
331 (insert (concat "\n" copy))
332 (apt-sources-list-change-type))))
333
334 ;;;###autoload
335 (define-derived-mode apt-sources-list-mode prog-mode "apt/sources.list"
336 "Major mode for editing APT’s “.list” files.
337
338 The “/etc/apt/sources.list” file and other files in
339 “/etc/apt/sources.list.d” tell APT, found on Debian-based systems
340 and others, where to find packages for installation.
341
342 This format specifies a package source with a single line, e.g.:
343
344 deb http://deb.debian.org/debian stable main contrib
345
346 For more information about the format you can read the manual
347 pages “apt(8)” and “sources.list(5)”, also on the web at URL
348 ‘https://manpages.debian.org/stable/apt/sources.list.5.en.html’
349 and URL ‘https://manpages.debian.org/stable/apt/apt.8.en.html’.
350
351 \\{apt-sources-list-mode-map}
352
353 The above editing commands will raise errors if the current line
354 is not a correctly-formatted APT source."
355 :syntax-table
356 (let ((syntab (make-syntax-table)))
357 (modify-syntax-entry ?# "<" syntab)
358 (modify-syntax-entry ?\n "> " syntab)
359 syntab)
360
361 (setq-local comment-start "#")
362 (setq-local comment-start-skip "#+ *")
363 (font-lock-add-keywords nil apt-sources-list-font-lock-keywords))
364
365 (add-to-list
366 'auto-mode-alist
367 (cons (rx (or (and (any "./") "sources.list")
368 (and "/sources.list.d/" (one-or-more anything) ".list"))
369 string-end)
370 #'apt-sources-list-mode))
371
372 (let ((map apt-sources-list-mode-map))
373 (define-key map (kbd "C-c C-i") #'apt-sources-list-insert)
374 (define-key map (kbd "C-c C-r") #'apt-sources-list-replicate)
375 (define-key map (kbd "C-c C-t") #'apt-sources-list-change-type)
376 (define-key map (kbd "C-c C-o") #'apt-sources-list-change-options)
377 (define-key map (kbd "C-c C-u") #'apt-sources-list-change-uri)
378 (define-key map (kbd "C-c C-s") #'apt-sources-list-change-suite)
379 (define-key map (kbd "C-c C-c") #'apt-sources-list-change-components)
380 (define-key map [remap forward-list] #'apt-sources-list-forward-source)
381 (define-key map [remap backward-list] #'apt-sources-list-backward-source)
382
383 (easy-menu-define apt-sources-list-mode-menu map
384 "Menu for APT sources.list mode."
385 '("APT"
386 ["Insert Source" apt-sources-list-insert]
387 ["Copy Source" apt-sources-list-replicate
388 (apt-sources-list-source-p)]
389 "--"
390 ["Backward Source" apt-sources-list-backward-source]
391 ["Forward Source" apt-sources-list-forward-source]
392 "--"
393 ["Change Type" apt-sources-list-change-type
394 (apt-sources-list-source-p)]
395 ["Change Options" apt-sources-list-change-options
396 (apt-sources-list-source-p)]
397 ["Change URI" apt-sources-list-change-uri
398 (apt-sources-list-source-p)]
399 ["Change Suite" apt-sources-list-change-suite
400 (apt-sources-list-source-p)]
401 ["Change Components" apt-sources-list-change-components
402 (ignore-errors
403 (and (apt-sources-list-match-source) (match-string 5)))])))
404
405
406 (provide 'apt-sources-list)
407 ;;; apt-sources-list.el ends here