如何控制 org babel 执行结果输出的长度

通过 org babel 执行 src block 里的代码,有时候 results 打印很长, 比如不小心在 shell src block 里执行了 cat file.txt, 而 file 是一个几千上万行的文本,或者 python 里 for 循环打印了太多,导致整个 emacs buffer 刷满甚至卡死。请问是否有什么方法能够指定输出的 results 的行数和策略,例如如果超出了行数限制就显示一个省略号,或者不断覆盖之前的输出,而始终保持固定的行数?

也可以是限制输出的字符数,因为有时候打印出来就是一个超长行,导致 emacs 卡死。

或者是否有别的处理方法,不限制输出长度,但当 buffer 行数很多或者有超长行的时候不卡,我的配置里开了 global-so-long-mode, 但 babel 打印出一个超长行, (这一行保存下来估计好几 MB)的时候整个 emacs 也卡住,重启后也打不开这个文档,但用 vim 却秒开。 当然关于超长行问题在其他贴有讨论,不是本帖重点,这里还是关注能否控制 babel 输出量

Org mode输出不清楚,但你可以控制代码输出长度吧。。

是,但有时候你不知道会输出多少,比如代码执行中动态生成的数据,或者想预览某个文件,但这个文件如果就是一个超长行,即便打印第一行也会卡掉,好几次因为这个导致要重启 emacs

(defvar org-babel-result-lines-limit 10)
(defvar org-babel-result-length-limit 100)

(defun org-babel-insert-result@limit (orig-fn result &rest args)
  (if (and result (or org-babel-result-lines-limit org-babel-result-length-limit))
      (let (new-result plines plenght limit)
        (with-temp-buffer
          (insert result)
          (setq plines (if org-babel-result-lines-limit
                           (goto-line org-babel-result-lines-limit)
                         (point-max)))
          (setq plenght (if org-babel-result-length-limit
                            (min org-babel-result-length-limit (point-max))
                          (point-max)))
          (setq limit (min plines plenght))
          (setq new-result (concat (buffer-substring (point-min) limit)
                                   (if (< limit (point-max)) "..."))))
        (apply orig-fn new-result args))
    (apply orig-fn result args)))

(advice-add 'org-babel-insert-result :around #'org-babel-insert-result@limit)
#+begin_src emacs-lisp :results output
(dotimes (i 100)
  (print i))
#+end_src

#+RESULTS:
#+begin_example

0

1

2

3

...
#+end_example

UPDATE: 2021-10-09T08:41:22+08:00

判断 result 非空:

-  (if (or org-babel-result-lines-limit org-babel-result-length-limit)
+  (if (and result (or org-babel-result-lines-limit org-babel-result-length-limit))

result 为空时不应使用 new-result:

-   (apply orig-fn new-result args)))
+   (apply orig-fn result args)))
2 个赞

感谢! 我遇到一个问题,goto-line 执行成功返回的是 0 , 因此 plines 一直都是 0, limit 也是 0, 导致 buffer-substring 的两个参数都是 0 而报错,我的emacs是 27.2, 是 goto-line 的接口改了造成的吗

我把那一行改成

(progn (goto-line org-babel-result-lines-limit) (point))

后可以了

应该是吧,我原本也是写 (progn (gotoline ...) (point)) 后来发现 goto-line 返回 point 的 “副作用”,就给简化了。


EDIT:

11个月前改的 https://emba.gnu.org/emacs/emacs/-/commit/80a87af1357492b16a057c1f31d0e9a8b501d7d0

应该说是改进了,原先的返回值有点莫名其妙:

emacs-version ;; => "27.2"
(with-temp-buffer (insert "1\n2\3n") (goto-line 1))   ;; => 0
(with-temp-buffer (insert "1\n2\3n") (goto-line 2))   ;; => 0
(with-temp-buffer (insert "1\n2\3n") (goto-line 10))  ;; => 7
(with-temp-buffer (insert "1\n2\3n") (goto-line 100)) ;; => 97

返回 0 还好理解,就是成功跳到了指定行。

但如果但“失败”,也不抛出错误,而是跳到末行,然后返回余数 (- 指定行 末行),这个数字有什么用?还不如抛出错误。

而且所谓失败也算不上失败,毕竟跳到了末行。所以还是返回最终的 point 比较有用。

再次感谢分享, 个人猜测应该是可以根据返回的值来告诉用户还需要插入多少行才能真正跳到目标行,不过用起来确实有点奇怪。

另外,你分享的这个代码可以很好解决 org babel 同步执行输出的长度问题,也就是执行一个 src block 后,所有的结果先传给一个 emacs 变量,等代码执行完后再一次性对 result 进行处理后 insert 到当前 buffer.

我现在 org 下主要用 emacs jupyter,并且结合 ob-async 异步执行,每次有输出的话马上把这一行结果 append 到当前 buffer 的 results 区域, 如果循环打印 100 行,那么 org-babel-insert-result@limit 对每一行进行检查都是正常输出,最终 100 行都会被打出来。

大概看了一下 org-babel-insert-result 和 ob-async 的实现, 只有一个模糊的理解,感觉对 result 的操作基本就是在 tmp buffer 或者当前 buffer 的 result 区域做一些字符串的操作,因此似乎也不是很复杂。感觉要设置一个全局的计数变量来追踪当前已经 append 到 buffer 的字符数或者行数,每次执行一个 block 的时候重置这个变量,然后在 org-babel-insert-result@limit 里去累加当前输出的长度,如果超过了长度,就把 append 策略改成 replace, 把 result 的最后几行替换成最新的打印,也可以直接在 org-babel-insert-result 里加一个 hook ,每次插入后去重新数一遍当前 result 里的行数和字数,再根据计数结果来删除一些中间行。

另外,对 result 的灵活操作让我觉得可以实现类似 jupyter web 端上用 matplotlib 画动态图的效果,因为动态画图无非就是隔一段时间在原图基础上生成一张新的图,然后覆盖原图,能对 result 进行修改的话,每次 result 输出一个图片文件名,类似 [[file:./.ob-jupyter/789e32211243b5bfe27893da6584883ceea07164.svg]] 然后 hook 调用 org-redisplay-inline-images 刷新显示图片,之后再画另一张图替换替换这个结果,继续 redisplay…

我对 elisp 还不熟,先把需求和想法写这,如果以后自己有实现在这贴出来,或者其他人也有同样需求并且已经实现,那就更好了:) ,欢迎分享~

4# 楼代码更新。