用 just 组织和管理常用命令集

在日常使用电脑的过程中,经常会用到一些命令,并且可能要将一组命令结合才能完成自己想要的结果。如果常用的一些命令(组合)每次使用都重新敲的话,势必会影响效率。此时你可能会想到类似于 make 这样的工具,当然这也是一个选择,但是 make 的缺点也是显而易见的(有兴趣的可以通过 makefile 介绍 — 跟我一起写 Makefile 1.0 文档 来入门)。几个月前我无意中发现了just,感觉在日常使用中完全可以替代 make。

just 语法和 make 很类似,如果你有 make 基础的话,可以很快上手。下面简单介绍一下 just 的用法以及和 make 的一些区别(首先一个区别是 Makefile 中每个执行项叫做目标(target),而 just 中叫做配方(recipe)):

  • 首先 just 和 make 一样,需要一个 Justfile(make 是 Makefile,just 这个文件的文件名可以是 Justfile、justfile 或者 .justfile 等),不区分大小写,可以在前面加个.来隐藏这个文件。just 会从当前目录向上查找 justfile 文件,而 make 不会。 这就意味着只有某一个父级目录存在一个 justfile 文件,那就可以执行该 justfile 里的相关的代码。

  • 如果 Makefile 的在当前目录下存在一个文件,这个文件和某个 target 名称相同,则这个 target 就不会执行,并且打印出错误信息:make: xxx' is up to date.,此时你需要在 Makefile 文件中加上一样 .PHONY xxx 来防止此问题的发生,just 则没有此问题。

  • Makefile 中 target 可以对另一个 target 进行依赖(即先执行另一个 target,执行完毕后在执行当前的 target),just 也可以,而且 just 还有“后依赖”,例如:

1
2
3
4
5
6
list_file := "formulae-list.json"

# generate installed homebrew formulae list json file
@generate-list: && preview
brew leaves | xargs brew info --json | \
jq '[.[]|{"name", "desc", "homepage", "tap", "caveats", "linked_keg"}]' > {{list_file}}

在执行了 just generate-list 之后,会执行 preview 这个 recipe。

1
2
recipe P='D':
echo {{P}}

上面的例子中 recipe 有一个参数 P,并且有个默认值 D,可以在下面的命令中用 {{P}} 来使用这个参数(使用的是mustache来进行变量替换),除此之外还可以用 *P代表该参数有 0 到多个值,+P 表示 1 到多个值。调用的时候可以用 just recipe E 就可以改变 P 的值为 E

  • just 可以通过 set shell := ["any-available-shell", "-c"] 的方式来改变默认的 shell;另外还可以加载 .env 文件的环境变量,只需要在 justfile 文件中加入 set dotenv-load 即可。

  • just 默认的情况下每一行是一条命令(想要写成多行需要在每行的行尾加上 \,使用的是默认的 shell 来执行),但是你可以通过 Shebang 来执行不同的脚本,这要看你想使用什么脚本,sh、bash、zsh、ksh、fish、python、ruby……一切支持 Shebang 的脚本,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
push-changes:
#!/usr/bin/env fish
set -e IFS
set MSG (./scripts/list-diff.py)
if test -z $MSG
echo "Nothing to commit"
exit 0
end

set DS (date +'%Y-%m-%d %H:%M:%S')
set HEADLINE "Updates ($DS)"
set COMMIT_MSG """$HEADLINE

$MSG
"""
git commit -am "$COMMIT_MSG"
git push

再如可以使用 Python 脚本写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 数一下当前 iOS 使用了多少个 pods
[no-cd, no-exit-message]
count-pods:
#!/usr/bin/env python3
import os, sys
try:
import yaml
except ImportError:
sys.stderr.write('Please install pyyaml first')
sys.stderr.flush()
exit(1)

file = 'Podfile.lock'
if not os.path.exists(file):
sys.stderr.write('Podfile.lock not found')
sys.stderr.flush()
exit(1)

with open(file) as f:
contents = yaml.load(f, yaml.Loader)
print(len(contents['SPEC CHECKSUMS']))
  • just 默认是会打印每条命令的,可以在每条命令前加上 @ 来取消打印(这一点和 make 一样),如果想要整个 recipe 不打印任何执行的命令,只需要在这个 recipe 前面加上 @ 即可。但是有个问题,不知道是不是 bug,就是如果你在有 Shebang 的 recipe 前加上 @ 的话反而会把整个 recipe 打印出来。

  • just 支持私有的 recipe,可以在 recipe 前加上一个 _ 或者在 recipe 的上方加上 [private] 属性让其变成私有;如果一个 recipe 设置为私有,则在执行 just -l 时就不会被列出来。

    just -l 有些类似于大部分命令的 --help 参数,它会列出来所有非私有的 recipe,后面跟着每个 recipe 的注释;另外还可以通过 just --choose 交互的执行相应的 recipe(会结合 fzf

  • just 中默认会选择 justfile 文件中的第一个 recipe 来作为默认的 recipe,也可以通过 default recipe 来指定,例如:

1
2
3
4
5
6
7
8
9
10
# 列出所有可用的 recipe
default:
just -l

# 或者另外一个
# 生成所有列表
@default:
just generate-list > /dev/null
just generate-cask-list > /dev/null
just generate-crate-list

执行默认 recipe 不需要额外的参数,只需要执行 just 即可。

  • justfile 中可以通过 variable_name := variable 的形式来设置变量,可以在执行 recipe 的时候传入不同的变量,例如:
1
2
3
4
name := "world"

@hello:
echo Hello {{ name }}

这里定义了一个变量为 name,默认值为字符串 world

1
2
just hello # 会打印出 Hello world
just hello name=Logan # 会打印出 Hello Logan
  • 一些常见的 recipe 属性

    • [private]:将 recipe 标记为私有
    • [no-exit-message]:如果配方执行失败,不要打印错误信息
    • [no-cd]:在执行配方之前不要改变目录。这里需要注意,默认情况下 just 在执行 recipe 的时候会将当前目录改变到 justfile 所在的目录,如果加上了此属性则不会改变目录,命令执行时当前的目录(pwd)为执行 just 时所在的目录。
    • 另外还有一些只有在指定操作系统下 recipe 才有效的属性,如:[linux][macos] 等,详情请见 just 的文档。

just 在日常还是很好用的(结合 fzf、自己喜欢的脚本等可以实现出很有意思的功能),但是终究无法代替 make,毕竟两者的定位不同。just 重在命令的管理和组织,而 make 在是定位为编译工具,在 C/C++ 的编译方面无法撼动。这篇文章一个月前就想写了,想来想去也不知道如何着手,后来想想其实也算只是推荐而已,更详细的看说明 - Just 用户指南足够了。我在平时的工作和生活中已经大量运用 just 来帮助我做一些事情了。除了一些官方和一些开源的 justfile 示例外,也可以看一下我自己写的一个小玩意儿里的 Justfile。工作中用到的一些 recipe 就不方便分享出来了,但是如果有什么问题也欢迎留言交流。