在共同的LISP中写小CLI程序

2021-03-18 03:41:47

我编写了很多命令行程序。对于微小的程序,我通常使用Thepical的UNIX方法:抛出一个半表明shell脚本并继续前进。对于大程序,我制作了一个完整的常见LISP项目,具有ASDF SystemDefinition和此类。但是,在那里的小ish程序中间地面' t' t自己自己保证了一个完整的存储库,但我仍然希望具有正确的--help和错误处理的realinterface。

我发现常见的LISP是一种写作这些小型命令程序的良好语言。但是,开始(特别是初学者)可能有点恐吓(初学者),因为普通的LISP是一种非常灵活的语言,并不是一个洛克利进入一种工作方式。

在这篇文章中我' ll描述了我如何编写小型,独立命令行计划常见的lisp。它可能为您工作,或者您可能希望修改您自己需要的东西。

当你'在普通的LISP中编写程序时,你有很多选择。找出我帮助我决定一种方法的要求。

首先:每个新程序应该是一个文件。作为整体的其他一些文件(例如,makefile)还可以,但是一旦设置一切,创建一个新程序应该是表示添加一个单个文件。对于大量的项目,完整的项目目录和ASDF系统很大,但对于每个程序的一个文件的小程序减少了精神开销。

这些程序需要能够以典型的常见Lispinteractive风格开发(在我的情况下:用SWANK和VLIME)。互动发展中的普通丽斯的最佳部分之一,我不愿意放弃。特别是这意味着壳牌脚本样式方法,用#!/ / path / to / sbcl --script和顶部和直接在文件中的toplevel上运行代码,并在两个主要原因的主要原因工作:

由于您在启动文件中有一些丑陋的读取器宏,否则加载该文件将失败。

该程序需要做像解析命令行参数的事情,并使用错误代码退出,并调用EXIT会杀死SWANK进程。

程序需要能够使用库,因此QuickLisp将需要留下来。常见的Lisp内置了很多美好的东西,但有一些太纤维化太纤维化了太用来了。

程序需要具有适当的用户界面。命令行argipsmust被强大地解析(例如,折叠-a -b -c foo -d进入-abcfoo -dshould,按预期工作),必须抓住格式或未知的选项,而不是在地板上捕获它们,错误消息应该有意义,而且 - Help应该彻底和仔细写成,所以我记得几个月后才能淘汰。一个人页是一个很好的奖金,但不是必需的。

依靠一些基本约定(例如,命令foo始终在foo中.Lispand定义了一个名为toplevel的包装,如果它更轻松,我的生活是可以的。这些程序只是对我而言,所以我不必担心想要创建名称或其他东西的空格的可执行文件。

常见的LISP实现之间的可移植性很好,但很好。如果使用一些SBCL特定的润滑脂,请让我避免一堆odextra依赖性,那就是这些小型个人计划的良好。

尝试了许多不同的方法,我' ve解决了那个解决方案' m很满意。第一个i' ll描述了一般方法,然后是我们的一个实际示例程序的lllook。

我将所有小单文件常见的LISP程序保存在LISP DoccorporInside My Dotfiles存储库中。它的内容如下所示:

BIN目录是可执行文件结束的位置。我将它添加到我的$ path中,所以我不必讨论或复制一副界限。

男人包含生成的人页面。因为它'毗邻垃圾箱(我的路径上的哪个),人类程序自动找到了人类页面Asexpected。

.lisp文件是程序。每个新程序我想添加次数添加< programname> .lisp文件在此目录中并运行make。

我的小型常见的LISP计划遵循一些制作楼宇的惯例。让'查看foo.lisp文件的骨架作为一个例子。我' llshow这里的整个文件,然后逐步缩小它。

(eval-何时(:compile-toplevel:load-toplevel:执行)(ql:Quickload'(:with-user-abort ...):silent t))(defpackage:foo(:使用:cl)(:出口:Toplevel * UI *))(包装中:Foo);;;;配置------------------------------------------(DefParameter *无论* 123);;;;错误------------------------------------------- -----(定义条件用户错误(错误)(错误))(定义条件丢失 - foo(用户错误)()(:report"需要foo,但没有提供。& #34;));;;;功能----------------------------------------------(裁决foo(字符串)...);;;;跑 - - - - - - - - - - - - - - - - - - - - - - - - - --------(Defun Run(参数)(地图零#39; foo参数);;;;用户界面------------------------------------------(Defmacro Exit-On-Ctrl-C(&身体主体)`(with-user-abort:with-user-abort(progn,@ body))(with-user-abort:user-abort()( sb-ext:退出:代码130))))(defeparameter * ui *(采用:make-interface:name" foo" ...))(defun toplevel()(sb-ext:disable-debugger) (on-on-ctrl-c(多值 - 绑定(参数选项)(采用:parse-options-or-exit * ui *)...;句柄选项。(处理程序 - 案例(运行参数)(用户错误( e)(采用:打印错误和退出e)))))))))

首先,我们快速加载任何必要的库。我们总是想这样做,甚至是编译文件,因为我们需要当我们稍后在文件中使用它们的符号时需要相应的包。

with-user-abort是一个用于轻松处理控制-c的库,所有这些小程序都将使用。

接下来我们定义一个包foo并切换到它。包始终将其命名为Companed的二进制文件和文件的基本名,而且alwaysexports符号toplevel和* ui *。这些约定使其稍后会轻松地自动释放所有内容。

接下来我们定义任何配置变量。稍后将设置这些命令行参数(当我们运行命令行程序时)orat roat(交互式开发)。

;;;;错误------------------------------------------- -----(定义条件用户错误(错误)(错误))(定义条件丢失 - foo(用户错误)()(:report"需要foo,但没有提供。& #34;)))

我们定义了一个用户错误条件,用户可能会从中使用任何错误。这将使您可以轻松处理用户错误(例如,CASSA MANGLED正则表达式(FOO +作为参数)不同地从显得错误(即错误)。这使得可以更容易地对待这些错误:

我们定义了一个函数运行,它带有一些参数(作为字符串)并执行程序的主要工作。

重要的是,运行不处理命令行参数解析,它不会以错误代码退出程序,这意味着我们可以安全地调用它tosay"运行整个程序"当我们'在没有担心的情况下交互方式,而不担心它杀死我们的LISP过程。

;;;;用户界面------------------------------------------(Defmacro Exit-On-Ctrl-C(&身体主体)`(with-user-abort:with-user-abort(progn,@ body))(with-user-abort:user-abort()(采用:130港))))))

我们' LL与用户中止有点宏,以使其更加令人讨厌。如果众分子按Ctrl-C,我们将退出130的LL退出。也许有一天的一天我' ll将此拉动到采用,所以我没有' t haveto到处都是这三条线。

在这里,我们定义了我们上面导出的符号的* UI *变量。采用ISA命令行参数解析图书馆我写道。如果您想使用不同的Library,请随时使用。

(defun toplevel()(sb-ext:disable-debugger)(on-ctrl-c退出(多值 - 绑定(参数选项)(采用:parse-options-or-exit * ui *)......;句柄选项。(处理函数(运行参数)(用户错误(e)(采用:打印错误 - 退出e)))))))

最后我们定义了toplevel函数。这将只被称为程序作为独立程序,从不交互方式。 ithands所有超出程序主要肠道的工作(由运行函数处理),包括:

Build-binary.sh是一个小脚本,用于从.lisp文件构建可执行二进制文件。 ./build-binary.sh foo.lisp将构建foo:

#!/ usr / bin / env bashset -euo pipefaillisp = $ 1name = $(baseName" $ 1" .lisp)shiftsbcl --load" $ lisp" \ - "(sb-ext:save-lisp-and die \" $ name \":可执行文件t:save-runtime-options t:toplevel' $ name :Toplevel)"

在这里,我们看到命名约定变得重要 - 我们知道包裹的名称与二进制文件相同,它将导出符号toplevel,这始终命名二进制文件的入口点。

build-manual.sh是类似的,并使用采用的人的页面构建。如果你不关心你的个人节目的建立男人网页,你可以忽略这一点。我承认,为这些程序生成了一个有点愚蠢,因为他们'只有我的敌人使用,但我可以免费获得它,为什么不用?

#!/ usr / bin / env bashset -euo pipefaillisp = $ 1name = $(baseName" $ lisp" .lisp)out =" $ name.1" shiftsbcl --load&# 34; $ lisp" - - 公开"(有开放文件(f \" $ out \":方向:输出:Ifsials:supersede)(采用:print-manual $名称:* ui *:流f))" \ - 辞职

这就是为什么我们始终命名采用接口变量* UI *并从包中导出它。

最后,我们有一个简单的makefile,所以我们可以运行使得重新生成约会二进制文件和男人页面:

文件:= $(通配符* .lisp)名称:= $(文件:.lisp =)。Phony:所有清洁$(姓名)全部:$(名称)$(姓名):%:bin /%man / man1 / %.1bin /%:%.lisp build-binary.sh makefile mkdir -p bin ./build-binary.sh $&lt; MV $(@ f)箱/人/ man1 /%。1:%.lisp build-manual.sh makefile mkdir -p man / man1 ./build-manual.sh $ <$ lt; MV $(@ F)MAN / MAN1 / CLEAN:RM -RF BIN MAN

我们使用通配符自动查找.lisp文件,以便我们在我们想要制作新程序后添加新文件后,我们没有任何额外的东西。

这里最值得注意的行是$(姓名):%:bin /%man / man1 /%。1使用静态模式RULETO自动定义构建每个程序的PHONY规则。如果$(姓名)是Foo Bar,这条线有效地定义了两个虚假规则:

这让我们运行使foo制作foo.lisp的二进制和手册页。

现在我们看到了骷髅,让我们看看我的一个实际程序之一。它&#39; s称为batchcolor和它&#39; s用来突出显示文本(通常日志文件)中的rencarexpression匹配的扭曲:每个独特的匹配在单独的颜色中突出显示,这使得它更容易在视觉上解析。

例如:假设我们有一些具有表单行的日志文件&lt;时间戳&gt; [&lt;请求ID&gt;]&lt; leel lock&gt; &lt;消息&gt; Request ID是UUID的位置,并且消息可能为各种事物提供其他UUID。这样的日志文件可能看起来它:

2021-01-02 14:01:45 [F788A624-8DCD-4C5E-B1E8-681D0A68A8D3]信息传入请求获取/用户/ 28B2D548-EFF1-471C-B807-CC2BCEE76B7D / MATTION / 7CA6D8D2-5038-42BD-A559-B3EE0C8B7543 / 2021-01-02 14:01:45 [F788A624-8DCD-4C5E-B1E8-681D0A68A8D3]信息7CA6D8D2-5038-42BD-A559-B3EE0C8B7543不缓存,检索... 2021-01-02 14:01:45 [F788A624-8DCD-4C5E-B1E8-681D0A68A8D3]警告用户28B2D548-Eff1-471C-B807-CC2BCEE76B7D无需进入物品7CA6D8D2-5038-42BD-A559-B3EE0C8B7543,否认请求.2021-01-02 14:01: 46 [F788A624-8DCD-4C5E-B1E8-681D0A68A8D3]信息返回HTTP 404.2021-01-02 14:01:46 [BEA6AE06-BD06-4D2A-AE35-3E83FEA2EDC7]信息传入请求获取/用户/ 28B2D548-EFF1-471C-B807 -cc2bcee76b7d / things / 7ca6d8d2-5038-42bd-a559-b3ee0c8d7543 / 2021-01-02 14:01:46 [BEA6ae06-BD06-4D2A-AE35-3E83FEA2EDC7] INFO THIPT 7CA6D8D2-5038-42BD-A559-B3EE0C8D7543未缓存,检索... 2021-01-02 14:01:46 [b04ced1d-1cfa-4315-aaa9-0e245ff9a8e1]信息传入请求发布/用户/注册/ 2021-01-02 14:01:46 [BEA6AE06-BD06-4D2A-AE35-3E83FEA2EDC7]信息返回HTTP 200.2021-01-02 14:01:46 [B04CED1D-1CFA-4315-AAA9-0E245FF9A8E1] ERR错误运行SQL查询:连接拒绝.2021 -01-02 14:01:47 [b04ced1d-1cfa-4315-aaa9-0e245ff9a8e1] err返回http 500。

如果我试图直接阅读这一点,它&#39;对于我的眼睛很容易瞥见除了逐步走向旁边。

不幸的是,它不太帮助太多,因为所有uuids都是相同的颜色:

这对我来说更容易视觉解析。 SeparateTequest日志的交错现在是明显的,从ID的颜色,它且易于逐渐匹配各种用户ID和事物ID。你甚至注意到叮叮当当于之前的呢?

BatchColor具有其他其他生活质量特征,如针对特定字符串的挑选显式科学(例如,ERR的红色):

我使用这个特定的BatchColor调用,所以我经常&#39;你把它放在自己的壳牌脚本中。我在本地日期开发时将其用来尾部文件,并且它在视觉上扫描日志输出更容易。它同时为其他类型的文本烹饪,如突出显示Irclog中的昵称。

首先是我们快速负载库。我们&#39; ll使用用于命令行参数处理,cl-ppcre for for rament表达式,并以前提到的用户中止以处理控制-c。

接下来我们defectameter一些变量来保存一些设置。 *开始*将在随机化颜色,Don&#39;现在担心它。

;;;;错误------------------------------------------- ------------------- 34;必需的正则表达式。&#34;))(定义 - 条件ormer-rege-regex(用户错误)((底层错误:initarg:底层错误))(:报告(lambda(cs)(格式s &#34;无效的正则表达式:〜a&#34;(插槽值c&#39;底层错误)))))(定义 - 条件重叠 - 组(用户错误)()(:报告&#34;无效的正则表达式:似乎包含重叠的捕获组。&#34;))(定义 - 条件畸形 - 显式(用户错误)((spec:initarg:spec))(:报告(lambda(cs)(格式s&# 34;无效的显式规格〜s,必须是表单\&#34; r,g,b:string \&#34;颜色为0-5。&#34;(插槽值C&#39;规格)))))))

在这里,我们定义了用户错误。其中一些是不言自明的,一旦我们在行动中看到它们就会更进一步。特定的索赔是&#39; t与整体想法一样重要:对于用户错误,我们知道Mighthappen,显示有用的错误消息,而不是只是在用户播出回溯。

接下来我们有程序的实际肉。显然,对于每个节目来说,这将是不同的,所以如果你没有&#39; tcare关于这个具体问题,可以自由跳过这个。

;;;;功能------------------------------------------- -----------(Defun RGB代码(RGB);; 256色模式颜色值基本上是基础6的R / G / B,但是;向上移动16次以占星部8 +8色。(+(* r 36)(* g 6)(* b 1)16)))

我们&#39;重新突出显示不同颜色的不同比赛。我们&#39; ll需要合理的颜色,使得这种有用的颜色,所以使用基本的8/16 Ansicolors ISN&#39; t足够。完整的24位TrueColor是矫枉过正的,但是8位Ansicolors将很好地工作。如果我们忽略基色,我们基本上有6 x 6 x 6 = 216种颜色。 RGB-Code将从0到5的红色,绿色,AndBlue值拍摄,并返回颜色代码。请参阅Wikipedia以了解更多信息。

(Defun Make-Colors(extudep)(:(结果(make-array 256:填充指针0)))(dotimes(r 6)(dotimes(g 6)(dotimes(b 6)(除非(funcall排除)(funcall + RGB))(矢量 - 推送延伸(RGB码RGB)结果))))))))))(defeParameter *暗色*(制作颜色(LABDA(v)(&lt; v 3)))))) (DEFPARAMETER *浅色*(制作颜色(LAMBDA(V)(&gt; v 11)))))

现在我们可以建立一些颜色阵列。我们可以使用216个可用性剔除者中的任何一个,但在实践中,我们可能不想,因为最黑暗的颜色太暗无法在黑暗的终端上读取,反之亦然。在实用性的特许经济级别。 39; LL生成两个单独的颜色阵列,一个排除的颜色,其总值太暗,一个排除什切尔的颜色太轻了。

(请注意*暗色*是&#34;适用于使用的颜色阵列,适用于Moderon黑暗终端和#34;而不是&#34;它们本身是黑暗的颜色阵列,它们是黑暗的&#34; .naming的东西很难。 )

请注意,在加载BatchColor.lisp文件时将生成这些数组,这是我们构建二进制文件时的生成。他们每次运行生成的二进制文件时都会重新计算。在这种情况下,它并不重要(尾部很小),但它值得记住,以防你有一些数据youmant(或者没有想要的)来计算在构建时间而不是运行时计算。

在这里,我们制作一个哈希表来存储我们想要的字符串的字符串和颜色,我们想要的字符串颜色(例如,Err应该是红色的,信息青色)。键将是帖子和值RGB代码。

(defun djb2(string); http:http://www.cse.yorku.ca/~oz/hash.html(减少(lambda(hash c)(mod(+(* 33哈希)c)(EXPT 2 64) ))字符串:初始值5381:密钥#&#39; char-code))(defun find-color(string)(gethash字符串* sexicicits *(:(如果*黑暗* *暗色* *浅色*))))(aref颜色(mod(djb2字符串)* start *)(长颜色))))))))

对于我们想要显式颜色的字符串,我们只需查找* SIXICITS *并返回它。

否则,我们希望突出不同颜色的独特匹配。例如,我们可以执行此操作的不同方式:我们可以在第一次看到字符串中随机选择颜色并将其存储在哈希表中进行追随遇到。但这意味着我们随着时间的推移而种植那个哈希表,而我经常使用这种实用程序的一件事是尾随的,在本地开发时,我会在批量生产中增长和增长,因此内存使用情况重启,哪个是&#39; t的理想。

相反,我们&#39; ll hash每个字符串,一个简单的djb哈希,并使用它toindex进入适当的颜色数组。这可确保相同的匹配相同的颜色,避免使用我们曾经看到的每一个匹配。

会有一些碰撞,但是没有太多&#39;我们可以用它的〜200种颜色来做。我们可以使用前面提到的16位颜色,但是我们&#39; D必须担心挑选挑选人类的颜色,以便对人类轻松讲解,并且对于这种简单的实用程序,我没有&#39;致意。

(Defun Ansi-color-start(颜色)(格式零&#34;〜c [38; 5; dm&#34;#\ resive color)(defun ansi-color-end()(格式nil&#34 ;〜C [0m&#34;#\ eashive))(Defun打印彩色(String)(格式*标准输出*&#34;〜a〜a〜a&#34;(Ansi-color-start(find-彩色字符串))字符串(ANSI-Color-End))))

接下来,我们有一些函数来输出适当的ANSI逃离到突出显示。我们可以使用图书馆,但它只有两条线。它&#39;鼻涕值得。

(Defun Colorize-L ......