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