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 build | release | [3/3] 打包 release |
| just build debug | debug | [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 耗时(毫秒) |
|---|---|
| 10 | 139.0 |
| 50 | 150.5 |
| 100 | 138.2 |
| 300 | 139.6 |
从 10 条到 300 条,耗时都在 139 毫秒上下,说明解析这个文件本身几乎不花时间。真正的时间在进程启动:
| 场景 | 耗时(毫秒) | 备注 |
|---|---|---|
| cmd /c exit 基线 | 140.7 | 不经过 just |
| just noop 单次 | 320.3 | 20 次平均 |
| 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 下没法给真实数据。










