just 是 Rust 写的命令运行器。一个叫 justfile 的文本文件里写好每条命令,之后敲 just 名字 就能跑,不用记参数、不用进目录翻脚本。它管的是「项目里那几条反复敲的命令」,比 shell 脚本多一层依赖排序和参数校验。

下面是一个能跑通的 justfile,五条 recipe 串成一条链,实测 5 层依赖链跑完 769 毫秒。

一个文件装下一套命令

set shell := ["cmd.exe", "/c"]

name := "shiceku"
ver  := env_var_or_default("APP_VER", "0.0.0")

deps:
    @echo [1/3] 安装依赖

lint: deps
    @echo [2/3] 静态检查

build target="release": lint
    @echo [3/3] 打包 {{target}}

ci: (build "debug") (build "release")
    @echo 全部完成

just ci 一条命令就会按依赖顺序走完整个流程:先是 deps,然后 lint,再分别用 debug 和 release 各打一次包,最后收尾。缩进是四个空格,@ 前缀表示这一行不回显命令本身。

解压出一个 exe,没有安装过程

Release 页给的是单个 just.exe,解压出来放哪都能跑,不写注册表也不带运行库。默认的 shell 在 Windows 上是 cmd,所以 recipe 里的语法按 cmd 写;想换成 PowerShell 就在文件顶部改 set shell 那一行。

just --version
just 1.59.0

just --list
Available recipes:
    build target="release"
    ci
    deps
    lint
    pack *files

--list 会按字母序列出所有 recipe,带参数的一并显示,不用翻文件找。配套的还有几条:--dry-run 只打印要执行的命令不真跑,--summary 只给名字一行排开,--justfile 指定另一个文件名,--working-directory 换执行目录。帮助里列出的还有 --verbose 回显每行命令、--quiet 只输出错误、--choose 交互选命令、--dotenv-path 指定 .env 文件。

调试变量时用 --evaluate,它把文件顶部定义的值全打出来;--set 可以临时覆盖其中一个:

> just --evaluate
name := "shiceku"
ver  := "0.0.0"

> just --set name other list
项目 other 版本 0.0.0

> just --init
wrote justfile to `justfile`

--init 在空目录里生成一份模板,从零开始写的时候省掉查语法的时间。

依赖链自己排序,不重复跑

> just ci
[1/3] 安装依赖
[2/3] 静态检查
[3/3] 打包 debug
[3/3] 打包 release
全部完成

deps 被 lint 依赖,lint 又被两个 build 依赖,just ci 只把 deps 跑了一次,然后把两个 build 各自展开。这条依赖图是 just 自己算的,文件里写的顺序不影响执行顺序。

参数写进 recipe 名字后面就是默认值。just build 不传参时用 release,传了就用传的:

调用方式target 取值实际执行
just buildrelease[3/3] 打包 release
just build debugdebug[3/3] 打包 debug
just greet 世界世界你好 世界
just pack a.png b.png两个参数需要打包 a.png b.png

带 * 的参数是可变长度,just pack 后面写几个都会收到;带 + 的则要求至少给一个,如果个数是 0 就直接报错退出,不会带着空参数往下跑。

不确定一条命令会干什么的时候,先加 --dry-run 过一遍:

> just --dry-run ci
echo [1/3] 安装依赖
echo [2/3] 静态检查
echo [3/3] 打包 debug
echo [3/3] 打包 release
echo 全部完成

输出是即将执行的那几行,顺序也就是真实的执行顺序,拿来确认依赖有没有排错。

变量插值和环境变量

文件顶部定义的变量在 recipe 里用双大括号取,写文件的时候就已经替换好,不是运行时执行:

APP_VER=2.4.1 just show
shiceku-2.4.1

just show
shiceku-0.0.0

env_var_or_default 会先读环境变量,读不到才用后面的默认值。CI 里给 APP_VER 注入版本号,本地不设就得到 0.0.0,一套 justfile 两边都能跑。

它慢,而且和 recipe 数量无关

每次调用 just 都要先启动一个 cmd 去执行 recipe 里的行,这部分开销是固定的。拿不同规模的 justfile 测 --list:

recipe 数量--list 耗时(毫秒)
10139.0
50150.5
100138.2
300139.6

从 10 条到 300 条,耗时都在 139 毫秒上下,说明解析这个文件本身几乎不花时间。真正的时间在进程启动:

场景耗时(毫秒)备注
cmd /c exit 基线140.7不经过 just
just noop 单次320.320 次平均
just 自身开销179.6上面两项之差
100 次调用平均291.1总计 29.11 秒
5 层依赖链769最优 733 / 最差 801

一次调用里 just 自己花 179.6 毫秒,剩下的 140.7 毫秒是 cmd 启动的成本,任何走 cmd 的方式都躲不掉。5 层依赖链跑 769 毫秒,等于每层多付一次进程启动。

把 100 次 noop 连着跑,总共 29.11 秒。这个量级在交互式使用中感觉不到,但塞进一个几千次的循环里就是几分钟的纯开销。

四种情况它会直接停下

just nosuch
error: justfile does not contain recipe `nosuch`

just fail          # recipe 里写了 exit /b 3
error: recipe `fail` failed on line 32 with exit code 3

just a             # a 依赖 b,b 又依赖 a
error: recipe `b` has circular dependency `a -> b -> a`

just x             # 第二行缩进少两个空格
error: recipe line has inconsistent leading whitespace,
       started with `    ` but found line with `  `

名字写错报 does not contain recipe,退出码 1。recipe 里某条命令失败时,just 把那条命令的退出码原样往外传,所以 exit /b 3 会让 just 也退 3,脚本里能拿到真实失败码。

如果两个 recipe 互相依赖,它会在解析阶段就报 circular dependency,不会死循环。缩进不一致同样当场报错,而且会把两行的空格数都印出来。

文件编码也有一道坎:如果把 justfile 用记事本另存成 ANSI(GBK),just 会直接报错停下,错误是 stream did not contain valid UTF-8,一条命令都不执行。文件必须存成 UTF-8。

有一处会静默通过:只要 recipe 里所有命令都返回 0,just 就返回 0。命令执行成功但结果不符合预期时,它看不出来,校验逻辑得自己写进 recipe。

什么时候值得引入

命令超过五条、参数开始记不住。安装、检查、打包、清缓存这类动作写进 justfile,新人 clone 下来 just --list 就知道能干什么,比 README 里贴一堆命令可靠。

多个系统都要跑。macOS 和 Linux 上换 shell 是一行的事,recipe 里的命令用各自平台的写法就行,不用维护两套脚本。

如果项目里只有 1 条命令,别引入。为了 npm run build 这种一行命令加一层包装,反而多一次 291.1 毫秒的进程启动。

要按文件时间戳跳过已完成的步骤,它不行。just 每次都跑,跳不跳由 recipe 自己判断;靠 mtime 决定要不要重新编译那套逻辑是 make 的地盘。

还没测的两项:Linux 和 macOS 上的并行执行 / 与 just 自带 --watch 的配合。这两样需要在别的系统上跑,Windows 下没法给真实数据。