设计原则
parsh 没有正式的宣言。下面这些是实现在反复回归的原则,尽量直白地写出来:
用表达式、值和明确的边界,替代 shell 的字符串约定; 用一点啰嗦,换回可预测性。
一种语言,两个入口
Section titled “一种语言,两个入口”提示符是一个省略了左括号的表达式。$ () 等待 echo hi,实际求值的是 (echo hi)。
交互输入和脚本共用同一个 reader、同一个求值器和同一套诊断,因此不存在需要额外学习、
也不存在需要保持同步的第二套命令行语法。
注释遵循同样的规则:(; ...) 是一个可以嵌套、遵守括号配平的形式,
而不是到行尾就结束的行注释。细节见语法与值。
文本优先,类型显式
Section titled “文本优先,类型显式”裸词就是文本。123 是文本 123,true、false 和 nil 在值位置没有字面量含义。
通过 #123、#false、#nil 等字面值显式构造类型值;算术和数值比较只接受数值。
动态转换使用 int、float、bool:
(eq 001 1)(= #001 #1)(let mode fast)(if (eq $mode fast) (echo fast))条件必须产生布尔值或命令状态。文本永远不会被隐式当作真值。
不做隐式转换
Section titled “不做隐式转换”文本永远是一个参数,列表只向外层命令参数展开一层。通配是一次调用,而不是一条语法规则:
(let files (list "a file.rs" "b.rs"))(printf "<%s>\n" $files)(for file (glob "*.rs") (echo $file))重点在于:脚本写的是什么,执行的就是什么——没有隐式拆词,没有隐藏的路径展开, 也不需要猜测空格在调用点意味着什么。
命令是表达式,程序是黑盒
Section titled “命令是表达式,程序是黑盒”一个表达式可以调用内置形式、宏、函数、结构体构造器或外部程序。外部程序直接启动: 表达式不会再交给另一个 Shell 解析,因此没有第二轮引号处理,也没有注入层。
命令结果是值。命令出现在值位置时,stdout 按 UTF-8 文本捕获并去掉尾部换行;
二进制输出走重定向或管道;退出状态是一个 status 值。普通非零状态会中止当前批次,
但条件与显式捕获可以检查它。完整结果保留退出和信号信息,异常机制用于恢复执行。参见命令与组合。
值语义与词法作用域
Section titled “值语义与词法作用域”读取、赋值和传参都会拷贝值,包括嵌套的列表和映射。集合更新返回新值,而不是修改旧值:
(let config {mode fast})(set config (map/set $config mode slow))函数值的副本保留相同身份和捕获环境;普通数据的集合更新仍返回新值。
每个括号表达式创建新的词法作用域。let 把绑定写入当前表达式的外围作用域,
同一作用域重复绑定会报错;set 更新最近的已有绑定并返回 nil。函数捕获定义处的作用域,
循环每一轮创建新的作用域。这让“某个值在哪里被改过”这类问题消失,
也让结构体和宏在组合时不必担心别名带来的意外。
结构先于约定
Section titled “结构先于约定”列表、映射、结构体、管道和重定向都是表达式。type 在不执行的情况下报告一个名字如何解析,
补全覆盖命令、路径、变量、结构体成员和宏:
(defstruct Point x y)(type Point.x)(| (find . -name "*.rs") (sort))目标是让人和工具去读结构,而不是猜字符串。同一个性质也让生成的脚本更容易审阅: 只有一套语法、没有引号层,脚本读起来就是它执行的样子。参见结构与宏。
有边界,也写进文档
Section titled “有边界,也写进文档”这门语言会说明自己还没做什么。管道阶段只接受外部命令;宏没有卫生性;
嵌套的 quasiquote 原样拷贝;原生后台任务使用隔离解释器线程,库导入属于后续工作。退出码和诊断位置稳定到可以依赖:
语言错误用 2,找不到程序用 127,权限不足用 126。参见错误与退出码。
限制是被写下来的,而不是被发现的。
- 用空格和引号携带结构。
- 把文本当成真值。
- 隐式拆词和隐式路径展开。
- 把表达式交给另一个 Shell 再解析一遍。
- 把未文档化的行为当作兼容性。
这些选择并非没有成本:
- 一次性命令更啰嗦:
(if (eq $x y) ...)而不是[ "$x" = y ]。 - 标量字面值需要
#前缀;(echo 1)仍传递文本。 - 以
#开头的字面文本需要加引号。 - 值语义让大集合的传递有拷贝成本。
- POSIX 习惯不能直接套用:
for f in *.rs要写成(for f (glob "*.rs"))。
这个交换是刻意的:多敲几个字符,换来一个行为可以从纸面上读出来的脚本。