9

I would like to tangle a single src block to several files, rather than just one. Ideally I would write something like

#+BEGIN_SRC conf :tangle one.conf two.conf three.conf

but this produces one file names “one.conf two.conf three.conf"

I can make it work by creating a named reference with noweb and then creating three different source blocks each tangled to the corresponding file but this seems to be a bit baroque (and error-prone)

Is there a clean way to do what I want?

Use-case: creating conf files for slightly different situations without code duplication.

Thanks in advance

Wilder
  • 95
  • 6
  • I see the goal. If the program you want to configure admits includes in the config file that would be preferable over duplicating sections of the configuration with the help of `org-babel-tangle`. But maybe the program to be configured does not allow includes. – Tobias Feb 24 '18 at 01:40

1 Answers1

11

Below you find an :override advice for org-babel-tangle-collect-blocks of org-mode 9.1.6 that allows lists of tangle file names instead of just one tangle file name.

The code of the advice is largely a copy of org-babel-tangle-collect-blocks. Only the lines marked with Tobias are modified or added. That would make the modification a good pull request if someone is willing to submit it to org-mode.

The elisp code also contains an :around advice for org-babel-tangle-single-src-block that takes over if you tangle only the source block at point by calling org-babel-tangle with prefix arg. That advice is not so appropriate to derive a pull-request from it. Therefore, I give a short description here what it does:

The advice for org-babel-tangle-single-src-block gets the source block info for the source block at point. If the :tangle header argument of the source block is not a list the old version of org-babel-tangle-single-src-block is called.

Otherwise for each tangle file in the :tangle list org-babel-get-src-block-info is modified such that it returns that file as value of the :tangle argument and org-babel-tangle-single-src-block is called.

(defun org-babel-tangle-collect-blocks-handle-tangle-list (&optional language tangle-file)
  "Can be used as :override advice for `org-babel-tangle-collect-blocks'.
Handles lists of :tangle files."
  (let ((counter 0) last-heading-pos blocks)
    (org-babel-map-src-blocks (buffer-file-name)
      (let ((current-heading-pos
         (org-with-wide-buffer
          (org-with-limited-levels (outline-previous-heading)))))
    (if (eq last-heading-pos current-heading-pos) (cl-incf counter)
      (setq counter 1)
      (setq last-heading-pos current-heading-pos)))
      (unless (org-in-commented-heading-p)
    (let* ((info (org-babel-get-src-block-info 'light))
           (src-lang (nth 0 info))
           (src-tfiles (cdr (assq :tangle (nth 2 info))))) ; Tobias: accept list for :tangle
      (unless (consp src-tfiles) ; Tobias: unify handling of strings and lists for :tangle
        (setq src-tfiles (list src-tfiles))) ; Tobias: unify handling
      (dolist (src-tfile src-tfiles) ; Tobias: iterate over list
        (unless (or (string= src-tfile "no")
            (and tangle-file (not (equal tangle-file src-tfile)))
            (and language (not (string= language src-lang))))
          ;; Add the spec for this block to blocks under its
          ;; language.
          (let ((by-lang (assoc src-lang blocks))
            (block (org-babel-tangle-single-block counter)))
        (setcdr (assoc :tangle (nth 4 block)) src-tfile) ; Tobias: 
        (if by-lang (setcdr by-lang (cons block (cdr by-lang)))
          (push (cons src-lang (list block)) blocks)))))))) ; Tobias: just ()
    ;; Ensure blocks are in the correct order.
    (mapcar (lambda (b) (cons (car b) (nreverse (cdr b)))) blocks)))

(defun org-babel-tangle-single-block-handle-tangle-list (oldfun block-counter &optional only-this-block)
  "Can be used as :around advice for `org-babel-tangle-single-block'.
If the :tangle header arg is a list of files. Handle all files"
  (let* ((info (org-babel-get-src-block-info))
     (params (nth 2 info))
     (tfiles (cdr (assoc :tangle params))))
    (if (null (and only-this-block (consp tfiles)))
    (funcall oldfun block-counter only-this-block)
      (cl-assert (listp tfiles) nil
         ":tangle only allows a tangle file name or a list of tangle file names")
      (let ((ret (mapcar
          (lambda (tfile)
            (let (old-get-info)
              (cl-letf* (((symbol-function 'old-get-info) (symbol-function 'org-babel-get-src-block-info))
                 ((symbol-function 'org-babel-get-src-block-info)
                  `(lambda (&rest get-info-args)
                     (let* ((info (apply 'old-get-info get-info-args))
                        (params (nth 2 info))
                        (tfile-cons (assoc :tangle params)))
                       (setcdr tfile-cons ,tfile)
                       info))))
            (funcall oldfun block-counter only-this-block))))
          tfiles)))
    (if only-this-block
        (list (cons (cl-caaar ret) (mapcar #'cadar ret)))
      ret)))))

(advice-add 'org-babel-tangle-collect-blocks :override #'org-babel-tangle-collect-blocks-handle-tangle-list)
(advice-add 'org-babel-tangle-single-block :around #'org-babel-tangle-single-block-handle-tangle-list)

The code is tested with `org-version` 9.1.6, `emacs-version` 25.1.50.2, and the following org-file. The test case is also an example for setting up a list of tangle files for one source block.

#+BEGIN_SRC emacs-lisp :tangle '("/tmp/first.el" "/tmp/third.el")
(message "Some code to be tangled into first and third")
#+END_SRC

#+BEGIN_SRC emacs-lisp :tangle "/tmp/first.el"
(message "I am the first file.")
#+END_SRC

#+BEGIN_SRC emacs-lisp :tangle '("/tmp/first.el" "/tmp/second.el")
(message "I am the first or the second file.")
#+END_SRC

#+BEGIN_SRC emacs-lisp :tangle "/tmp/third.el"
(message "I am the third file.")
#+END_SRC
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • 2
    Definitely, a +1. This snippet worked well and simplified my org code significantly by avoiding no-web hacks. It is also a nice exercise in emacs-lisp and org-mode hacking. Nice work! – Julien Chastang Apr 17 '18 at 00:01
  • 1
    To allow you to tangle using variables in-place of strings for filenames, delete the `'light` argument in the first definition. This is great; thanks! – Musa Al-hassy Mar 05 '19 at 10:54
  • Hi Tobias, on emacs 27 with Org-mode 9.3 this breaks standard evaluation of conditional tangling, such as `:tangle (unless foo "bar")' – Wilder Oct 22 '20 at 02:38
  • @MusaAl-hassy would that be something line `(list file1 file1)`? When I use an expression `'(file1 file2)` it evaluates to `(file1 file2)`. The variables are not expanded. – vfclists Dec 13 '20 at 07:29
  • Try `(myfile1 myfile2)`, no quote; that worked for me I think. – Musa Al-hassy Dec 13 '20 at 12:07
  • @Tobias I've come to depend on this code over the last few years and it seems to be broken in org-mode 9.6. Is there a chance you could kindly update it, please? – vfclists Oct 18 '22 at 08:53
  • @vfclists There is no version 9.6 on https://git.savannah.gnu.org/cgit/emacs/org-mode.git/ Do you mean 9.5.5 or the `main` branch? – Tobias Oct 18 '22 at 16:25
  • @vfclists The [`ORG-NEWS`](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/etc/ORG-NEWS) contains: `* Version 9.6 (not yet released)`. Maybe, you really mean the `main`-branch. Can the problem also be reproduced with Org 9.5.5? That would simplify things for me. – Tobias Oct 18 '22 at 16:35
  • @Tobias Thanks for your response. You are right the version I'm using is 9.5.4 on Emacs 27.1. I got the idea of 9.6 on Doom Emacs which is compiled with the latest Org mode. What happens is the first file in the list gets added to itself in accord with the number of files in the list and the rest don't get saved at all. `org-version` produces - (org-version t t)Org mode version 9.5.4 (9.5.4-ge0b05b @ /home/vfclists/.emacs.d/elpa/org-9.5.5/) – vfclists Oct 18 '22 at 20:18
  • Looking at the latest commit tags it seems I'm still on 9.5.4. I will install it from the repo and get back to you. – vfclists Oct 18 '22 at 20:28
  • @Tobias I have set the org version correctly and it is now 9.5.5 – vfclists Nov 09 '22 at 11:42