如何从命令行格式化常见的 lisp 代码(包括换行符)?
Posted
技术标签:
【中文标题】如何从命令行格式化常见的 lisp 代码(包括换行符)?【英文标题】:How can I format common lisp code (including newlines) from the command line? 【发布时间】:2022-01-15 03:16:48 【问题描述】:我正在寻找一种从命令行漂亮地打印/美化/自动格式化 Common Lisp 源代码的方法。我基本上想要为 python 复制black
的功能(参见https://github.com/psf/black)。这将是一个具有最小依赖项的命令行工具(例如,我不想在 emacs 中运行它),它是幂等的,并在适当的情况下自动插入和删除换行符,以及进行缩进。
基本上,我希望能够为其提供仅包含一行的源代码并让它生成一个可读的文件。是否存在满足所有甚至部分要求的东西?我已经在 github 上查看了大多数低调的果实,它们似乎只进行自动缩进,而不是自动格式化(不能分解长行)。如果没有,在 lisp 中是否有此类事情的先例,或者由于某种特定于该语言的原因,它是否非常困难?
【问题讨论】:
部分解决方案内置于语言中,请参阅the Lisp Pretty Printer。但是,您仍然有一个问题,源代码需要在read
中,并且任何阅读器宏都会立即展开而不是保留。
对于一般情况,您需要知道手头源代码的系统上下文,因为您需要知道任何应该生效的自定义阅读器宏。例如,[
和 ]
经常用于 DSL,但在标准的 readtable 中,apple][
是一个有效且不起眼的符号。
@Svante 我有点新手,“系统上下文”是什么意思?问题必须受到多大限制才能使其成为非问题?
基本上:没有宏(标准中的宏除外),没有阅读器宏。然后就很琐碎了。但这些都是严重的限制。
你看到这里列出的 3 个项目了吗? github.com/CodyReichert/awesome-cl#linting-code-formatting trivial-formatter、cl-indentify、lisp-format(看起来 trivial-formatter 想要一个 ASDF 系统来格式化而不是单个文件)。 cl-indentify 在我的测试中没有拆分很长的一行。其他两个没试过。
【参考方案1】:
[这应该是评论,但太长了。]
这介于困难和不可能之间。考虑下面的表格,这里在一行中给出:
(with-collectors (odd even) (iterate next ((i 0)) (when (< i 100) (if (evenp i) (even i) (odd i)) (next (1+ i)))))
这应该如何缩进?好吧,这是一个完全支持 lisp 的编辑器可能会如何缩进它:
(with-collectors (odd even)
(iterate next ((i 0))
(when (< i 100)
(if (evenp i)
(even i)
(odd i))
(next (1+ i)))))
那是……大错特错。以下是同一编辑器稍后将如何缩进:
(with-collectors (odd even)
(iterate next ((i 0))
(when (< i 100)
(if (evenp i)
(even i)
(odd i))
(next (1+ i)))))
这次做对了。
发生了什么变化?好吧,改变的是语言:特别是第二个示例中的语言已扩展为包括编辑器现在知道如何处理的with-collectors
表单以及它也可以理解的iterate
表单。
所以这似乎是一个晦涩的问题,但事实并非如此。因为 Lisp 的全部意义(可以说)是,为了解决问题,你可以逐步无缝地将语言从你开始使用的基础语言扩展到你想用来解决问题的语言。
这意味着许多 Lisp 程序由一系列对该语言的扩展组成,然后是用这种新的扩展语言编写的程序,在其中解决了问题。 Lisp 是一种面向语言的编程语言。
那个的意思是,了解如何缩进 Lisp 程序的唯一真正可靠的方法是询问程序。在上面的例子中,最初系统认为with-collectors
是一个函数,它就这样缩进了它。后来,当它知道定义时,它意识到这是一个let
样式的构造,并正确缩进了它。 iterate
也是如此。
所有这一切的意思是,一个独立的工具确实没有希望很好地缩进一个实质性的 Lisp 程序,因为要做到这一点,它需要比没有成为程序的情况下更多地了解该程序.当然,这就是为什么 Lisp 鼓励“驻留”开发环境,将正在开发的程序加载到开发环境中,而不是开发环境或多或少与正在开发的程序完全分离的“分离”环境.通过解析程序中的定义并找出扩展语言的定义,一个独立的工具可能会获得大部分的成功。但要做到这一点,再次,需要你成为程序。
作为一种面向语言的编程语言有显着的好处,但也有成本,不幸的是,这就是其中之一。
如果您的任务非常有限,并且如果您真的想在一行上使用一些大表达式(因此,可能没有 cmets),那么下面将尝试执行此操作。你需要把它包装成一个程序。
警告购买者。此代码肯定不安全,可以根据其输入执行任意代码。 不要使用它,除非你确定你输入的输入是安全的。所以,实际上不要使用它。
;;;; Note horrid code, This is *certainly* unsafe
;;;
;;; This uses EVAL which I think is necessary here, but is what makes
;;; it unsafe.
;;;
(in-package :cl-user)
(eval-when (:compile-toplevel :load-toplevel :execute)
(warn "UNSAFE CODE, USE AT YOUR OWN RISK."))
(defvar *tlf-handlers* (make-hash-table))
(defmacro define-tlf-handler (name ds-arglist &body forms)
(let ((formn (make-symbol "FORM")))
`(progn
(setf (gethash ',name *tlf-handlers*)
(lambda (,formn)
(destructuring-bind ,ds-arglist (rest ,formn)
,@forms)))
',name)))
(define-tlf-handler in-package (package)
(let ((p (find-package package)))
(if p
(progn
(format *debug-io* "~&Setting package ~S~%" package)
(setf *package* p))
(warn "no package ~S" package))))
(define-tlf-handler defpackage (package &body clauses)
(format *debug-io* "~&Defining package ~S~%" package)
(eval `(defpackage ,package ,@clauses)))
(define-tlf-handler defmacro (name arglist &body forms)
(format *debug-io* "~&Defining macro ~S~%" name)
(eval `(defmacro ,name ,arglist ,@forms)))
(define-tlf-handler eval-when (times &body forms)
(declare (ignore times forms))
(warn "Failing to handle eval-when"))
(define-condition pps-reader-error (reader-error simple-error)
())
(defparameter *pps-readtable* (copy-readtable nil))
(set-dispatch-macro-character
#\# #\+
(lambda (s c n)
(declare (ignore c n))
(error 'pps-reader-error
:stream s
:format-control "Can't handle #+"))
*pps-readtable*)
(set-dispatch-macro-character
#\# #\-
(lambda (s c n)
(declare (ignore c n))
(error 'pps-reader-error
:stream s
:format-control "Can't handle #-"))
*pps-readtable*)
(defun pp-stream (s &optional (to *standard-output*))
(with-standard-io-syntax ;note binds *package*
(let ((*readtable* *pps-readtable*)
(*read-eval* nil)
(*print-case* :downcase))
(do ((form (read s nil s) (read s nil s)))
((eq form s) (values))
(format to "~&")
(pprint form to)
(when (and (consp form) (symbolp (car form)))
(let ((handler (gethash (car form) *tlf-handlers*)))
(when handler (funcall handler form))))))))
(defun pp-file (f &optional (to *standard-output*))
(with-open-file (in f)
(pp-stream in to)))
【讨论】:
您知道与此功能有些接近的现存事物吗?也许一个命令行工具可以执行您建议的完全列表感知编辑器可能会执行的操作? 很好解释! @bxw:不,我不知道。但我永远不会使用这样的东西,所以这并不意味着它不存在。但他们可能不会,因为写 Lisp 的人通常不会使用我期望的这种东西。 @ignisvolens 那么也许我问错了问题。写 Lisp 的人是做什么的?他们只是简单地记住了风格指南,或者他们专门使用带有插件的编辑器来为他们管理这类事情? 这与您第一次解释用例的“格式化 1 条巨线”完全不同!是的,与 Python 不同,没有太多语法或代码样式可供观看,因此编辑器适合您。如果您仍想确保一致性,我们有一些工具,就像我在其他评论中描述的那样。【参考方案2】:使用 emacs 作为命令行工具将是最好的解决方案...
问题是,如果您要使用具有正确设置的 emacs,则 emacs 会为您完成缩进。
的确,emacs 对于初学者来说并不容易使用。非常压倒性的。我尝试了几次,中间有很长一段时间都放弃了。
我在a blogpost 中列出了一组我在编写 lisp 代码时使用的最小命令。一个列表,我会喜欢有人在启动 emacs 时给我的。
我还写了a blogpost 用于使用 Roswell 设置 Common Lisp 环境,这是迄今为止我见过的最好的 Common Lisp 实现管理器和包管理器。
Emacs,然后您可以在终端/命令行中运行 - 以 $ emacs -nw
开头,它是 --no-window-system
的缩写。
然后它像 vim 一样在终端内运行,您只需使用按键在其中移动光标并编辑代码。 - 但是您仍然可以在终端内打开新的“窗口”和缓冲区(通过C-x 3
垂直剖析您的窗口,通过C-x 2
水平剖析您的窗口。将光标从一个窗口跳转到另一个窗口:C-x o
。
我在第一次提到的blogpost中以备忘单的方式列出了所有这些命令。
每当您安装 emacs 时,您的主文件夹中都会有一个 .emacs.d
文件(至少在 Linux 和 MacOs 中 - 在 Windows 中我非常缺乏经验,因为我已经 10 多年没有使用它了......)。里面是一个init.el
文件,它控制emacs 的启动行为以及您配置设置的位置。
作为 Common Lisp 编程的最小工作 init.el
- 使用 Roswell 时(意味着如果您按照我的 second blogpost 中的所有安装说明进行操作,您可以在那里复制粘贴(语言这个 init.el 文件中的设置是 emacs-lisp):
;; initialize/activate package management
(require 'package)
(setq package-enable-at-startup nil)
(setq package-archives '())
;; connect with melpa emacs lisp package repository
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
;; initialization of package list
(package-initialize)
(package-refresh-contents)
;; Ensure `use-package` is installed - install if not
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; slime for common-lisp
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; to connect emacs with roswell
(load (expand-file-name "~/.roswell/helper.el"))
;; for connecting slime with current roswell Common Lisp implementation
(setq inferior-lisp-program "ros -Q run");; for slime;; and for fancier look I personally add:
(setq slime-contribs '(slime-fancy));; ensure correct indentation e.g. of `loop` form
(add-to-list 'slime-contribs 'slime-cl-indent);; don't use tabs
(setq-default indent-tabs-mode nil);; set memory of sbcl to your machine's RAM size for sbcl and clisp
;; (but for others - I didn't used them yet)
(defun linux-system-ram-size ()
(string-to-number (shell-command-to-string
"free --mega | awk 'FNR == 2 print $2'")))(setq slime-lisp-implementations
`(("sbcl" ("sbcl" "--dynamic-space-size"
,(number-to-string (linux-system-ram-size))))
("clisp" ("clisp" "-m"
,(number-to-string (linux-system-ram-size))
"MB"))
("ecl" ("ecl"))
("cmucl" ("cmucl"))))
保存。并重新启动 emacs。那么正确的缩进至少会起作用。
还有更多方便的设置。 (例如,在您编写通用 lisp 代码时自动向您显示预期的函数参数等 - 要拥有它们,您可以通过 git clone my .emacs.d
文件夹
git clone https://github.com/gwangjinkim/.emacs.d.git
并暂时用它替换您的.emacs.d
。并启动 emacs。但风险自负。
【讨论】:
以上是关于如何从命令行格式化常见的 lisp 代码(包括换行符)?的主要内容,如果未能解决你的问题,请参考以下文章
如何防止 Visual Studio Code Flutter/Dart Editor 格式化新行/换行代码?