7 个版本 (4 个稳定版)

1.2.0 2023 年 1 月 15 日
1.1.0 2022 年 10 月 15 日
0.3.0 2022 年 8 月 28 日
0.2.0 2022 年 8 月 15 日
0.1.0 2022 年 7 月 30 日

#324 in 构建工具

GPL-3.0-only

200KB
4K SLoC

Yamis

build codecov License: GPL v3

适用于团队和个人的任务运行器。使用 Rust 编写。

索引

动机

该项目源于对现有工具的不满和对 Rust 学习的渴望。它旨在易于使用,并且适用于个人和团队,尤其是那些在不同平台上工作的团队。为了在不破坏团队工作流程的前提下(至少不是太多,现实点),它旨在实现向下兼容,无论是同一主要版本之间还是与之前的主要版本之间。

受到不同工具的启发,如 cargo-makedoskeybashdocker-compose

向下兼容性

从版本1.0.0开始,目标是与主要版本保持向后兼容,并遵循语义化版本控制。发布新版本时,计划通过在TOML或YAML文件中使用version键设置主要版本来支持最新版本的配置文件,例如:version: "1"。如果没有设置版本,将使用支持的最小主要版本。

安装

如果您已安装RustCargoRust安装说明),则运行

cargo install --force yamis

进阶技巧:确保~/.cargo/bin目录已添加到您的PATH环境变量中。

二进制发布

二进制文件也适用于Windows、Linux和macOS,可在发布版中找到。安装时,下载适合您系统的zip文件,解压缩,并将二进制文件复制到所需位置。您需要确保包含二进制文件的文件夹位于PATH中。

更新

运行时,如果可用新版本,将显示带有命令的消息。可以通过运行yamis --update来执行更新,这将下载并替换二进制文件。或者,可以通过在安装二进制发布版中再次遵循安装说明来更新。

请注意,程序将缓存更新信息24小时,因此无需担心每次运行时都会进行请求。

快速入门

第一步是在项目根目录中添加一个YAML或TOML文件,例如:project.yamis.yaml

以下是一个示例YAML文件,用于演示一些功能

# project.yamis.yaml
env:  # global env variables
  DEBUG: "FALSE"
  DOCKER_CONTAINER: sample_docker_container

tasks:
  _debuggable_task:
    private: true   # cannot be invoked directly
    env:
      DEBUG: "TRUE"  # Add env variables per task

  say_hi:
    help: "Just say hi"  # help message, printend when running `yamis -i say_hi`
    script: "echo Hello {name}"  # takes a name argument, i.e. `--name John`
  
  say_hi.windows:
    script: "echo Hello {name} from Windows"  # Task version for windows systems
  
  folder_content:  # Default for linux and macOS, can be individually specified like for windows.
    script: "ls {$1?}"  # Takes a single optional argument
    
    windows:  # Another way of specifying OS specific tasks
      script: "dir {$1?}"

  compose-run:
    wd: ""  # Working dir is the dir containing the config file
    program: "docker-compose"
    # `{$DOCKER_CONTAINER}` 
    args: [
      "run",
      "{$DOCKER_CONTAINER}",  # passes an environment variable into the program arguments
      "{ $@ }"  # passes all extra given arguments 
    ]

  compose-debug:
    bases: ["compose-run", "_debuggable_task"]  # Inherit from other tasks
    args+: ["{$DEBUG?}"]  # Extends args from base task. Here DEBUG is an optional environment variable

配置文件准备好后,您可以通过调用yamis、任务的名称以及任何参数来运行任务,例如:yamis say_hi --name "world"。传递相同的参数多次也会多次添加它,例如:yamis say_hi --name "person 1" --name="person 2"相当于echo Hello person 1 person 2

关于 YAML 和 TOML 文件的说明

用于处理YAML文件的解析器将值视为字符串,如果期望字符串。

例如,以下示例是等效的

env:
  DEBUG: Yes  # Normally becomes true
  AGENT: 007  # Normally becomes 7
env:
  DEBUG: "Yes"
  AGENT: "007"

然而,在TOML文件的情况下,解析器返回适当的类型,因此会导致错误。

即以下内容无效

[env]
    AGENT = 007

我们不隐式执行此转换,因为我们需要修改TOML解析器。如果在解析文件后进行转换,我们将得到AGENT=7,这可能是我们不希望看到的。

使用方法

命令行选项

可以通过运行yamis -hyamis --help来查看有关命令行选项的帮助信息。基本使用方式如下

Usage: yamis [OPTIONS] [COMMAND]

Options:
  -l, --list              Lists configuration files that can be reached from the current directory
  -t, --list-tasks        Lists tasks
  -i, --task-info <TASK>  Displays information about the given task
  -f, --file <FILE>       Search for tasks in the given file
      --update            Checks for updates and updates the binary if necessary
  -h, --help              Print help information
  -V, --version           Print version information

您可以直接通过传递任务名称及其参数来调用任务,例如 yamis say_hi --name John,或者您可以通过使用 -f 选项指定要使用的配置文件,例如 yamis -f project.yamis.yaml say_hi --name John。请注意,-f 选项必须在任务名称之前设置,否则它将被解释为任务的参数。

下一节将讨论任务文件的自动发现方式。

任务文件

任务文件必须是具有适当扩展名的 TOML 或 YAML 文件,例如 project.yamis.toml,或 project.yamis.yml。请注意,本文件中给出的示例可以是任一版本,但它们之间的转换是直接的。

调用任务时,程序将从工作目录开始,继续到根目录,按顺序查找配置文件,直到找到任务、找到 project.yamis(无论是 TOML 还是 YAML)任务文件,或者没有更多的父文件夹(到达根目录)。在这些文件名称中,大小写敏感的系统中大小写是敏感的,即 PROJECT.yamis.toml 在 Linux 上将不起作用。

配置文件(按优先级顺序,省略扩展名)的命名如下

  • local.yamis:应包含私有任务,且不应提交到仓库中。
  • yamis:应在项目的子目录中使用,用于特定于该文件夹及其子目录的任务。
  • project.yamis:应包含整个项目所需的任务。

如果任务仍未找到,它将查找 ~/.yamis/user.yamis.toml~/.yamis/user.yamis.yaml~/.yamis/user.yamis.yml 以查找用户范围的任务。这对于与特定项目无关的日常任务非常有用。

脚本

⚠️警告:请勿在脚本中将敏感信息作为参数传递。脚本存储在系统的临时目录中的一个文件中,操作系统负责删除它,但是不能保证一定会这样做。因此,任何传递的参数都将无限期地保留。

任务内部 script 的值将在命令行中执行(Windows 中默认为 cmd,Unix 中默认为 bash)。脚本可以跨多行,并包含 shell 内置程序和程序。在传递多个参数时,默认情况下它们将被展开,常见的示例是 "{ $@ }" 标签,它展开为所有传递的参数。

生成的脚本存储在临时目录中,文件名将是一个哈希值,这样如果脚本之前曾用相同的参数调用过,我们可以重用之前的文件,本质上起到缓存的作用。

自动引号

默认情况下,所有传递的参数都被引号引用(使用双引号)。这可以通过指定 quote 参数来在任务或文件级别进行更改,该参数可以是以下之一

  • always:始终引用参数(默认值)
  • spaces:如果参数包含空格则引用参数
  • never:从不引用参数

尽管引用可以防止因传递包含空格的参数而导致错误等常见错误,但它可能在某些边缘情况下失败。

替换脚本运行器

默认情况下,Windows中的脚本运行器是CMD,Unix系统中的是bash。要使用其他程序,您可以在任务中设置script_runner选项。此外,您还可以设置script_runner_args,它应该是一个包含传递给运行程序的额外参数的列表,例如,["-x"]将在bash中以调试模式运行脚本。

您可能还想覆盖script_ext(或script_extension)选项,它是一个包含脚本文件扩展名的字符串,并且可以带点或不带点。对于某些解释器,扩展名无关紧要,但对于其他解释器则不然。在Windows中,扩展名默认为cmd,Unix中为sh

示例

# Python script that prints the date and time
[tasks.hello_world]
script_runner = "python"
script_ext = "py"  # or .py
script = """
from datetime import datetime

print(datetime.now())
"""

如果经常使用此功能,使用继承来缩短任务将很有用。上面的示例可以变成

[tasks._py_script]
script_runner = "python"
script_ext = "py"  # or .py
private = true

[tasks.hello_world]
bases = ["_py_script"]
script = """
from datetime import datetime

print(datetime.now())
"""

程序

任务内部program的值将作为单独的进程执行,并通过args传递参数。请注意,每个参数最多可以包含一个标签,即{$1}{$2}是不有效的。当传递多个值时,它们将被解包为程序参数,即"$@"将导致所有参数传递给程序。

使用继承时,可以使用args_extend而不是args来扩展基本任务的参数。这对于添加额外的参数而无需重写它们非常有用。

串行运行任务

运行任务的明显选项是创建一个脚本,例如以下

yamis say_hi
yamis say_bye

另一种选项是使用serial,它应该接受一个任务列表,以顺序运行这些任务,例如

[tasks.greet]
serial = ["say_hi", "say_bye"]

请注意,任何传递的参数都将平等地传递给两个任务。

执行相同的任务或以无限循环结束是可能的。这不会被阻止,因为可以通过使用脚本来绕过。

脚本与程序

由于正确转义参数可能会很快变得非常复杂,因此如果传递某些参数,脚本可能会失败。为了防止经典错误,参数默认被引用(见自动引用),但这并不完全安全。此外,脚本保存在临时目录中,可能无限期地存在。

另一方面,程序在它们自己的进程中运行,参数直接传递给它,因此无需转义。这些也可以更容易地扩展,例如通过扩展参数。然而,缺点是我们不能执行内置的shell命令,如echo,并且我们需要将参数定义为列表。

命令行中的任务参数

任务的参数可以是键值对传递,例如--name "John Doe",或作为位置参数传递,例如"John Doe"

命名参数必须以一个或两个短横线开头,后跟一个ASCII字母或下划线,然后跟任意数量的字母、数字、-_。值将是下一个参数或等于号后面的值,即 --name "John Doe"--name-person1="John Doe"-name_person1 John 都是有效的。请注意,"--name John" 不是一个命名参数,因为它被引号包围并包含空格,然而 "--name=John" 是一个有效的命名参数。

命名参数也被视为位置参数,即如果传递 --name John --surname=Doe$1 将是 --name$2 将是 John,而 $3 将是 --surname="Doe"。因此,建议首先传递位置参数。

如果您想以原样将参数传递给程序,则它们的格式无关紧要,您可以使用 {$@} 标签,该标签将展开为所有参数。

您可以在标签表达式部分中了解更多有关任务中参数使用的信息。

标签

标签用于将动态值插入我们想要调用的程序脚本和参数中。标签可以用来插入位置参数和命名参数、环境变量(使用跨平台语法)以及调用函数。

标签内的表达式(包括函数)可以返回字符串或字符串列表。实际上,这两个数据类型可以直接在标签中使用。注意,空列表和只有一个字符串的列表不会被强制转换为字符串以避免歧义,有关更多信息,请参阅表达式部分。

整数只有在切片时才允许使用,即 {values[0]} 是有效的,但 {1} 是无效的。

在可选表达式中,没有空值,它们将简单地返回一个空字符串/列表。例如,如果 {$1?} 未传递 $1,它将返回一个空字符串。

为什么不支持更多的数据类型,比如整数?因为解析只有在返回脚本主体或程序参数时才有意义,而这两种情况都始终是字符串或字符串列表。此外,对于更复杂的操作,调用外部脚本会更有意义。

表达式

位置参数

从1开始索引,以$开头,后跟一个数字,例如{$1}{$2}。这些返回单个字符串,因此它们的切片将返回子字符串。

命名参数

区分大小写,按名称传递,例如{out}{file}等。注意,参数前的任何连字符都将被删除,例如,如果传递了--file out.txt,则{file}将接受它。有关更多信息,请参阅命令行中的任务参数部分。

这些始终返回字符串列表,因此索引切片将返回字符串,而范围切片将返回子数组。例如,{ file[0][0] }返回第一个传入的file参数的第一个字符,而file[0]将返回第一个文件参数。

所有参数

使用 { $@ } 将会按原样传递所有参数列表。例如,如果调用带有参数 -o file.txt -o=file2.txt 的任务,它将返回一个列表 ["hello", "-o", "file.txt", "-o=file2.txt"]。它们可以通过索引和切片访问,例如 { $@[0] }{ $@[0..2] } 是有效的。也可以是可选的,即 { $@? }

环境变量

$ 为前缀,例如 { $HOME }{ $PATH } 等。这些表示为字符串,因此切片将返回子字符串。不要与位置参数混淆,位置参数是数字的,即 $1,或者与所有参数语法 $@

注意,与此语法不同,环境变量是在解析脚本或程序参数时加载的,而原生语法在程序的参数中不会工作,在脚本的情况下,将由shell加载。这是故意的,以避免歧义并保持它们分开,或者使用不同解释器(如Python)的env变量。

字符串参数

字符串是另一种有效的表达式类型,但它们在函数的上下文中更为相关。字符串由单引号或双引号定义,不能包含未转义的换行符。例如,{ "\"hello\" \n 'world'" } 是一个有效的字符串。字符串也可以切片,但这只是尝试保持解析器简单而不是一个有用特性的副作用。

格式化字符串

这些只是一些特殊处理的普通字符串。例如,fmt 函数接受一个格式字符串和多个参数。字符串中每个 %s 出现都会被相同索引的参数替换。注意,在格式字符串中,% 需要被另一个 % 转义,即 %%s 将被替换为 %s

例如,{ fmt("hello %s", $1) } 将返回 hello <第一个参数>

函数

有关可用函数的列表,请查看函数部分。

预定义函数可以以不同的方式转换参数。它们可以接受值,并且可以嵌套。例如,{ join(" ", split(",", $1)) } 将根据 "," 分割第一个参数,并使用空格将它们重新连接。

目前无法定义自定义函数,因为这需要使用外部语言(如 Python)或嵌入式语言(如 Lua),或者实现一种新的编程语言。这个程序的一个目标是有简单明了的语法,因此添加支持定义函数会破坏这一点。在大多数需要执行复杂操作的情况下,最好使用单独的脚本(例如 bash 或 Python)来执行所需的操作,然后从具有适当参数的任务中调用它。不过,将来可能会添加新函数来支持灵活的参数解析操作。请随时通过在存储库中提交新问题来请求新函数。

可选表达式

默认情况下,表达式必须返回一个非空字符串或非空字符串数组,否则会引发错误。可以通过添加 ? 使表达式成为可选的,例如 { $1? }{ map("hello %s", person?)? }{ $@? }{ output? }

索引和切片

表达式,包括函数的输出,可以进行切片以增加灵活性。切片索引从0开始,接受正数和负数索引。整个表达式可以是必填的也可以是可选的,即exp[1][0]?如果exp未设置或者exp[1]超出了范围,则不会失败并返回空值。注意,像exp?[1]?[0]?这样的写法是无效的。

以下是一些参数的示例:hello world -p=1 -p=2 -p=3

表达式 结果
echo{ $@[0] } echo hello
echo{ $@[0][0] } echo h
echo{p[0] } echo1
echo{p[:2] } echo1 2
echo{p[1:] } echo2 3
echo{ $@[0:999] } echo hello world--p=1 --p=2 --p=3
echo{ $@[:-1] } echo--p=3
echo{ $@[-3:-1] } echo--p=1 --p=2
echo{ $@[990:999][0]? } echo
echo{ $@[-999]? } echo

解包

返回数组的表达式将被解包。例如,给定以下任务

[tasks.say-hi]
script = "echo hello {person}"

[tasks.something]
program = "imaginary-program"
args = ["{ map('-o %s', f) }"]  # map returns an array of strings

如果我们调用yamis hello --person John1 --person John2,它将运行echo hello John1 John2。同样地,yamis something --f out1.txt --f out2.txt将使用imaginary-program并带有["-o", "out1.txt", "-o", "out2.txt""]"参数。注意,在最后一种情况下,我们调用了一个名为函数map

设置环境变量

可以在任务级别定义环境变量。这两种形式是等价的

[tasks.echo]
env = {"DEBUG" = "TRUE"}

[tasks.echo.env]
DEBUG = "TRUE"

它们也可以全局传递

[env]
DEBUG = "TRUE"

此外,可以在任务或全局级别指定一个env文件。路径相对于配置文件,除非它是绝对路径。

env_file = ".env"

[tasks.some]
env_file = ".env_2"

如果在同一级别设置了env_fileenv选项,两者都将被加载,如果有重复的键,则env将优先。类似地,即使在任务级别设置了这些选项,也会在任务级别加载全局环境变量和env文件,任务上定义的环境变量将优于全局环境变量。

特定于操作系统(OS)的任务

可以为每个任务指定不同的OS版本。如果找不到当前OS的任务,如果存在,将回退到非OS特定的任务。即。

tasks:
  ls: # Runs if not in windows 
    script: "ls {$@?}"

  windows:  # Other options are linux and macOS
    script: "dir {$@?}"

OS任务也可以在单个键中指定,即以下示例与上面的示例等价。

tasks:
  ls: 
    script: "ls {$@?}"

  ls.windows:
    script: "dir {$@?}"

请注意,OS特定的任务不会隐式继承非OS特定的任务,如果您想这样做,您将必须显式定义基础,即。

tasks:
  ls:
    env:
      DIR: "."
    script: "ls {$DIR}"

  ls.windows:
    bases: [ls]
    script: "dir {$DIR}"

工作目录

默认情况下,任务的当前工作目录是执行它的目录。这可以通过任务级别或根级别,通过wd来改变。路径可以是相对路径或绝对路径,相对路径将相对于配置文件解析,而不是任务执行的目录,这意味着""可以被用来使工作目录与配置文件目录相同。

任务文档化

可以使用help键来对任务进行文档化。与注释不同,当运行yamis -<TASK>时,将打印帮助信息。请注意,帮助信息是继承的。如果您想删除它,可以将它设置为""

任务继承

任务可以通过添加一个bases属性从多个任务继承,这个属性应该是一个列表,列出了同一文件中的任务名称。这类似于Python等常见语言中的类继承,但并非所有值都被继承。

继承的值包括

  • wd
  • help
  • quote
  • script
  • script_runner
  • script_runner_args
  • script_ext
  • script_extensionscript_ext的别名)
  • program
  • args
  • serial
  • env(值是合并而不是覆盖)
  • env_file(值是合并而不是覆盖)

不继承的值包括

  • args_extend(添加到继承的args中,之后销毁)
  • args+args_extend的别名)
  • private

继承从下到上工作,子类在父类之前处理。不允许循环依赖,这会导致错误。

它将尝试首先找到并继承特定的os-specific任务,如果未找到,则使用常规任务。例如

tasks:
  sample.windows:
    script: "echo hello"
  
  sample:
    script: "echo hi"
  
  inherit:
    bases: [sample]

等同于

tasks:
  sample.windows:
    script: "echo hello windows"
  
  sample:
    script: "echo unix"
  
  inherit:
   script: "echo hello windows"

这样可以为每个操作系统定义基础任务,并为子任务提供一个版本。但是,请注意,特定于操作系统的任务不会隐式地从非特定于操作系统的任务继承。例如,在上面的例子中,sample.windows不会隐式地从sample继承。

扩展程序参数

可以使用args_extend或其别名args+来扩展参数。这些将给定的列表追加到从基任务继承的args

示例

tasks:
  program:
    program: "program"
    args: ["{name}"]

  program_extend:
    bases: ["program"]
    args_extend: ["{phone}"]

  other:
    env: {"KEY": "VAL"}
    args: ["{other_param}"]
    private: true  # cannot be called directly, field not inherited

  program_extend_again:
    bases: ["program_extend", "other"]
    args+: ["{address}"]  # args+ is an alias for args_extend

在上面的例子中,program_extend_again将等同于

tasks:
  program_extend_again:
    program: "program"
    env: {"KEY": "VAL"}
    args: ["{name}", "{phone}", "{address}"]

私有任务

可以通过将private = true设置为私有来标记任务。私有任务不能被用户调用,但对继承很有用。

调试选项

可以在任务或文件级别在debug_config下添加一些调试选项

  • print_file_path:布尔值,在文件级别定义,默认为false。如果为true,则在运行任务时将显示绝对配置文件路径
  • print_task_name:布尔值,在任务或文件级别定义,默认为true。如果为true,则在运行任务时将显示任务名称

函数列表

预定义函数列表。

map函数

签名: map<S: str | str[]>(fmt_string: str, values: S) -> S

将每个值映射到 fmt(fmt_string, val)

参数

示例

sample:
  quote: never
  script: |
    echo {map("'%s'", $@)}


sample2:
  program: merge_txt_files
  args: ["{map('%s.txt', $@)}"]

yamis sample person1 person2 将导致 echo hi 'person1' 'person2'

yamis sample2 file1 file2 将导致以参数 ["file1.txt", "file2.txt"] 调用 merge_txt_files

连接函数

签名: join<S: str | str[]>(join_str: str, values: S) -> str

join 的第一个参数是将在第二个参数给出的所有值之间插入的字符串,返回一个字符串。如果第二个参数是单个字符串,则按原样返回。

参数

  • join_str: 在值之间插入的字符串
  • values: 要连接的值

示例

sample:
  quote: never
  script: |
    echo hello {join(" and ", $@)}

yamis sample person1 person2 将导致 echo hi person1 and person2'

jmap 函数

签名: jmap<S: str | str[]>(fmt_string: str, values: S) -> S

join("", map(fmt_string, values)) 的快捷方式

参数

示例

sample:
  quote: never
  script: |
    echo hi{jmap(" '%s'", $@)}


sample2:
  program: some_program
  args: ["{jmap('%s,', $@)}"]

yamis sample person1 person2 将导致 echo hi 'person1' 'person2'

yamis sample2 arg1 arg2 将导致以参数 ["arg1,arg2,"] 调用 some_program

fmt 函数

签名: fmt(fmt_string: str, *args: str) -> str

fmt的第一个参数是格式字符串,其余的值是用于格式化字符串的参数。请注意,这些额外参数必须是字符串值,而不是字符串列表,即不能直接传递$@

参数

示例

sample:
  quote: never
  script: |
    echo {fmt("Hi %s and %s", $1, $2)}

yamis sample person1 person2将导致echo Hi person1 and person2

trim函数

签名: trim<S: str | str[]>(value: S) -> S

从字符串或字符串列表中的每个字符串中删除前导和尾随空格(包括换行符)。

参数

  • value:要修剪的字符串或字符串列表

示例

sample:
  quote: never
  script: |
    echo {trim("  \n  hello world  \n")}

yamis sample将导致echo hello world

split函数

签名: split(split_val: str, split_string: str) -> str

使用给定的值拆分字符串

参数

  • split_val:拆分的值
  • split_string:要拆分的字符串

示例

sample:
  quote: never
  script: |
    echo {split(",", "a,b,c")}

yamis sample将导致echo a b c

常见问题解答(FAQ)

为什么不使用make、cargo-make或cmake等类似工具?

它们是优秀的工具,但有时简单更好。我创建这个工具是因为我不太适应现有的解决方案,特别是在参数解析方面,但你可以使用任何适合你需求的工具,不要有太多的情绪。此外,使用这个工具与其他工具一起使用不应有任何问题。

我可以定义自己的函数吗?

目前不可能,除非你分叉存储库并添加自己的(如果你了解Rust,这很容易做到)。你可以随时贡献,让每个人都能从中受益。虽然我有计划在未来添加更多功能,但允许自定义函数并不那么直接,可能不值得费力,也许更好的方法是使用单独的脚本,即Python。我欢迎任何建议。

贡献

请随时创建问题来报告错误、提问或请求更改。

你也可以分叉存储库来提出拉取请求,只需确保代码经过了良好的测试。首选已签名的提交。

依赖关系

~22–40MB
~641K SLoC