7

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

  • 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 1:40
10

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
  • 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 0: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 5 '19 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.