文章使用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.141.0e10
  • 布尔值只有 truefalse 两个值,可用于条件表达式和断言。
  • null 是一个单例值,常用于表示"无"或"缺省"。

Nix 提供了内建类型检查函数,如 builtins.isIntbuiltins.isFloatbuiltins.isBoolbuiltins.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
    }

r1r2 的值完全相同,但 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 属性的属性集。对于整数等类型,需要使用 toStringbuiltins.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."

注意 L2a+b+ 两侧没有空格,这与 L1a +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 语言的更多高级特性(如函数定义、模式匹配参数、内建函数、推导等)将在后续文章中继续探讨。


参考文档: Nix 2.34 Reference Manual