文章使用opencode+glm,根据我的练习代码以及官方参考资料生成,这种文章没有必要自己写了
Nix 语言基础语法速览 @
Nix 不仅仅是一个包管理器,它还拥有一门专门为描述软件构建而设计的纯函数式领域特定语言(DSL)。Nix 语言的核心特性包括:惰性求值、动态类型、声明式、无副作用。本文将通过一系列由浅入深的代码示例,带你快速掌握 Nix 语言的基础语法。
1. 基本数据类型 @
Nix 语言支持以下基本数据类型:
{
strVar = "hello world"; # 字符串 (String)
intVar = 3; # 整数 (Integer),64 位有符号整数
floatVar = 3.1415; # 浮点数 (Float),64 位 IEEE 754
boolVar = true; # 布尔值 (Boolean):true 或 false
nullVar = null; # 空值 (Null)
}
几点值得注意:
- 整数是 64 位有符号整数,负数通过取负运算符(
-)创建。 - 浮点数遵循 IEEE 754 标准,如
3.14、1.0e10。 - 布尔值只有
true和false两个值,可用于条件表达式和断言。 - null 是一个单例值,常用于表示"无"或"缺省"。
Nix 提供了内建类型检查函数,如 builtins.isInt、builtins.isFloat、builtins.isBool、builtins.isString 等,可在运行时判断值的类型。
2. 列表(List) @
列表是 Nix 中的基本复合数据类型之一,用方括号 [ ] 包裹,元素之间以空格分隔:
listVar = [1 "tux" false];
关键特性:
- 列表是异构的,可以同时包含不同类型的元素(整数、字符串、布尔值等)。
- 元素之间用空格分隔,而非逗号。
- 如果元素是复杂表达式(如函数调用、属性集、嵌套列表),需要用括号包裹:
[ (f 1) { a = 1; b = 2; } [ "c" ] ]
- 使用
builtins.isList可以判断一个值是否为列表。
3. 属性集(Attribute Set) @
属性集是 Nix 中最核心的数据结构,类似于其他语言中的字典/对象/记录,用花括号 { } 包裹:
setVar = {
a = 1;
b = "str";
c = false;
};
3.1 点号扁平写法 @
Nix 支持一种更简洁的"扁平写法",用点号 . 表示嵌套关系。以下两种写法完全等价:
# 写法一:嵌套写法
setVar = {
a = 1;
b = "str";
c = false;
};
# 写法二:点号扁平写法
setVara.a = 1;
setVara.b = "str";
setVara.c = false;
这在定义深层嵌套的属性时特别方便:
# 以下两种写法等价
{ foo = { bar = 1; }; }
{ foo.bar = 1; }
3.2 属性访问 @
使用 .操作符 访问属性集中的属性:
{ x = 1; y = 2; }.x # 结果为 1
还可以使用 or 提供默认值,防止访问不存在的属性时报错:
{ x = 1; y = 2; }.z or 3 # 结果为 3,因为 z 不存在
3.3 递归属性集(rec) @
普通属性集内的属性不能相互引用:
{
a = 1;
b = a + 1; # 错误!普通属性集中 a 未定义
}
如果需要属性之间相互引用,需要使用 rec 关键字声明递归属性集:
rec {
a = 1;
b = a * 2 + 1; # 正确!b = 3
}
rec 使得属性集内的所有属性都在彼此的作用域内可见。这在定义相互依赖的配置时非常有用,但也需谨慎使用——过度依赖 rec 可能导致隐式耦合,降低代码可读性。
4. let 表达式 @
let...in 是 Nix 中的局部变量绑定机制,类似于其他语言中的 let/where:
let
a = 1;
b = 2;
in
a + b # 结果为 3
4.1 作用域规则 @
let 中定义的变量仅在 in 之后的表达式中可见,出了 in 就不可访问:
{
a = let x = 1; in x; # 正确,x 在 in 后的表达式中可用,a = 1
b = x; # 错误!x 在此作用域不可见
}
4.2 let 中的点号写法 @
let 同样支持点号扁平写法。以下两种写法等价:
# 写法一
let
a = { b = { c = 123; }; };
in
a.b.c # 结果为 123
# 写法二
let
a.b.c = 123;
in
a.b.c # 结果为 123
5. with 表达式 @
with 表达式将一个属性集中的所有属性添加到当前作用域,从而简化对属性集中属性的反复访问:
let
a = {
x = 1;
y = 2;
};
in
{
r1 = [ a.x a.y ]; # 不使用 with,需要写前缀 a.
r2 = with a; [x y]; # 使用 with,直接使用 x、y
}
r1 和 r2 的值完全相同,但 with 写法更简洁。
5.1 with 的就近遮蔽规则 @
重要: with 引入的属性会被外层作用域的同名变量遮蔽。也就是说,with 中的属性优先级低于 let 绑定和函数参数:
let
x = 0; # 外层定义 x = 0
a = {
x = 1; # 属性集中的 x = 1
y = 2;
};
in
{
R1 = [ a.x a.y ]; # [1 2],显式访问
R2 = with a; [ x y ]; # [0 2],x 取外层的 0,而非 a.x 的 1
}
在上例中,R2 中的 x 取的是外层 let 绑定的 0,而不是 with a 引入的 1。这是因为 with 的优先级低于词法作用域绑定。这是 Nix 中一个常见的陷阱,使用 with 时务必注意。
根据官方文档的说明,with 的遮蔽行为可以总结为:词法作用域(let 绑定、函数参数)的变量会遮蔽 with 引入的同名属性。
6. inherit 关键字 @
inherit 是 Nix 提供的一种语法糖,用于简化"从作用域中引入同名属性"的操作。
6.1 基本用法 @
以下两种写法等价:
# 写法一:手动赋值
let
a = 1;
b = 2;
x = 3;
y = 4;
in
{
m = a;
n = b;
x = x; # x = x 看起来冗余
y = y; # y = y 看起来冗余
}
# 写法二:使用 inherit
let
a = 1;
b = 2;
x = 3;
y = 4;
in
{
m = a;
n = b;
inherit x y; # 等价于 x = x; y = y;
}
6.2 从指定属性集继承 @
inherit (set) attr1 attr2; 可以从指定的属性集中提取属性:
let
a = { x = 1; y = 2; };
in
{
inherit (a) x y; # 等价于 x = a.x; y = a.y;
}
# 结果:{ x = 1; y = 2; }
6.3 在 let 中使用 inherit @
inherit 同样可以在 let 绑定中使用:
let
inherit ({ x = 1; y = 2; }) x y;
# 等价于:
# x = { x = 1; y = 2; }.x;
# y = { x = 1; y = 2; }.y;
in
[ x y ] # 结果为 [1 2]
inherit 在 Nixpkgs 中极为常见,用于将 pkgs 中的包"继承"到当前属性集,避免重复书写 pkgs. 前缀。
7. 路径(Path) @
路径是 Nix 中的一种一等公民数据类型,与字符串不同,路径值以 / 开头:
/. # 根目录,绝对路径
./. # 当前目录,相对路径(相对于包含该表达式的文件所在目录)
<nixpkgs> # 检索路径(Lookup Path),通过 $NIX_PATH 环境变量解析
关于路径的重要说明:
- 路径不能以
/结尾,Nix 会自动规范化路径(去除尾部斜杠、重复斜杠、.和..)。 - 相对路径在求值时会自动解析为绝对路径,基准目录是包含该 Nix 表达式的文件所在目录。
- 当路径通过字符串插值(如
"${./foo}")转换为字符串时,对应的文件或目录会被自动复制到 Nix Store 中,结果字符串为 Store 路径。 <nixpkgs>这种检索路径语法依赖$NIX_PATH环境变量,在现代 Nix 实践中不推荐使用,建议使用 Flakes 的flake.nix来管理依赖。
8. 字符串插值 @
字符串插值是 Nix 语言中最实用的特性之一,使用 ${表达式} 语法将表达式的值嵌入字符串中:
8.1 基本插值 @
let
name = "xxxx";
in
"hello ${name}" # 结果为 "hello xxxx"
8.2 非字符串类型的插值 @
插值的表达式必须求值为字符串、路径或具有 outPath/__toString 属性的属性集。对于整数等类型,需要使用 toString 或 builtins.toString 进行显式转换:
let
x = 1;
in
"${toString x} + ${toString x} = ${toString (x + x)}"
# 结果为 "1 + 1 = 2"
8.3 插值嵌套 @
字符串插值支持任意深度的嵌套,内层字符串中的插值会先被求值:
let
a = "pen";
b = "apple";
c = "pineapple";
in
{
L1 = "${a + " plus ${b + " equals ${c}"}"}.";
L2 = "${a+" plus ${b+" equals ${c}"}"}.";
}
# L1 和 L2 的值均为 "pen plus apple equals pineapple."
注意 L2 中 a+ 和 b+ 两侧没有空格,这与 L1 中 a + 和 b + 的写法结果完全一致——因为 + 是 Nix 的字符串拼接运算符,空格不影响运算。
9. 多行字符串(Indented String) @
Nix 提供了用两个单引号 '' '' 包裹的缩进字符串语法,特别适合书写多行文本(如 Shell 脚本、配置文件):
9.1 基本用法 @
以下两种写法等价:
# 写法一:双引号 + 转义
"Please run\n cat /etc/os-release\nto get distro info.\n"
# 写法二:缩进字符串
''
Please run
cat /etc/os-release
to get distro info.
''
缩进字符串的核心特性是自动去除公共前导空格:Nix 会计算所有非空行的最小缩进量,然后从每一行中去除该数量的前导空格。这使得你可以在 Nix 代码中自由缩进多行字符串,而不会影响最终结果。
注意: 缩进字符串不会去除前导 Tab 字符,只会去除空格。如果使用 Tab 缩进,Tab 将保留在结果中。
9.2 开头空行忽略 @
紧跟在开始 '' 后的空白行(如果该行没有非空白字符)会被忽略,这允许你将字符串内容从下一行开始书写。
10. 字符串转义 @
10.1 双引号字符串中的转义 @
在双引号字符串中,以下字符需要用反斜杠 \ 转义:
| 转义序列 | 含义 |
|---|---|
\" |
双引号 |
\\ |
反斜杠 |
\${ |
字面量 ${(防止被解析为插值) |
\n |
换行符 |
\r |
回车符 |
\t |
制表符 |
"this is a \"string\" \\" # 结果为:this is a "string" \
$${(双美元符花括号)可以原样书写,无需转义。
10.2 缩进字符串中的转义 @
缩进字符串使用不同的转义规则,更贴近 Shell 脚本书写习惯:
| 转义序列 | 含义 |
|---|---|
''$ |
字面量 $ |
''' |
字面量 ''(两个单引号) |
''${ |
字面量 ${(防止被解析为插值) |
''\n |
换行符 |
''\r |
回车符 |
''\t |
制表符 |
''\c |
字面量字符 c(转义任意字符) |
let
a = "1";
in
''the value of a is:
''${a}
''
# 结果为 "the value of a is:\n ${a}\n"
注意这里 ''${a} 被转义为字面量文本 ${a},而不是被插值为 "1"。这在书写 Shell 脚本时尤其有用——Shell 变量引用 ${VAR} 不会被 Nix 误解析。
重要: 双引号字符串中转义
${用\$,而缩进字符串中转义${用''$,两者语法不同,使用时需注意区分。
总结 @
本文覆盖了 Nix 语言最核心的语法要素:
| 语法要素 | 关键概念 |
|---|---|
| 基本类型 | 字符串、整数、浮点数、布尔值、null |
| 列表 | 空格分隔、异构、括号包裹复杂元素 |
| 属性集 | 花括号语法、点号扁平写法、rec 递归属性集 |
| let 表达式 | 局部绑定、in 后可见、作用域隔离 |
| with 表达式 | 引入属性集作用域、注意遮蔽规则 |
| inherit | 语法糖、从作用域或指定属性集继承属性 |
| 路径 | 一等类型、自动解析、检索路径 |
| 字符串插值 | ${expr} 语法、支持嵌套、非字符串需 toString |
| 多行字符串 | '' '' 语法、自动去缩进、适合脚本 |
| 转义 | 双引号 vs 缩进字符串的不同转义规则 |
掌握这些基础语法后,你就可以阅读和编写 Nix 表达式了——无论是定义软件包、编写 NixOS 配置,还是使用 Flakes 管理项目开发环境,这些语法都是不可或缺的基石。
Nix 语言的更多高级特性(如函数定义、模式匹配参数、内建函数、推导等)将在后续文章中继续探讨。