Mask: 用 Markdown 革新命令行任务自动化
mask
是一个命令行任务运行器,它通过一个简单的 Markdown 文件来定义。它会在当前目录中搜索 maskfile.md
,然后解析该文件中的命令和参数。
一个 maskfile.md
不仅是一份人类可读的文档,同时也是一份命令定义!以文档为中心的特性使得其他人可以通过简单地阅读你的 maskfile.md
来轻松开始你的项目开发设置。使用 Markdown 的一个好处是,许多编辑器和渲染器(比如 GitHub 本身)都内置了代码块的语法高亮。
以下是 mask
自身使用的 maskfile.md[1] 作为示例!
任务
mask 的开发任务。
运行开发模式 (maskfile_command)
构建并运行 mask 的开发模式
注意: 使用 cargo run
构建并运行 mask 的开发模式。当前目录中必须存在一个 maskfile
(此文件),并且必须提供该 maskfile
的有效命令 (maskfile_command
) 以测试你对 mask 所做的更改。由于目前只能针对此 maskfile
进行测试,你可以在底部添加子命令并运行这些子命令,而不是运行现有命令。
示例: mask run "test -h"
- 输出此 test
命令的帮助信息
选项
watch 标志:-w --watch 描述:文件变更时重新构建
if [[ $watch == "true" ]]; then
watchexec --exts rs --restart "cargo run -- $maskfile_command"
else
cargo run -- $maskfile_command
fi
注意: 在 Windows 平台上,mask 默认回退到运行 powershell
代码块。
param (
$maskfile_command = $env:maskfile_command,
$watch = $env:watch
)
$cargo_cmd = "cargo run -- $maskfile_command"
$extra_args = "--exts rs --restart $cargo_cmd"
if ($watch) {
Start-Process watchexec -ArgumentList $extra_args -NoNewWindow -PassThru
} else {
cargo run -- $maskfile_command
}
构建
构建 mask 的发布版本
cargo build --release
cargo build --release
链接
构建 mask 并用它替换全局安装的版本以进行测试
cargo install --force --path ./mask
[Diagnostics.Process]::Start("cargo", "install --force --path ./mask").WaitForExit()
测试
运行所有测试
选项
file 标志:-f --file 类型:string 描述:只从特定文件名运行测试
extra_args=""
if [[ "$verbose" == "true" ]]; then
# 线性运行测试并使日志在输出中可见
extra_args="-- --nocapture --test-threads=1"
fi
echo "Running tests..."
if [[ -z "$file" ]]; then
# 默认运行所有测试
cargo test $extra_args
else
# 测试特定的集成文件名
cargo test --test $file $extra_args
fi
echo "Tests passed!"
param (
$file = $env:file
)
$extra_args = ""
$verbose = $env:verbose
if ($verbose) {
$extra_args = "-- --nocapture --test-threads=1"
}
Write-Output "Running tests..."
if (!$file) {
cargo test $extra_args
} else {
cargo test --test $file $extra_args
}
Write-Output "Tests passed!"
格式化
格式化所有源文件
选项
check 标志:-c --check 描述:显示哪些文件格式不正确
if [[ $check == "true" ]]; then
cargo fmt --all -- --check
else
cargo fmt
fi
param (
$check = $env:check
)
if ($check) {
cargo fmt --all -- --check
} else {
cargo fmt
}
检查
使用 clippy 对项目进行静态代码检查
cargo clippy
要开始使用,请按照下面的指南操作,或者查看 mask
拥有的更多高级特性[2],比如位置参数、可选标志、子命令、其他脚本运行时等!
安装
预编译的二进制文件
前往 [Releases 页面][releases] 查看最新发布的版本。在 Assets 下,你会看到可供下载的 Linux、macOS 和 Windows 的 zip 文件。下载后,你可以解压它们,然后将 mask
二进制文件移动到 $PATH
中的某个可访问位置,如 mv mask /usr/local/bin
。
Homebrew
mask
可在 [Homebrew][homebrew] 中获取,你可以通过 brew install mask
来安装它。
Cargo
mask
发布在 [crates.io][crate] 上,你可以通过 cargo install mask
来安装它。
从源代码构建
如果你更喜欢从源代码构建,克隆这个仓库,然后运行 cargo build --release
。
开始使用
首先,在你的项目中定义一个简单的 maskfile.md
。
# 我的项目任务
## build
> 构建我的项目
```sh
echo "正在构建项目..."
test
测试我的项目
你可以在任何地方编写文档。只有特定类型的 Markdown 模式 被解析以确定命令结构。
下面定义为 js 的代码块意味着它将使用 node 运行。Mask 还 支持其他脚本运行时,包括 python、ruby 和 php!
console.log("正在运行测试...")
然后,尝试运行你的命令!
```sh
mask build
mask test
特性
位置参数
这些参数定义在命令名旁边的(圆括号)内。它们是必需的参数,必须提供这些参数命令才能运行。[可选参数][2]即将推出。参数名称作为环境变量注入到脚本的作用域中。
示例:
## test (file) (test_case)
> 运行测试
```bash
echo "正在 $file 中测试 $test_case"
### 命名标志
你可以为你的命令定义一系列命名标志。标志名称作为环境变量注入到脚本的作用域中。
**示例:**
```markdown
## serve
> 服务这个目录
<!-- 你必须在标志列表之前定义 OPTIONS -->
**OPTIONS**
- port
- 标志:-p --port
- 类型:string
- 描述:在哪个端口上服务
```sh
PORT=${port:-8080} # 如果没有提供,则设置回退端口
if [[ "$verbose" == "true" ]]; then
echo "正在 PORT: $PORT 上启动 http 服务器"
fi
python -m SimpleHTTPServer $PORT
你也可以通过将标志的 `type` 设置为 `number` 来让你的标志期望一个数值。这意味着 `mask` 将自动为你验证它是否为数字。如果验证失败,`mask` 将以有用的错误消息退出。
**示例:**
```markdown
## purchase (price)
> 计算某物的总价格。
**OPTIONS**
- tax
- 标志:-t --tax
- 类型:number
- 描述:税是多少?
```sh
TAX=${tax:-1} # 如果没有提供,则回退到 1
echo "总计:$(($price * $TAX))"
如果你省略了 `type` 字段,`mask` 将把它当作一个 `boolean` 标志。如果传递了标志,其环境变量将是 `"true"`,否则它将不被设置/不存在。
重要的是要注意,`mask` 会自动注入一个非常常见的 `boolean` 标志 `verbose` 到每一个命令中,即使你没有使用它,这也为你节省了一些打字的工作。这意味着每个命令隐含地已经有一个 `-v` 和 `--verbose` 标志了。
**示例:**
```markdown
## test
> 运行测试套件
**OPTIONS**
- watch
- 标志:-w --watch
- 描述:文件变更时运行测试
```bash
[[ "$watch" == "true" ]] && echo "以监视模式启动..."
[[ "$verbose" == "true" ]] && echo "使用额外日志运行..."
标志默认是可选的。如果你在标志定义中添加了 `required`,`mask` 会在用户没有提供它时出错。
**示例:**
```markdown
## ping
**OPTIONS**
- domain
- 标志:-d --domain
- 类型:string
- 描述:要 ping 的域名
- 必需
```sh
ping $domain
### 子命令
由于它们简单地通过 Markdown 标题的级别来定义,所以可以轻松创建嵌套的命令结构。H2 (`##`) 是你定义顶级命令的地方。之后的每一级都是一个子命令。
**示例:**
```markdown
## services
> 与启动和停止服务相关的命令
### services start (service_name)
> 启动一个服务。
```bash
echo "正在启动服务 $service_name"
services stop (service_name)
停止一个服务。
echo "正在停止服务 $service_name"
你可能注意到上面的 `start` 和 `stop` 命令都带有它们的父命令 `services` 的前缀。在某些情况下,用祖先命令来前缀子命令可能有助于可读性,但这是完全可选的。下面的示例与上面的相同,但没有前缀。
**示例:**
```markdown
## services
> 与启动和停止服务相关的命令
### start (service_name)
> 启动一个服务。
```bash
echo "正在启动服务 $service_name"
stop (service_name)
停止一个服务。
echo "正在停止服务 $service_name"
### 支持其他脚本运行时
除了 shell/bash 脚本,`mask` 还支持使用 node、python、ruby 和 php 作为脚本运行时。这让你有自由选择适合手头特定任务的正确工具。例如,假设你有一个 `serve` 命令和一个 `snapshot` 命令。你可以选择 python 来 `serve` 一个简单的目录,也许选择 node 来运行一个 puppeteer 脚本,为每个页面生成一个 png `snapshot`。
**示例:**
```markdown
## shell (name)
> 一个示例 shell 脚本
有效的语言代码:sh, bash, zsh, fish... 任何支持 -c 的 shell
```zsh
echo "你好,$name!"
node (name)
一个示例 node 脚本
有效的语言代码:js, javascript
const { name } = process.env;
console.log(`你好,${name}!`);
python (name)
一个示例 python 脚本
有效的语言代码:py, python
import os
name = os.getenv("name", "世界")
print("你好," + name + "!")
ruby (name)
一个示例 ruby 脚本
有效的语言代码:rb, ruby
name = ENV["name"] || "世界"
puts "你好,#{name}!"
php (name)
一个示例 php 脚本
$name = getenv("name") ?: "世界";
echo "你好," . $name . "!\n";
#### Windows 支持
你甚至可以添加 powershell 或批处理代码块与 Linux/macOS 代码块一起。这取决于运行的平台,将执行正确的代码块。
**示例:**
```markdown
## link
> 构建并全局链接二进制文件
```bash
cargo install --force --path .
[Diagnostics.Process]::Start("cargo", "install --force --path .").WaitForExit()
### 自动帮助和使用输出
你不必花时间手动编写帮助信息。`mask` 使用你的命令描述和选项自动生成帮助输出。对于每个命令,它添加了 `-h, --help` 标志和一个替代的 `help <name>` 命令。
\*\*示例:
\*\*
```sh
mask services start -h
mask services start --help
mask services help start
mask help services start
所有输出相同的帮助信息:
mask-services-start
启动或重启服务。
用法:
mask services start [标志] <service_name>
标志:
-h, --help 打印帮助信息
-V, --version 打印版本信息
-v, --verbose 设置详细程度
-r, --restart 如果服务已在运行,则重启
-w, --watch 文件变更时重启服务
参数:
<service_name>
在脚本中运行 mask
如果你需要将命令链接在一起,可以很容易地在脚本中调用 mask
。但是,如果你计划使用不同的 maskfile 运行 mask[3],你应该考虑使用 $MASK
实用程序,它允许你的脚本不依赖于位置。
示例:
## bootstrap
> 安装依赖项,构建,链接,迁移数据库,然后启动应用程序
```sh
mask install
mask build
mask link
# $MASK 也可以工作。它是 `mask --maskfile <path_to_maskfile>` 的别名变量
# 它保证你的脚本即使从另一个目录调用也能正常工作。
$MASK db migrate
$MASK start
### 继承脚本的退出码
如果你的命令以错误退出,`mask` 将以其状态码退出。这允许你链接命令,它们将在第一个错误上退出。
**示例:**
```markdown
## ci
> 运行测试并检查 lint 和格式化错误
```sh
mask test \
&& mask lint \
&& mask format --check
### 使用不同的 maskfile 运行 mask
如果你在一个没有 `maskfile.md` 的目录中,但你想要引用其他地方的一个,你可以使用 `--maskfile <path_to_maskfile>` 选项。
**示例:**
```sh
mask --maskfile ~/maskfile.md <subcommand>
提示: 为此创建一个 bash 别名,以便你可以轻松地从任何地方调用
# 给它起个有趣的名字
alias wask="mask --maskfile ~/maskfile.md"
# 你可以从任何地方运行这个
wask <subcommand>
环境变量实用工具
在每个脚本的执行环境中,mask
注入了一些可能有用的环境变量助手。
$MASK
这在在脚本中运行 mask[4]时很有用。这个变量允许我们使用 $MASK command
而不是 mask --maskfile <path> command
在脚本中调用,这样它们就可以不依赖于位置(不在乎它们被调用的地方)。这对于你可能从任何地方调用的全局 maskfiles 特别方便。
$MASKFILE_DIR
这个变量是 maskfile 父目录的绝对路径。拥有父目录的可用性允许我们相对于 maskfile 本身加载文件,这在你有依赖其他外部文件的命令时可能很有用。
文档部分
如果一个标题没有代码块,它将被视为文档并被完全忽略。
示例:
## 这是一个没有脚本的标题
它作为记录像设置指南或命令可能依赖的所需依赖项
或工具的地方非常有用。
使用案例
这里有一些 mask
可能很有用的场景示例。
项目特定任务
你有一个项目,有很多随机的构建和开发脚本,或者一个笨重的 Makefile
。你想通过拥有一个单一的、可读性强的文件来简化它,让你的团队成员可以添加和修改现有任务。
全局系统实用工具
你想要一个全局的 CLI 实用工具,用于各种系统任务,例如备份目录或重命名大量文件。通过为 mask --maskfile ~/my-global-maskfile.md
制作一个 bash 别名,这是很容易实现的。
FAQ
mask
是否作为库可用?
[mask-parser][mask_parser] 包是可用的。然而,它尚未文档化,也不被认为是稳定的。
灵感来自哪里?
我绝对不是第一个想到使用 Markdown 作为 CLI 结构定义的人。
我对 make
语法的挫败感是我寻找其他选项的原因。我有一段时间使用了 [just][just],这是一个相当好的改进。我最喜欢的 just
的特性是它支持其他语言运行时,这也是为什么 mask
也有这个能力!然而,它仍然没有一些我想要的功能,比如嵌套子命令和多个可选标志。
在寻找过程中,我偶然发现了 [maid][maid],这是 mask
大部分灵感的来源。我认为使用 Markdown 作为命令定义格式,同时仍然如此易于阅读,这是非常巧妙的。
那么,为什么我选择重新造轮子而不是使用 maid
呢?首先,我更喜欢安装一个单一的二进制文件,就像 just
那样,而不是安装一个带有数百个依赖项的 npm 包。我还有一些改进 maid
的想法,这就是为什么 mask
支持多级嵌套子命令以及可选标志和位置参数。另外...我真的很想用 Rust 再构建一些东西 :)
我还需要提到 [clap][clap] 和 [pulldown-cmark][cmark],它们是 mask
的核心部分,使得创建它变得如此容易。
附录
https://github.com/jacobdeichert/mask/tree/master
maskfile.md: /maskfile.md
[2]高级特性: #features
[3]使用不同的 maskfile 运行 mask: #running-mask-with-a-different-maskfile
[4]在脚本中运行 mask: #running-mask-from-within-a-script