Genesis 11:7 “…Come, let us go down and confuse their language so they will not understand each other.”
If supporting many languages in Babel was not confounding enough, lets support arbitrary interpreters too!

The need to specify a custom interpreter arose when I needed to provide my own interpreter for generating an ASCII graph from a dot script.

Objective

Specify an :interpreter and/or :filter command to override the execute behaviour.

An interpreter should take a path to the code as its first parameter, where a filter should take the code as stdin.

Filter may be used to clean up the output of the interpreter, for example.

This is what we want

show-dot is a script which creates an ASCII graph from dot code.

The default behaviour of ob-dot is to create a pdf file.

It only lets you specify arguments to the dot command, but does not let you switch the dot command for something else.

1
2
3
4
5
#+BEGIN_SRC graphviz-dot -n :interpreter show-dot :results verbatim
  digraph Q {
  node1 -> node2
  }
#+END_SRC
1
2
3
4
5
6
7
8
9
+-------+
| node1 |
+-------+
  |
  |
  v
+-------+
| node2 |
+-------+

Existing features can take us close

The problem with this is that you are forced to use sh-mode highlighting and you don’t have access to yasnippets that way either.

Another frustration is that the interpreter (e.g. show-dot) must be shebang compatible (i.e. its first argument must be the path of a file containing the code).

1
2
3
4
5
#+BEGIN_SRC sh -n :shebang "#!/usr/bin/env show-dot" :results verbatim
  digraph Q {
  node1 -> node2
  }
#+END_SRC
1
2
3
4
5
6
7
8
9
+-------+
| node1 |
+-------+
  |
  |
  v
+-------+
| node2 |
+-------+

Solution: Extend babel

Create the org-babel-execute:generic function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defun org-babel-execute:generic (body params)
  ":interpreter executes body like 'interpreter file'
:interpreter filters the source like 'cat source | filter'"
  (let ((interpreter (or (cdr (assoc :interpreter params))
                         (cdr (assoc :i params))))
        (filter (or (cdr (assoc :filter params))
                    (cdr (assoc :f params))))
        (tmp (org-babel-temp-file "generic-")))
    (with-temp-file tmp (insert body))
    (if (not (and (boundp 'interpreter) interpreter))
        (setq interpreter "cat"))
    (if (and (boundp 'filter) filter)
        (setq filter (concat "| " filter)))
    (shell-command-to-string (format "%s %s %s" interpreter tmp filter))))

Extend the org-babel-execute-src-block function

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
;; New code:
;; --------
;; (interpreter (cdr (assq :interpreter params)))
;; (filter (cdr (assq :filter params)))
;; (if (or interpreter filter)
;;     (setq cmd 'org-babel-execute:generic))

(defun org-babel-execute-src-block (&optional arg info params)
  "Execute the current source code block.
Insert the results of execution into the buffer.  Source code
execution and the collection and formatting of results can be
controlled through a variety of header arguments.

With prefix argument ARG, force re-execution even if an existing
result cached in the buffer would otherwise have been returned.

Optionally supply a value for INFO in the form returned by
`org-babel-get-src-block-info'.

Optionally supply a value for PARAMS which will be merged with
the header arguments specified at the front of the source code
block."
  (interactive)
  (let* ((org-babel-current-src-block-location
          (or org-babel-current-src-block-location
              (nth 5 info)
              (org-babel-where-is-src-block-head)))
         (info (if info (copy-tree info) (org-babel-get-src-block-info))))
    ;; Merge PARAMS with INFO before considering source block
    ;; evaluation since both could disagree.
    (cl-callf org-babel-merge-params (nth 2 info) params)
    (when (org-babel-check-evaluate info)
      (cl-callf org-babel-process-params (nth 2 info))
      (let* ((params (nth 2 info))
             (interpreter (or (cdr (assq :interpreter params))
                              (cdr (assq :i params))))
             (filter (or (cdr (assq :filter params))
                         (cdr (assq :f params))))
             (cache (let ((c (cdr (assq :cache params))))
                      (and (not arg) c (string= "yes" c))))
             (new-hash (and cache (org-babel-sha1-hash info)))
             (old-hash (and cache (org-babel-current-result-hash)))
             (current-cache (and new-hash (equal new-hash old-hash))))
        (cond
         (current-cache
          (save-excursion               ;Return cached result.
            (goto-char (org-babel-where-is-src-block-result nil info))
            (forward-line)
            (skip-chars-forward " \t")
            (let ((result (org-babel-read-result)))
              (message (replace-regexp-in-string "%" "%%" (format "%S" result)))
              result)))
         ((org-babel-confirm-evaluate info)
          (let* ((lang (nth 0 info))
                 (result-params (cdr (assq :result-params params)))
                 ;; Expand noweb references in BODY and remove any
                 ;; coderef.
                 (body
                  (let ((coderef (nth 6 info))
                        (expand
                         (if (org-babel-noweb-p params :eval)
                             (org-babel-expand-noweb-references info)
                           (nth 1 info))))
                    (if (not coderef) expand
                      (replace-regexp-in-string
                       (org-src-coderef-regexp coderef) "" expand nil nil 1))))
                 (dir (cdr (assq :dir params)))
                 (default-directory
                   (or (and dir (file-name-as-directory (expand-file-name dir)))
                       default-directory))
                 (cmd (intern (concat "org-babel-execute:" lang)))
                 result)
            (if (or interpreter filter)
                (setq cmd 'org-babel-execute:generic))
            (unless (fboundp cmd)
              (error "No org-babel-execute function for %s!" lang))
            (message "executing %s code block%s..."
                     (capitalize lang)
                     (let ((name (nth 4 info)))
                       (if name (format " (%s)" name) "")))
            (if (member "none" result-params)
                (progn (funcall cmd body params)
                       (message "result silenced"))
              (setq result
                    (let ((r (funcall cmd body params)))
                      (if (and (eq (cdr (assq :result-type params)) 'value)
                               (or (member "vector" result-params)
                                   (member "table" result-params))
                               (not (listp r)))
                          (list (list r))
                        r)))
              (let ((file (cdr (assq :file params))))
                ;; If non-empty result and :file then write to :file.
                (when file
                  (when result
                    (with-temp-file file
                      (insert (org-babel-format-result
                               result (cdr (assq :sep params))))))
                  (setq result file))
                ;; Possibly perform post process provided its
                ;; appropriate.  Dynamically bind "*this*" to the
                ;; actual results of the block.
                (let ((post (cdr (assq :post params))))
                  (when post
                    (let ((*this* (if (not file) result
                                    (org-babel-result-to-file
                                     file
                                     (let ((desc (assq :file-desc params)))
                                       (and desc (or (cdr desc) result)))))))
                      (setq result (org-babel-ref-resolve post))
                      (when file
                        (setq result-params (remove "file" result-params))))))
                (org-babel-insert-result
                 result result-params info new-hash lang)))
            (run-hooks 'org-babel-after-execute-hook)
            result)))))))

Results!

Check out this article for some sweet graphviz graphs in ASCII, using this method.

https://mullikine.github.io/posts/review-of-the-illustrated-transformer/

Example 1

1
2
3
4
5
6
7
8
9
#+BEGIN_SRC graphviz-dot -n :interpreter "sed /delete/d" :filter "show-dot | sed s/node2/__2__/ | dot-pretty" :results verbatim
  digraph Q {
  node1 -> node2
  node2 -> node3
  node3 -> node4

  node1 -> deletethis
  }
#+END_SRC
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
※―――――――※
| node1 |
※―――――――※
  |
  |
  v
※―――――――※
| __2__ |
※―――――――※
  |
  |
  v
※―――――――※
| node3 |
※―――――――※
  |
  |
  v
※―――――――※
| node4 |
※―――――――※

dot-pretty

1
2
3
4
5
6
7
8
9
#!/bin/bash
export TTY

sed -e 's/|/┃/g' \
    -e 's/-/━/g' \
    -e 's/+/∘/g' \
    -e "s/'/┆/g" \
    -e 's/ ━/┄┄/g' \
    -e 's/[┄━] /┄┄/g'

Example 2

1
2
3
#+BEGIN_SRC graphviz-dot -n :filter dot-digraph :async :results verbatim
  node1 -> node2
#+END_SRC
1
2
3
4
5
6
7
8
9
+-------+
| node1 |
+-------+
  |
  |
  v
+-------+
| node2 |
+-------+

dot-digraph

1
2
3
4
5
6
7
#!/bin/bash

{
    echo "digraph Q {"
    cat
    echo "}"
} | show-dot "$@"

Example 3

1
2
3
4
5
6
7
8
#+BEGIN_SRC graphviz-dot -n :filter show-dot :async :results verbatim
  digraph Q {
      node1 -> cluster_R
      subgraph cluster_R {
          nodeA -> nodeB
      }
  }
#+END_SRC
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
+-------+       +-----------+
| node1 | -->   | cluster_R |
+-------+       +-----------+
              + - - - - - - - +
              '   cluster_R   '
              '               '
              ' +-----------+ '
              ' |   nodeA   | '
              ' +-----------+ '
              '   |           '
              '   |           '
              '   v           '
              ' +-----------+ '
              ' |   nodeB   | '
              ' +-----------+ '
              '               '
              + - - - - - - - +

Example

This should open vim in a tmux split pane with hi as input.

1
2
3
#+BEGIN_SRC sh -n :i "spv bash" :async :results none
  echo hi | vim -
#+END_SRC

Org babel hydra

This is an entry for my hydra to expand a babel template for a vertical split.

1
("V" (hot-expand "<s" "spv") "sh")

Babel template script

org-template-gen is an external script I use which contains templates for babel.

With this babel block in my blog post org file I can open the relevant section in vim in a tmux vertical split by pressing C-c C-c.

1
2
3
#+BEGIN_SRC sh -n :interpreter "spv bash" :async :results none
  vim +/"spv" "$HOME/scripts/org-template-gen"
#+END_SRC

This is the relevant section that expands the template for a shell babel that executes with a tmux vertical split.

1
2
3
4
5
6
spv) {
    echo "#+BEGIN_SRC sh -n :interpreter \"spv bash\" :async :results none"
    cat "$input_fp" | sed 's/^\(\s*\)#+/,\1#+/' | indent 2 | awk 1
    echo -n "#+END_SRC"
}
;;

Demonstration

asciinema recording

Another example – latex

We want to write pseudocode and have it displayed as png.

We use a filter script called texalg2png which wraps the procedure code inside a template and send it to tex2png which creates the png.

Specifying raw in the options allows the png to be immediately embedded in the blog.

1
2
3
4
5
6
7
8
9
#+BEGIN_SRC latex -n :f texalg2png self-attention :async :results raw drawer
\Procedure{Score}{$i,j$}
\For{$\textrm{each word }i$}
\For{$\textrm{each word }j$}
\State $\mathit{score}_\mathit{i,j} = q_i \cdot k_j$
\EndFor
\EndFor
\EndProcedure
#+END_SRC

texalg2png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
export TTY

read -r -d '' texcode <<HEREDOC
\documentclass{standalone}
\usepackage{varwidth}
\usepackage{algorithm} %ctan.org\pkg\algorithms
\usepackage{algpseudocode}
\begin{document}
\begin{varwidth}{\linewidth}
\par\noindent
\begin{algorithmic}[1]
$(cat)
\end{algorithmic}
\end{varwidth}
\end{document}
HEREDOC

printf -- "%s" "$texcode" | tex2png "$@"

tex2png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/bash
export TTY

tf_tex="$(ux tf tex || echo /dev/null)"
trap "rm \"$tf_tex\" 2>/dev/null" 0

fp="$tf_tex"

name="$1"

if test "$name" = nil; then
    name=""
fi

fn=$(basename "$fp")
dn=$(dirname "$fp")
ext="${fn##*.}"
mant="${fn%.*}"

orig_dir="$(pwd)"
cd "$dn"

out=$(pdflatex "$fn"; pdf2svg "${mant}.pdf" "${mant}.svg" 2>&1)

genf="$dn/${mant}.svg"
cd "$orig_dir"

if ! test -f "$genf"; then
    echo "$out" 1>&2
    exit 1
fi

: ${name:="$mant"}
# echo "[[file:${name}.svg][$name]]"

nf="${name}.svg"
cp -a "$genf" "$orig_dir/$nf"

echo "[[file:$nf]]"