Hi Ihor, thanks again for you input. I've reworked the proposal once more.
Ihor Radchenko <[email protected]> writes: >> Would it still compose as a macro? E.g., could I call one block >> assigning its variables using the result of two others? > > Yes. Good to know, yet I'd prefer to keep it a function and variable names conventional string arguments which might be crafted by some expression as well. >> (org-babel-call "foo" positional1 positional2 :dir "/home/daniel") > > What if you need to pass :dir or other keyword as value? You're right, thanks for pointing that out. I think, the practical way is to provide such a variable explicitly with `:var` in such a case, instead of passing it positionally. This new version handles a keyword value cleanly and addresses your other comments. >> (org-babel-call "bar" :var "var1" lisp-variable) ;; (1) > > This may work. > >> (org-babel-call "xyz" :var "var2=ref[3]") ;; (2) > > Looks like defeating the purpose of pluggable ELisp. I simply removed variant (2) entirely. A `:var` keyword now always takes exactly two arguments, a NAME string and the VALUE, of which the VALUE is used as-is, so a keyword is no longer a problem there either: (org-babel-call "foo" :var "a" :some-keyword) If someone wants to define a variable using a resolved reference, e.g. to read from a table, they can still use `org-babel-ref-resolve` explicitly (positionally or named). I mentioned that in the docstring. (org-babel-call "foo" :var "a" (org-babel-ref-resolve "tbl[1,1]")) Attached find v4 of the patch. This patch revision also includes some tests, which the ealier versions did not. Regarding your comments on the complementary function `org-babel-result`: > Why not just :var hello=hello[:cache yes]() + > (setq org-babel-update-intermediate 'cache)? I checked this and I don't think it's a substitute for my proposed function. The idea of `org-babel-result` is to have something that obviously and reliably never modifies or re-executes the producing block (regardless of any settings). Let's say, the referenced block took an hour to run (I happen to have such queries) or even is a one-shot thing whose result isn't reproducible at all. Now with the result already in the buffer, I want to consume the result in another block. With the proposed `org-babel-result` I can do that in an elisp body or in a variable assignment in the header. I'm looking forward to turn in `org-babel-result` once we have settled on the syntax for `org-babel-call`. Kind regards, Daniel
>From 9aeb42a86e949e17a0be4663b75474bef7230677 Mon Sep 17 00:00:00 2001 From: Daniel Bausch <[email protected]> Date: Mon, 6 Jul 2026 14:53:38 +0200 Subject: [PATCH] ob-core: Add org-babel-call * lisp/ob-core.el (org-babel-call): New function. Call a named source block from Lisp following the argument model of a "#+call:" line: positional arguments bind the block's declared variables in Babel's declaration order as real Lisp objects; the `:var' keyword assigns one variable by name and always takes exactly two arguments, NAME and VALUE, with VALUE bound exactly as given, of any type -- to use the reference syntax instead (table indexing, a named block call), call `org-babel-ref-resolve' and pass its result as VALUE; any other keyword is a header argument merged into the block's execution, each taking exactly one Lisp value. Built on `org-babel-execute-src-block' with cons-valued `:var' parameters so values are forwarded as objects rather than serialized into a string, and located with `org-babel-find-named-block' to avoid the buffer side effects of `org-babel-goto-named-src-block'. During export the block is resolved in `org-babel-exp-reference-buffer', like Babel reference resolution, so calls executed at export time find blocks that are themselves not exported. Argument parsing errors clearly if a non-keyword is found where a keyword is expected (most likely a missing `:var' value) instead of silently misparsing the rest of the argument list. * etc/ORG-NEWS (New functions and changes in function arguments): Announce it. * testing/lisp/test-ob.el (test-ob-core/org-babel-call-positional) (test-ob-core/org-babel-call-var-by-value) (test-ob-core/org-babel-call-var-keyword-value) (test-ob-core/org-babel-call-header-arg) (test-ob-core/org-babel-call-results-locked) (test-ob-core/org-babel-call-too-many-positional) (test-ob-core/org-babel-call-missing-block) (test-ob-core/org-babel-call-var-requires-name-and-value) (test-ob-core/org-babel-call-var-name-must-be-string) (test-ob-core/org-babel-call-reference-resolution) (test-ob-core/org-babel-call-composes): New tests. --- etc/ORG-NEWS | 22 ++++++++++ lisp/ob-core.el | 71 ++++++++++++++++++++++++++++++ testing/lisp/test-ob.el | 95 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 9263bfeb0..2f687e1da 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -242,6 +242,28 @@ information. Given the completed and total number of tasks, format the percent cookie =[N%]=. +*** New function ~org-babel-call~ + +Execute a named source block and return its result with its type +intact: + +#+begin_src emacs-lisp +(org-babel-call "summary" 2026 :var "rows" data :dir "/srv") +#+end_src + +Arguments before the first keyword bind the block's declared +variables positionally, in declaration order. The keyword ~:var~ +binds one variable by name and always takes exactly two arguments, +=:var "name" VALUE=; NAME is a string, so it can be computed, and +VALUE is any Lisp object, bound as-is. Any other keyword is a +header argument merged into the block's execution, e.g. ~:dir~ or +~:cmdline~ — each takes exactly one Lisp value, so a multi-word +header-line value such as ~:cmdline~'s is one Lisp string +(~:cmdline "-O2 -Wall"~). Unlike a =#+call:= line, there is no +reference syntax: for table indexing or a block call, use +~org-babel-ref-resolve~ and pass its result as VALUE. ~:results~ +cannot be overridden. + ** Removed or renamed functions and variables *** ~org-babel-remote-temporary-directory~ is now obsolete diff --git a/lisp/ob-core.el b/lisp/ob-core.el index 7f283012c..374269325 100644 --- a/lisp/ob-core.el +++ b/lisp/ob-core.el @@ -982,6 +982,77 @@ guess will be made." (run-hooks 'org-babel-after-execute-hook) result))))))) +;;;###autoload +(defun org-babel-call (name &rest args) + "Execute the source block named NAME, binding its variables to the +values given, and return the result with its type intact. + +Arguments before the first keyword bind the block's declared +variables positionally, in declaration order. The keyword `:var' +binds one variable by name and always takes two arguments, NAME +and VALUE. NAME is a string, an ordinary expression rather than a +literal, so it can be computed; VALUE is any Lisp object, bound +as-is. Any other keyword is a header argument merged into the +block's execution, e.g. `:dir' or `:cmdline'. Each keyword takes +exactly one Lisp value, so a value that looks like several words +in header-line syntax, such as `:cmdline' \"-O2 -Wall\", is a +single Lisp string here, not two arguments. + +Unlike a \"#+call:\" line, there is no reference syntax: for table +indexing or a block call, use `org-babel-ref-resolve' and pass its +result as VALUE. `:results' cannot be overridden. + +\(fn NAME [POSITIONAL...] [:var NAME VALUE]... [KEYWORD VALUE]...)" + ;; Resolve against the pristine buffer during export, like other + ;; Babel references, so a call finds blocks not themselves exported. + (with-current-buffer (or org-babel-exp-reference-buffer (current-buffer)) + (let* ((location (or (org-babel-find-named-block name) + (error "No source block named `%s'" name))) + (info (org-with-point-at location + (org-babel-get-src-block-info 'no-eval))) + (declared (org-babel--get-vars (nth 2 info))) + (params nil)) + ;; Positional arguments bind declared variables in order. + (while (and args (not (keywordp (car args)))) + (let ((decl (pop declared))) + (unless decl + (error "Too many positional arguments calling block `%s'" name)) + (push (cons :var + (cons (cond ((consp decl) (car decl)) + ((and (stringp decl) + (string-match + "^\\([^= \f\t\n\r\v]+\\)[ \t]*=" decl)) + (intern (match-string 1 decl))) + (t (error "Cannot bind variable `%S'" decl))) + (pop args))) + params))) + ;; Keyword arguments: `:var' bindings and header-argument overrides. + (while args + (let ((key (pop args))) + (unless (keywordp key) + (error "Expected a keyword, got `%S' calling block `%s' -- missing a `:var' value?" + key name)) + (pcase key + (:var + (unless (cdr args) + (error "`:var' requires a name and a value calling block `%s'" name)) + (let ((var-name (pop args)) + (value (pop args))) + (unless (stringp var-name) + (error "`:var' name must be a string, not `%S', calling block `%s'" + var-name name)) + (push (cons :var (cons (intern var-name) value)) params))) + (:results + (error "`:results' cannot be set as a header argument")) + (_ + (unless args + (error "Missing value for `%s' calling block `%s'" key name)) + (push (cons key (pop args)) params))))) + ;; Forced silent so nothing is inserted and the result composes + ;; as an ordinary Lisp value. + (org-babel-execute-src-block + nil info (append (nreverse params) '((:results . "silent"))))))) + (defun org-babel-expand-body:generic (body params &optional var-lines) "Expand BODY with PARAMS. Expand a block of code with org-babel according to its header diff --git a/testing/lisp/test-ob.el b/testing/lisp/test-ob.el index 3644a511b..7572ca57c 100644 --- a/testing/lisp/test-ob.el +++ b/testing/lisp/test-ob.el @@ -2912,6 +2912,101 @@ A (should (string= (org-element-property :value eap) (concat region "\n")))))))) +(ert-deftest test-ob-core/org-babel-call-positional () + "Positional arguments bind declared variables in order, as real +Lisp objects." + (should + (equal 30 + (org-test-with-temp-text + "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n" + (org-babel-call "add" 10 20))))) + +(ert-deftest test-ob-core/org-babel-call-var-by-value () + "`:var NAME VALUE' binds VALUE exactly as given." + (should + (equal 30 + (org-test-with-temp-text + "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n" + (org-babel-call "add" :var "a" 10 :var "b" 20))))) + +(ert-deftest test-ob-core/org-babel-call-var-keyword-value () + "A `:var' VALUE that is itself a keyword is bound literally, not +mistaken for the start of the next keyword argument." + (should + (eq :foo + (org-test-with-temp-text + "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n" + (org-babel-call "echo" :var "a" :foo))))) + +(ert-deftest test-ob-core/org-babel-call-header-arg () + "Any keyword other than `:var' is a header argument merged into +the block's execution." + (should + (equal "/tmp/" + (org-test-with-temp-text + "#+name: showdir\n#+begin_src emacs-lisp\ndefault-directory\n#+end_src\n" + (org-babel-call "showdir" :dir "/tmp/"))))) + +(ert-deftest test-ob-core/org-babel-call-results-locked () + "`:results' cannot be overridden." + (should-error + (org-test-with-temp-text + "#+name: foo\n#+begin_src emacs-lisp\n1\n#+end_src\n" + (org-babel-call "foo" :results "value")))) + +(ert-deftest test-ob-core/org-babel-call-too-many-positional () + "Supplying more positional arguments than declared variables is +an error." + (should-error + (org-test-with-temp-text + "#+name: one-var\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n" + (org-babel-call "one-var" 10 20)))) + +(ert-deftest test-ob-core/org-babel-call-missing-block () + "Calling a name with no matching source block is an error." + (should-error + (org-test-with-temp-text "text, no blocks\n" + (org-babel-call "nope")))) + +(ert-deftest test-ob-core/org-babel-call-var-requires-name-and-value () + "`:var' with a missing value is a clear error, not a silent +misparse of whatever keyword follows." + (should-error + (org-test-with-temp-text + "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n" + (org-babel-call "echo" :var "a" :var "b" 1)))) + +(ert-deftest test-ob-core/org-babel-call-var-name-must-be-string () + "`:var' NAME must be a string." + (should-error + (org-test-with-temp-text + "#+name: echo\n#+begin_src emacs-lisp :var a=1\na\n#+end_src\n" + (org-babel-call "echo" :var 'a 1)))) + +(ert-deftest test-ob-core/org-babel-call-reference-resolution () + "Org's reference syntax (table indexing, etc.) is available by +calling `org-babel-ref-resolve' directly and passing its result as +VALUE -- `org-babel-call' has no dedicated syntax of its own for +this." + (should + (equal 2 + (org-test-with-temp-text + (concat "#+name: tbl\n#+begin_src emacs-lisp\n'((1 2) (3 4))\n#+end_src\n\n" + "#+name: showcell\n#+begin_src emacs-lisp :var cell=1\ncell\n#+end_src\n") + (org-babel-call "showcell" :var "cell" (org-babel-ref-resolve "tbl[0,1]")))))) + +(ert-deftest test-ob-core/org-babel-call-composes () + "Positional and `:var' values are ordinary Lisp expressions, +evaluated the normal way -- variables and nested `org-babel-call' +results compose with no special quoting convention." + (should + (equal 107 + (org-test-with-temp-text + (concat "#+name: seven\n#+begin_src emacs-lisp\n7\n#+end_src\n\n" + "#+name: add\n#+begin_src emacs-lisp :var a=1 :var b=2\n(+ a b)\n#+end_src\n") + (let ((my-var 100)) + (org-babel-call "add" my-var (org-babel-call "seven"))))))) + (provide 'test-ob) ;;; test-ob.el ends here -- 2.55.0
