2

How to evaluate the expressions or functions in a code-block before tangling a source code block?

  • I generate my init.el file using org-babel-tangle
    AND
  • I'm using chemacs2 - which allows using multiple configurations parallely. Chemacs2 uses a special file called .emacs-profiles.el where we store location and environment properties of each of the configurations as mentioned below.

But the .emacs-profiles.el file cannot have any functions since it doesn't understand how to process them as mentioned here & here.

So, I'm looking for a way to evaluate the functions and replace the code content with the result. According to the documentation, we can use org-babel-pre-tangle-hook for this, but I'm very new to elisp and I'm not sure how to write the hook.

Please note that I have a working configuration with chemacs that handles multiple configurations like spacemacs, doom emacs... But the .emacs-profiles.el config is specific to my current windows PC - since it mentions C:\\Users\\<username> in the file path. So I cannot directly use the same .emcas-profiles.el config in another system as is without updating the <username>. That's the reason I'm looking for this approach.


Below is the source code block that I want to be evaluated:

( ("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . ( ("HOME" . (concat (getenv "APPDATA") "\\AG-Emacs"))) )))) ) 

I want this to be evaluated and tangled like this:

( ("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . ( ("HOME" . "C:\\Users\\<username>\\AppData\\Roaming\\AG-Emacs") )))) ) 

Useful/Similar resources I found:

4
  • That's just an expression: it's as if you have 2 + 3 standing on its own; you can replace it by 5 but what good does that do? It will have no effect on anything. Is this the complete code block? Commented Jun 20, 2024 at 19:19
  • Yes, that's basically it, I added on item in the list, but there can be more items based on user's preferences. This is the documentation from chemacs for reference. Commented Jun 20, 2024 at 19:22
  • I think you are biting off more than you can chew at this point. Why worry about three different things (configuring Emacs, tangling the init file and multiplexing between different versions) all at the same time? Divide and conquer! Don't worry about tangling the init file: that's the last thing you should do. Get plain Emacs working first: only then get Doom working. At that point, worry about Chemacs - and so on. Give enough time and effort to each stage to understand what's going on, figure out how to debug problems and how to fix them... Commented Jun 20, 2024 at 21:40
  • I have everything working at my end with chemacs perfectly, but the .emacs-profiles.el config is specific to my current windows PC - since it mentions C:\\Users\\<username> in the file path. So I cannot directly use the same .emcas-profiles.el config in another system as is. That's the reason I'm looking for this approach.. I'll add this point in the question Commented Jun 20, 2024 at 22:01

2 Answers 2

2

Create a template file, template.el, with these contents (this is the backquoted form of what you show above):

`(("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . (("HOME" . ,(concat (getenv "APPDATA") "\\AG-Emacs"))))))) 

You can add org-babel-execute-buffer to org-babel-pre-tangle-hook to make sure that the pre-processing is done before the tangling. The backquote quotes the form just like ', except that it allows partial evaluation using the , mechanism (see Backquote in the Emacs Lisp Ref manual).

Assuming you have an environmental definition for APPDATA, if you cut-and-paste this form into your scratch buffer and evaluate it with C-j at the end of form, you will get your second form above (it may look different but it is actually the same list - the Lisp printer prefers to write list structure instead of dotted-pair structure whenever possible):

(("default" (user-emacs-directory . "~/emacs-configs/my-config") (env ("HOME" . "C:\\Users\\<username>\\AppData\\Roaming\\AG-Emacs")))) 

You can do the pre-processing manually as above, or by using a script:

emacs --batch --load foo.el\ --eval '(ndk/pre-process "template.el")' 

where foo.el looks like this:

;(setenv "APPDATA" "C:\\Users\\<username>\\AppData\\Roaming") (defun ndk/pre-process (file) (with-current-buffer (find-file-noselect file) (print (eval (read (current-buffer)))))) 

I tested with the setenv uncommented, since I did not have an environmental definition for APPDATA, but presumably you do, so you can leave the setenv commented out.

I didn't bother to write the output to a file in the script, since I can use redirection:

emacs --batch --load foo.el \ --eval '(ndk/pre-process "template.el")' > .emacs-profiles.el 

But I don't know how to do this by using org-babel-tangle (and actually I don't think it's possible, at least not straightforwardly, but that may be a failure of imagination on my part).


EDIT:The following is close but fails because the elisp implementation of Org Babel has the bad habit of leaving out double quotes around strings:

* Code First, evaluate this code block to produce the expanded template in another code block - then `C-c C-v C-t' to tangle into `output.el': #+name: template #+begin_src elisp :results drawer :wrap src elisp `(("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . (("HOME" . ,(concat (getenv "APPDATA") "\\AG-Emacs"))))))) #+end_src * Results :PROPERTIES: :header-args:elisp: :tangle output.el :END: #+RESULTS: template #+begin_src elisp ((default (user-emacs-directory . ~/emacs-configs/my-config) (env (HOME . C:\Users\<username>\AppData\Roaming\AG-Emacs)))) #+end_src 

The output is:

((default (user-emacs-directory . ~/emacs-configs/my-config) (env (HOME . C:\Users\<username>\AppData\Roaming\AG-Emacs)))) 

Close, but no cigar...


EDIT 2: OK, this is ugly but it works:

#+name: template #+begin_src elisp :results output :wrap src elisp (print `(("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . (("HOME" . ,(concat (getenv "APPDATA") "\\AG-Emacs")))))))) #+end_src 

EDIT 3: Here's a function that you can add to org-babel-pre-tangle-hook. That way, the pre-processing will happen before tangling:

#+begin_src elisp (defun ndk/pre-tangle () "Execute the \"template\" src block before tangling." (save-excursion (org-babel-goto-named-src-block "template") (org-babel-execute-src-block))) #+end_src 

It executes just the template block which fills out the result with the pre-processed code block in the Results section. The property in the section then tangles the result src block to the output.

7
  • See the third edit for the automation. For WIndows, somebody else will have to help you. Commented Jun 21, 2024 at 15:46
  • Oh, what a tangled web we weave... The automation does not work because the produced source block is also evaluated. It needs a more surgical evaluation process. Commented Jun 21, 2024 at 16:09
  • the result of the code in Edit & Edit 2 seems to be missing the periods (.) after "default" and env. But its printing the . after "HOME" - not sure why. Are you getting the same result? Can we retain that? Commented Jun 21, 2024 at 16:33
  • The result is semantically identical: see the parenthetical comment at the end of my third paragraph. Commented Jun 21, 2024 at 20:05
  • okay, that's helpful. I was able to get it to work with these inputs using the :noweb header argument. Thank you so much for your help Really appreciate. Commented Jun 21, 2024 at 20:26
2

With the help of @NickD, I was able to figure this out.

org-babel-tangle allows you to extract source code from other source code blocks. During this tangling process, Org expands variables in the source code, and resolves any noweb style references (see Noweb Reference Syntax). Below is the code I used to achieve this:

Solution

First, add a source code block which

  • has the un-evaluated code with a name to the code block (#+name: dynamic-chemacs-profiles in this case) and
  • we also add :tangle no header argument so that this code block doesn't get tangled.
#+name: dynamic-chemacs-profiles #+begin_src emacs-lisp :results output :tangle no (print `( ("default" . ((user-emacs-directory . "~/emacs-configs/my-config") (env . (("HOME" . ,(concat (getenv "APPDATA") "\\AG-Emacs")))))) )) #+end_src 

Then add another code block which will

  • copy the results of the code block with name dynamic-chemacs-profiles by adding the code <<dynamic-chemacs-profiles()>> inside the code block using :noweb yes.
  • Optionally, you can also add more lines in this code block if necessary.

When we run the command org-babel-tangle, this code block will be tangled which will add the results of the above code block

#+begin_src emacs-lisp :noweb yes :tangle test-files/tangleFolder/testProfiles.el ;; You can add more code here <<dynamic-chemacs-profiles()>> #+end_src 

This is the code that gets generated from the above code blocks

(("default" (user-emacs-directory . "~/emacs-configs/my-config") (env ("HOME" . "C:\\Users\\gangu\\AppData\\Roaming\\AG-Emacs")))) 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.