Simple CLI building tool and script management tool.
- 对标:
- Simple CLI building tools: argparse, click and typer
- Script management tool: Just
在图形化(GUI)软件出现之前,计算机操作员使用的是命令行(CLI)软件。两种软件本质是将人的指令交给计算机。
学术的说,在CLI中,有argument参数,不带前缀,是字符串;flag标志,通常以-
开头,通常是可选的,相当于bool值;options选项,通常以-
或者--
开头,通常后接参数。注意git add
中的add
并不属于此三者的任何一个,而是命令本身。
形象的说,命令、参数、标志和选项是人和计算机交流的语言。而围绕某一个事物的一系列修饰词中,将归纳汇总并依次连接,可形成树形结构,很多命令行工具的选项都是。
举个🌰,网易的分布式存储系统下有对集群的运维工具curve-tool
有子命令,分别是bs
(管理块存储集群)、completion
()、fs
(管理文件存储集群)、upgrade
(升级)。
使用一个子命令继续看
还有子命令,可以理解为针对块存储集群的各种操作,比如check
(检测)、create
(创建资源)、delete
(删除资源)、list
(罗列资源)等等。
继续使用一个子命令继续看
还有子命令,对于“检测”这个动作,还能继续“修饰”,到底检测什么“事物”
继续使用一个子命令继续看
至此确实没有了,开始添加flag了
可以想象,将这里所有的子命令取出来,并通过前后关系作为Parent结点和Child结点,可以形成树形结构。
再举个🌰,在完成CMU 15445 Lab过程中,需要较多命令,比如测试、格式化、打包等等。而每个动作还能细分,比如Lab分成多个Project,每个Project又分成多个Task。于是就出现了“测试Project1 Task1相关代码”这样的命令。同理,格式化和打包操作也类似。所以可形成如下树形结构(以Json表示)
{
"test": {
"p1": {
"t1": ...,
"t2": ...,
...
},
"p2": ...,
...
},
"format": ...,
"submit": ...,
}
故本项目的使用场景呼之欲出
- Simple CLI building tools: 如果命令行工具可以描述为嵌套的子命令,则该框架可以很简单的构建。
- Script management tool: 如果围绕某个项目或者在工作流中有一系列的命令,可以用该框架管理使之维护在一个文件中。同时大量的命令几乎无法记忆(比如在15445中使用的命令),通过子命令的方式使记忆它们称为可能(子命令对应一个脚本)。同时,文件主体是一个Python文件,命令作为字符串嵌入其中,同样是脚本语言,Python有较于Bash或者Powershell强的多的表意能力和跨平台性。
该工具以嵌入我的工作流中并极大的提高我的效率。
-
通过PyPI:PyPI
pip install dryads
同时会下载命令
ds
,它会在当前路径递归向上寻找dryadsfile
文件并使用Python解释器运行它。我们也建议每个项目下都有一个dryadsfile
用来管理维护该项目需要命令,原因在Just Further Ramblings。
-
命令
ds
,如上,它会在当前路径递归向上寻找dryadsfile
文件并使用Python解释器运行它。- 编译器通常通过文件后缀名识别文件类型,但是
dryadsfile
并没有文件后缀,在VSCode中,可以在.vscode/settings.json
中添加下面内容解决。"files.associations": { "dryadsfile": "python" },
- 编译器通常通过文件后缀名识别文件类型,但是
-
如果是在Linux系统,通过在脚本前添加shebang
#!/usr/bin/env python3 # -*- coding: utf-8 -*-
则可以通过
./script.py
这种很接近命令的形式使用
使用字符串表达脚本内容,而Python中“万物皆对象”,可以将函数本身作为值传递。使用dict
数据类型即可描述出树形结构,将该dict
交给框架,子命令的解析和执行交给框架即可。
下面是一个经典的例子。
# test/classic_example.py
from dryads import Dryads, DryadsContainer, DryadsFlag, run_shell_cmd
def create_python():
run_shell_cmd(f"poetry new {DryadsContainer.DryadsArg}")
def create_rust():
run_shell_cmd(f"cargo new {DryadsContainer.DryadsArg}")
CMDS = {
"echo": {
"English": "echo Hello World",
"Chinese": "echo 我可以吞下玻璃而不受到伤害",
"Math": ["echo 42", "echo 3.14"],
},
"work": {
DryadsFlag.PrefixCmd: ["cd Project"],
"build": "cd build && make -j`nproc`",
"run": "./build/bin/work",
},
"create": {
"python": [
DryadsFlag.Anchoring,
DryadsFlag.AcceptArg,
create_python,
],
"rust": [
DryadsFlag.Anchoring,
DryadsFlag.AcceptArg,
create_rust,
],
},
("-d", "--dryads"): "echo Hello Dryads",
}
Dryads(CMDS)
其中子命令env
和-h
/--help
是默认生成的
下面聊聊语法
-
以嵌套的
dict
数据结构描述树形结构 -
dict
的键只能是str
或者tuple[str]
或者DryadsFlag
(这是什么后面再聊)来描述子命令 -
叶子节点以
str
/Callable
/list[str | Callable]
类型表示具体的要执行的脚本内容。-
每个
str
类型字面量作为一个Shell脚本一起交给Shell执行,即一个字符串可以是多行的,它们是连续的。- 如果在
list[str]
中有cd
命令,列表中的其他字符串中的命令不会被影响。
- 如果在
-
-
执行:一个命令的所有子命令相当于从根到叶子的路径
-
标记
DryadsFlag
,当希望改变某些默认的行为时,以标记的方式实现。其本身是枚举量,作为键或者叶子执行修改某种行为。DryadsFlag.Anchoring
: 作为叶子的值, 表示该叶子中的命令都是以执行脚本的路径开始, 默认从脚本所在的路径开始, 例子在AnchoringDryadsFlag.AcceptArg
: 作为叶子的值, 表示该选项还接收一个可选参数, 并将参数放在变量DryadsArg中, 例子在AcceptArg, 还有两个非法的例子, AcceptArg Invalid | AcceptArg InvalidDryadsFlag.InVisible
: 作为叶子的值, 表示执行的脚本是否打印, 默认打印, 使用该标志表示不打印, 例子在InVisibleDryadsFlag.IgnoreErr
: 作为叶子的值, 表示命令执行出错后是否停止, 默认停止, 使用该标志表示不停止, 例子在IgnoreErrDryadsFlag.PrefixCmd
: 作为某个节点的键, 其值对应的脚本为子树中所有脚本的前置脚本, 例子在PrefixCmd- 该标记只能用于
dict
不能用于list
,但往往是对叶子节点list
中的一系列命令设置前置脚本,可通过再套一层dict解决。
- 该标记只能用于