
CLI编程语言开发工具【免费下载链接】elvishPowerful scripting language versatile interactive shell项目地址https://gitcode.com/gh_mirrors/el/elvish点击查看免费下载Elvish 是一款将完整编程语言作为设计目标的交互式 shell其语义与传统 POSIX shell 有着根本性差异标准输出被拆分为字节流与值通道两条路径退出状态被异常机制取代代码执行遵循先整体解析编译、再执行的三阶段模型连赋值都建立在持久化数据结构之上。本文以 website/learn/unique-semantics.md 为主线结合仓库源码逐一剖析这四组设计帮助你从底层理解 Elvish 的行为方式并写出更安全、更可预测的脚本。从字符串一切到结构化数据Elvish 的 IO 设计动机传统 shell 用字符串承载一切数据字符串可以存入变量、作为函数参数、写进输出、读入输入。字符串简单易用但一旦数据本身具有内在结构例如列表、键值对就力不从心。常见的伪结构化方案是把数据编码成每行一条记录、每个空白分隔的字段一个属性的格式——只要数据里不出现空白字符这种方案尚且可用一旦数据本身包含空格、换行或特殊字符转义与引号的复杂度就会迅速失控最终不得不对字符串做各种黑魔法处理。部分 shell 也提供了列表、映射等数据结构但它们通常不是一等公民可以存入变量却往往无法嵌套、无法作为函数参数传递、也无法从函数中返回。Elvish 的回应是让数据结构成为一等公民并让它们能像字符串一样在管道中自由流动。数据结构与返回它们put与双通道输出Elvish 对列表、映射等数据结构提供一等公民支持。下面是用列表的例子~ var li [foo bar lorem ipsum] ~ kind-of $li # kind is like type ▶ list ~ count $li # count the number of elements in a list ▶ 3内建数据结构的完整说明见 language reference。变量能存列表、能作为命令参数传列表但如果不能从函数中返回列表价值就大打折扣。一种直觉的做法是用echo打印列表、再用输出捕获回收它~ fn f { echo [foo bar lorem ipsum] } ~ var li (f) # (...) is output capture, like $(...) in other shells ~ kind-of $li ▶ string ~ count $li # count the number of bytes, since $li is now a string ▶ 23可以看到输出列表的尝试把它变成了字符串Elvish 的echo和其他 shell 一样是面向字符串的要echo 一个列表必须先把它序列化成字符串。Elvish 为此提供了put命令按原样输出结构化值~ fn f { put [foo bar lorem ipsum] } ~ var li (f) ~ kind-of $li ▶ list ~ count $li ▶ 3put与echo的差异来自底层实现。在源码 pkg/eval/builtin_fn_io.go 中可以看到这两者的注册与实现分属两个阵营put调用fm.ValueOutput()后逐个out.Put(a)把值原封不动地放进内部的值通道保持其全部内部结构echo底层复用print调用fm.ByteOutput()并通过vals.ToString(arg)把每个参数序列化成字符串再写入字节输出。也就是说Elvish 的标准输出由两部分组成传统的、面向字节的文件以及内部的、面向值的通道。echo写文件、put写值通道。如果你在命令提示符下直接运行put输出值会带一个前导▶~ put [foo bar] ▶ [foo bar]前导箭头只是该命令往值通道写了东西的可视化标识并非值本身的一部分。回顾一下你会发现kind-of、count这些内建命令的输出其实也都走值通道。通过管道传递数据结构标准输出有两条通道并非故事的全貌Elvish 的管道同样拥有这两条通道结构数据可以顺着管道的值通道流动。例如each命令从值通道读取输入对每个值应用一个函数~ put lorem ipsum | each {|x| echo Got $x } Got lorem Got ipsum许多内建命令都以值作为输入或输出。例如take保留固定数量的条目~ put [lorem ipsum] foo\nbar [keyvalue] | take 2 ▶ [lorem ipsum] ▶ foo\nbar值得注意列表、字符串、映射这些不同类型的值可以混在同一条值管道中而不发生类型坍塌——这正是take能原样保留foo\nbar中换行转义的原因。从实现看管道两侧各有一个值通道。在 pkg/eval/compile_effect.go 中pipelineChanBufferSize 32规定了值通道的缓冲大小同一个文件里的pipelineOppkg/eval/compile_effect.go负责为每条管道创建端口其中值通道端口Port.Chan与文件端口Port.File并列存在由formOwnedPort跟踪归属并在表单结束后关闭pkg/eval/compile_effect.go。此外 Elvish 还提供了only-bytes与only-values两个命令分别丢弃值通道或字节通道、只转发另一部分见 pkg/eval/builtin_fn_io.go可以用于强制降级为纯字节或纯值的数据流。与外部命令互操作to-json/from-json结构值无法直接传给外部命令因为外部命令只认字节流。Elvish 为此内置了 JSON 序列化/反序列化命令对注册于 pkg/eval/builtin_fn_io.go 的from-json与to-json。下面的例子展示如何与一个 Python 脚本互操作~ cat sort-list.py import json, sys li json.load(sys.stdin) li.sort() json.dump(li, sys.stdout) ~ put [lorem ipsum foo bar] | to-json | python sort-list.py | from-json ▶ [bar foo ipsum lorem]在此基础上为外部命令写一个包装函数非常容易~ fn sort-list { to-json | python sort-list.py | from-json } ~ put [lorem ipsum foo bar] | sort-list ▶ [bar foo ipsum lorem]值得注意的是管道在这里发生了两次通道切换to-json把值通道转换为字节流交给 Pythonfrom-json再把 Python 输出的字节流还原为值通道中的结构值最终▶符号确认了结果确实是以列表身份返回的。语言未来还可能加入更多序列化/反序列化命令。退出状态与异常从$?到异常中断Unix 命令以非零退出码表示错误传统 shell 用$?变量暴露它true echo $? # prints 0 false echo $? # prints 1内建命令和用户自定义函数也遵循同样的约定尽管它们并非 Unix 命令bad() { return 2 } bad echo $? # prints 2这个模型只有在大多数错误非致命前一条命令的错误通常不影响后续命令执行且脚本作者记得为少数致命错误检查$?时才算凑合可用。Elvish 没有退出状态的概念取而代之的是异常异常一旦抛出就会中断执行流。上面bad函数的 Elvish 等价写法如下fn bad { fail bad things have happened # throw an exception } bad # will print a stack trace and stop execution echo after bad # not executed如果交互式运行需要在bad之后按Alt-Enter输入一个真正的换行确保echo after bad与bad处于同一个 chunk 中执行。外部命令的非零退出状态同样会转化为异常false # will print a stack trace and stop execution echo after false另一种描述方式是Elvish确实有退出状态只是非零退出状态默认会终止执行你可以用try块包裹命令来捕获并处理非零退出状态。与 POSIX shell 相比Elvish 的行为相当于全局set -eset -o errexit或者说在所有命令之间隐式地插入了。默认在坏事发生时停止执行让 Elvish 更安全、行为更可预测。从实现上看fail命令定义于 pkg/eval/builtin_fn_flow.go它返回一个FailError若参数本身是error则原样返回FailError实现了vals.PseudoMap可通过.type与.content字段访问是 Elvish 异常体系中的一等值。异常在中途抛出后当前 chunk 的剩余部分不会执行——这一短路行为直接体现在 chunk 的执行逻辑中见下文代码执行的三个阶段。谓词与if从退出码到布尔值退出状态的用途不止于错误。Unix 工具箱中有不少命令以退出码 0 表示真、1 表示假例如test即[测试文件类型、比较数字和字符串grep有匹配时退出 0否则退出 1diff文件相同时退出 0否则退出 1true和false分别恒以 0 和 1 退出。POSIX shell 的if控制结构就是为这类谓词命令设计的它接收一条管道当管道中最后一条命令以 0 退出时执行分支体。示例# command 1 if true; then echo always executes fi # command 2 n10 if test $n -gt 2; then echo executes when $n 2 fi # command 3 if diff a.txt b.txt; then echo a.txt and b.txt are the same fi既然 Elvish 把非零退出状态视为一种异常POSIX shell 那套谓词命令 if的配合方式在 Elvish 中就行不通了。Elvish 的if与大多数非 shell 编程语言类似接收一个值若该值按布尔语义为真则执行分支体。上面的第一条命令在 Elvish 中写作if $true { echo always executes }第二条命令的写法需要先解释 Elvish 中谓词的工作方式Elvish 的谓词只是输出一个布尔值要么是$true要么是$false~ 10 2 ▶ $true ~ 1 2 ▶ $false要在if中使用谓词只需用()捕获它的输出。因此第二条命令写作var n 10 if ( $n 2) { echo executes when $n 2 }注意if后面的括号与 C 语言不同在 C 中括号是语法的强制要求而在 Elvish 中它起的是输出捕获运算符的作用。有时你需要判断的正是外部命令是否以 0 退出。这时可以使用异常捕获运算符?()if ?(diff a.txt b.txt) { echo a.txt and b.txt are the same }在 Elvish 中所有异常按布尔语义都为假而特殊的$ok值按布尔语义为真。若diff以 0 退出?(...)求值为$ok真否则求值为一个异常假。整体上与 POSIX 的if语义相近。?()的实现可在 pkg/eval/compile_value.go 的exceptionCaptureOp.exec中看到执行子操作若无异常则返回[]any{OK}有异常则返回[]any{exc}——异常本身被当作普通值捕获下来。作为对比普通的输出捕获()由outputCaptureOppkg/eval/compile_value.go实现它借助ValueCapturePort()收集子操作写入值通道的所有值。需要特别警惕的是下面的写法有严重缺陷?()会阻止任何异常抛出。这里的本意只是把diff退出码为 1这一种情况转化为布尔值如果diff以 2 退出通常意味着真正的错误例如a.txt不存在吞掉这类错误违背了 Elvish 宁可谨慎的哲学。更完善的退出状态处理机制仍在设计中。代码执行的三个阶段解析、编译、求值一段作为一个整体求值的代码称为一个chunk借自 Lua 的术语。从命令行运行elvish some-script.elv时整个脚本就是一个 chunk在交互模式下每次按回车输入的代码就是一个 chunk。Elvish 分三个阶段解释一个代码 chunk首先把代码解析成语法树然后把语法树编译为内部表示最后求值刚生成的内部表示。如果前两个阶段发生任何错误Elvish 会拒绝整个 chunk 而不执行其中任何一部分。例如未闭合的括号在 Elvish 中属于解析阶段错误。下面的代码作为一个 chunk 执行时除了打印解析错误外什么也不会做echo before echo (同样的代码按 bash 解释也包含语法错误。但如果你把它存为bad.bash并运行bash bad.bashbash 会先执行第一行然后才抱怨第二行的语法错误。同样地在 Elvish 中使用未赋值的变量是编译错误所以下面的代码同样什么也不会执行# assuming $nonexistent was not assigned echo before echo $nonexistent其他 shell 中似乎没有与编译错误对应的概念但这一多出来的编译阶段让语言更安全。将来可能引入的可选类型检查也会融入编译阶段。从源码看chunk 的顺序执行语义在 pkg/eval/compile_effect.go 的chunkOp.exec中体现它依次执行 chunk 内的每条管道pipelineOp一旦某条管道返回异常就立即返回、不再执行后续管道。而解析 → 编译 → 求值的三阶段流程贯穿于pkg/parse语法解析器与pkg/eval编译器与求值器两个包的协作之中chunkOp、pipelineOp、formOp等编译产物pkg/eval/compile_effect.go正是编译阶段产出的内部表示。赋值语义赋值即复制在 Python、JavaScript 及许多其他语言中如果把一个容器如映射赋给多个变量通过任一变量所做的修改都会作用于同一个容器。下面的例子最能说明问题m {foo: bar, lorem: ipsum} m2 m m2[foo] quux print(m[foo]) # prints quux原因在于这类语言的变量并不持有真正的映射而持有对它的引用。赋值m2 m之后两个变量指向同一个映射随后的元素赋值m2[foo] quux改变了底层映射所以m[foo]也跟着变了。Elvish 不是这样~ var m [foobar loremipsum] ~ var m2 $m ~ set m2[foo] quux ~ put $m[foo] ▶ bar看起来赋值m2 $m时整个映射被从$m复制进了$m2之后对$m2的任何修改都不会影响原映射。你可以完全采用这种理解方式把赋值看作复制就能正确建模 Elvish 的行为。但每次赋值都复制整个列表或映射会不会很昂贵不会——这里的复制其实非常廉价。它是用写时复制copy-on-write实现的吗——即复制被延迟到$m2真正被修改时才发生也不是之后对新的$m2的修改同样非常廉价。想弄明白这是如何做到的请看下一节。实现细节持久化数据结构与 Python、JavaScript 一样Elvish 的变量如$m、$m2也只持有对底层映射的引用。但那个映射是不可变的——创建之后永不改变。这就解释了为什么$m没有变化因为$m所引用的映射从未改变。可映射既然不可变m2[foo] quux又是怎么做到的呢Elvish 的映射实现还有另一个性质虽然映射不可变但很容易从一张映射创建出它的微调版本——给定一张映射很容易得到另一张几乎相同的映射要么 1) 多一个键值对要么 2) 改变某个键的值要么 3) 少一个键值对。这个操作很快即使原映射非常大。底层能力由assocassociate和dissocdissociate两个内建命令暴露~ assoc [] foo quux # add one pair ▶ [fooquux] ~ assoc [foobar loremipsum] foo quux # modify one pair ▶ [loremipsum fooquux] ~ dissoc [foobar loremipsum] foo # remove one pair ▶ [loremipsum]assoc、dissoc的实现非常薄它们分别直接转发到vals.Assoc与vals.Dissoc见 pkg/eval/builtin_fn_container.go而vals层最终落到pkg/persistent的持久化数据结构上。在 pkg/persistent/hashmap/map.go 中Map接口的文档明确写道它是一个不可变的持久化关联数据结构支持近 O(1) 地创建共享底层数据的修改版本且因为不可变所有方法都并发安全。接口上定义了Assoc(k, v)返回带新键值对的近似映射与Dissoc(k)返回去掉某键的近似映射。那么赋值是怎么回事呢映射不可变但变量可变。当你对$m2的元素赋值时Elvish 把它转成对$m2自身的赋值set m2[foo] quux # is just syntax sugar for: set m2 (assoc $m2 foo quux)这类支持廉价创建微调版本的不可变数据结构称为持久化数据结构persistent data structures常用于函数式编程语言。而 Elvish 把$m2[foo]的元素赋值改写为对$m2自身的赋值这种处理方式在同类语言中似乎是新思路。小结Elvish 的四组独特语义环环相扣共同塑造了它完整编程语言的定位结构化 IO让列表、映射成为一等公民并能跨管道、跨命令边界流动异常取代退出状态让错误处理从事后检查$?变成出错即中断配合if与?()实现更安全的控制流三阶段执行模型保证了解析与编译期的错误绝不会留下半执行的副作用而赋值即复制 持久化数据结构则在不牺牲性能的前提下带来了引用共享语言难以提供的可预测性。理解这些语义是写出地道、安全 Elvish 脚本的前提也是进一步研读 pkg/eval 与 pkg/persistent 源码的起点。赞分享CLI编程语言开发工具【免费下载链接】elvishPowerful scripting language versatile interactive shell项目地址https://gitcode.com/gh_mirrors/el/elvish点击查看免费下载相关推荐Mindustry 数据持久化方案存档文件结构与加载机制深度解析Mindustry 数据持久化方案存档文件结构与加载机制深度解析 Mindustry 作为一款优秀的自动化塔防RTS游戏其数据持久化机制是游戏体验的核心。本游戏开发Redis面试专题数据结构与持久化深度解析Redis面试专题数据结构与持久化深度解析 前言为什么Redis在技术面试中如此重要 在当今的高并发、大数据时代Redis作为高性能的内存数据库教程知识库Elvish 0.10 技术解读告别 SQLite 走向纯 Go持久化数据结构重塑赋值语义Elvish 0.10 技术解读告别 SQLite 走向纯 Go持久化数据结构重塑赋值语义 导读 本文以 Elvish 官方第二期 NewsletterCLI编程语言开发工具上一篇终极图片格式转换指南如何用docker-icloudpd实现HEIC到WebP批量处理下一篇《Go语言高级编程》实战基于 Protobuf 定制代码生成插件为 Go 标准库 net/rpc 自动生成安全接口创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考