用 just 管理终端命令,使“命令即代码”

50 阅读7分钟

just 受 make 这个老牌编排工具启发,专注于命令集的管理,适合用于记录和运行开发阶段的命令,上手容易,开发活跃,推荐做开发的朋友使用。

GitHub: casey/just: 🤖 Just a command runner (github.com)

官方提供了详细又容易理解的语法文档,可以到 GitHub 首页查看。因此本文不会讲语法,主要是传递“命令即代码”的理念和分享我的真实用例。

受限于篇幅,一些不影响了解 just 的内容会被缩减

安装

官方文档: github.com/casey/just#…

下面是 Linux 上的通用安装命令(安装位置 /usr/local/bin 可以根据需要改)

curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | sudo bash -s -- --to /usr/local/bin
just --help

定义命令(在 justfile 里也叫 recipe,配方),就是创建一个名为 justfile 的文件,这个文件可以放到任意目录下,在执行 just 时,会从当前目录开始寻找,没有就一层层向根目录方向寻找,直到找到最近的一个。

用法直接看文档,中文文档更新不及时,第一次可以花 45min 看一遍中文文档,后面最好还是以英文为主。

解决的开发痛点

以 Python Django 项目开发为例。

使用 just 前:

  • 每次启动开发服务器,都需要复制一遍 uv run manage.py runserver 0.0.0.0:7456 粘贴到终端运行。
  • 每次运行测试,都需要执行 PROJECT_CONFIG_FILE=Navigation/fixtures/test_config.yaml uv run manage.py test Navigation ,这里除了测试命令,还需要通过环境变量指定测试环境

使用 just 后。

  • 启动开发服务器只需要输入 just run 后回车。实际使用上利用补全,整个过程可以非常快速自然:【输入 ju】-【 tab 】-【点击 r】-【 tab 】-【回车】,一秒左右。
  • 运行测试,输入 just test Navigation 即可。

just 封装了命令的细节,使执行某个操作更像是按一个按钮,而不是手动排线后上电,长度更短则减少了 token 输入,给大脑留出更多上下文。封装本身,又能把开发和调试命令两个阶段分开,开发看的是整个项目、调试命令就只关注 justfile 文件,分区更明确。

真实用例

单一个 just 不带命令名,会运行 justfile 里第一个命令,我一般会把这个留给启动命令 run,如果是一个包,我会留给测试命令 test。

git 命令

我习惯这样使用 git。

在写代码之前,先创建一个空的提交,填上这次的需求类型和简单描述。使用 git commit 时可以指定一个 .gitmessage 文件,这样会携带上其中的内容作为注释的模板。

# (必填)提交类型: feat|fix|docs|style|refactor|perf|test|build|dev|config|chore|revert 和简述,模块可选
# <type>[optional scope]: <description>
:

当写完一部分代码后,使用 amend 把当前的变更添加到 commit 里,然后再写新的代码,再继续 amend。

将这两个做成 recipe 放到 justfile 里,就是 just newjust amend

root := justfile_directory()

# 创建一个空 commit ,带提交信息模板
@new:
  cd {{root}} && git commit --allow-empty --edit --file={{root}}/.gitmessage

# 将所有文件 amend,若取消则将暂存区文件移出
@amend:
  cd "{{root}}" && git add . && git commit --amend || (git restore --staged . && false)

@forget:
  if git diff --quiet HEAD HEAD~1; then \
    git reset --soft HEAD~1 && echo "✅ 空提交已删除"; \
  fi

如果使用 just new 后悔了,可以用 just forget 取消这次空提交。

Django 项目命令

我使用 monorepo 结构,即在一个仓库存放多个包和项目,因此仓库根目录 root 是在当前项目的上上级目录,虚拟环境也在那。可以在 justfile 里先把所有路径设置好。

root := justfile_directory() / "../.."
python_environment := root / ".venv"
python_interpreter := python_environment / "bin/python"
uvicorn := python_environment / "bin/uvicorn"

@run:
  just _echo_url
  cd "{{justfile_directory()}}" && {{python_interpreter}} manage.py runserver 0.0.0.0:8535

@_echo_url:
  echo "网址: http://localhost:8535/"
  echo "后台网址: http://localhost:8535/admin/"

@watch_and_run:
  just _echo_url
  cd "{{justfile_directory()}}" && {{python_interpreter}} manage.py tailwind runserver 0.0.0.0:8535

build_tailwind:
  cd "{{justfile_directory()}}" && {{python_interpreter}} manage.py tailwind build --force

@shell:
  cd "{{justfile_directory()}}" && {{python_interpreter}} manage.py shell_plus

@test app="Navigation":
  ALLABOUTME_CONFIG_FILE={{app}}/fixtures/test_config.yaml uv run manage.py test {{app}}

熟悉 Django 的,上面的命令应该都能看明白,就不再多说,

有一点值得提一下。just run 里先执行了 just _echo_url,echo_url 就是显示项目常用的网址,我感觉这个很实用,启动后点击网址就能直接打开浏览器查看,很方便。放在之前,我总是到浏览器手动输入网址。

flutter 项目命令

flutter 上,管理命令更为必要,因为 dart 支持编译期的代码裁剪,只要 if 后面跟的是常量 false 就能把整块代码删除,这对于控制开发版本和发布版本非常有用。在编译不同版本时,通过传参改变常量的值,使用 just 把这一长串命令封装为几个简单的单词。

简单介绍一下下面值得说的:

  • app_version 和 app_build,从 pubspec.yaml 里拿到版本号,在编译时作为常量传入,这样就能在 app 里显示版本号,并且是常量而非动态获取。
  • start_api 和 stop_api,在测试时,需要启动服务端,这里通过 just 的语法,确保在执行 just test 时,先执行 start_api ,在正常退出后,再执行 stop_api,完全自动化。
  • app_release_path,在本地编译完文件后,由于编译产物在很深的目录里,手动打开就很心烦,所以在 just build 中在编译好之后通过 Windows 的 explorer.exe 直接打开对应目录,非常巴适。
  • HTTP_PROXY,flutter 的很多命令,会强制在开始时检查包版本,而中国的网络会让这个检查超级慢,乃至失败,所以在这些命令前使用环境变量设置代理地址,再也不用操心了。
root := justfile_directory()
build_dir := join(root, "build/app/outputs/flutter-apk")
app_release_path := join(build_dir, "app-release.apk")
app_version := `grep '^version:' pubspec.yaml | cut -d ' ' -f2 | cut -d '+' -f1`
app_build := `grep '^version:' pubspec.yaml | cut -d ' ' -f2 | cut -d '+' -f2`

HTTP_PROXY := "http://192.168.2.205:10808"

set positional-arguments

# === 项目配方 ===

@run:
  flutter run --dart-define=APP_VERSION={{app_version}} --dart-define=APP_BUILD_NUMBER={{app_build}}

# 指定具体文件 just test test/model/plan/trigger_model_test.dart
@test *args: start_api && stop_api
  flutter test "$@" || true

@build:
  HTTP_PROXY="{{HTTP_PROXY}}" HTTPS_PROXY="{{HTTP_PROXY}}" just inner_release_build "true"
  if [ -f "{{app_release_path}}" ]; then \
      /mnt/c/Windows/explorer.exe "$(wslpath -w {{build_dir}})" || true; \
  else \
      echo "APK not found!" >&2; exit 1; \
  fi

@inner_release_build in_dev="false":
  flutter build apk \
    --target-platform android-arm64 \
    --release \
    --obfuscate \
    --split-debug-info=build/debug-info \
    --dart-define=APP_VERSION={{app_version}} \
    --dart-define=APP_BUILD_NUMBER={{app_build}}

@start_api:
  cd ~/pythonServe/plantodo_sync/services/sync && just > api.log 2>&1 &

@stop_api:
  kill $(ss -tunlp | grep ':25842 ' | awk -F'[=,]' '{print $3}')

说到 Flutter,我最近上架了一个待办应用 —— PlanTodo,支持全平台,可以到官网查看下载链接和介绍: plantodo.app/ 。相比其他待办应用,它支持更强大的循环任务(计划),常规周期之外,还能指定:每月最后一个周六、联动触发等模式。

AI

我目前使用 Claude Code 加国内的 coding plan,通过环境变量控制 Claude Code 使用自定义的端点网址和模型。

@ai model="glm-5" args="":
  HTTP_PROXY="{{HTTP_PROXY}}" HTTPS_PROXY="{{HTTP_PROXY}}" \
  ANTHROPIC_BASE_URL="https://coding.dashscope.aliyuncs.com/apps/anthropic" \
  ANTHROPIC_AUTH_TOKEN="sk-sp-123456789987654321xxcbxxxxx" \
  API_TIMEOUT_MS=3000000 \
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
  ANTHROPIC_MODEL="{{model}}" \
  ANTHROPIC_SMALL_FAST_MODEL="{{model}}" \
  ANTHROPIC_DEFAULT_SONNET_MODEL="{{model}}" \
  ANTHROPIC_DEFAULT_OPUS_MODEL="{{model}}" \
  ANTHROPIC_DEFAULT_HAIKU_MODEL="{{model}}" \
  CLAUDE_CONFIG_DIR="$HOME/claude-model/.project_claude" \
  claude {{args}}

@aicontinue:
  just ai "glm-5" --continue

有时候退出后希望返回上次的对话里,就使用 just aicontinue


另外,我会在 CLAUDE.md 或 AGENT.md 中,明确说明使用定义的 just 命令。

这样既能约束其行为避免意外操作,又能在 just 里设置好所有内容,确保运行成功。以 test 为例,使用 just 确保测试前启动、测试后关闭,AI 完全不需要关心后端的存在。


原文链接: yanh.tech/2026/04/jus…

版权声明:本博客所有文章除特別声明外,均为 AhFei 原创,采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技焉洲 (yanh.tech)