Go 初学者项目:用 Go 创建任务运行器
我们要构建什么
我们将制作一个类似“make”的工具,我们可以使用它使用像这样的简单 yaml 文件来运行任务。
tasks:
build:
description: "compile the project"
command: "go build main.go"
dependencies: [test]
test:
description: "run unit tests"
command: "go test -v ./..."让我们开始吧,首先我们需要概述行动方案。我们已经定义了任务文件模式。我们可以使用 json 而不是 yaml,但为了这个项目,我们将使用 yml 文件。
从文件中我们可以看到,我们需要一个结构来存储单个任务,以及在继续执行主任务之前运行依赖任务的方法。让我们从启动项目开始。创建一个新文件夹并运行:
go mod init github.com/vishaaxl/mommy
您可以随意命名您的项目,我将使用这个“妈妈”名称。我们还需要安装一些软件包来处理 yaml 文件 - 基本上将它们转换为地图对象。继续安装以下软件包。
go get gopkg.in/yaml.v3
接下来创建一个新的“main.go”文件并开始定义“Task”结构。
package main
import (
"gopkg.in/yaml.v3"
)
// Task defines the structure of a task in the configuration file.
// Each task has a description, a command to run, and a list of dependencies
// (other tasks that need to be completed before this task).
type Task struct {
Description string `yaml:"description"` // A brief description of the task.
Command string `yaml:"command"` // The shell command to execute for the task.
Dependencies []string `yaml:"dependencies"` // List of tasks that need to be completed before this task.
}这个很容易理解。它将保存每个单独任务的值。接下来我们需要一个结构来存储任务列表,并将 `.yaml` 文件的内容加载到这个新对象中。
// Config represents the entire configuration file,
// which contains a map of tasks by name.
type Config struct {
Tasks map[string]Task `yaml:"tasks"` // A map of task names to task details.
}
// loadConfig reads and parses the configuration file (e.g., Makefile.yaml),
// and returns a Config struct containing the tasks and their details.
func loadConfig(filename string) (Config, error) {
// Read the content of the config file.
data, err := os.ReadFile(filename)
if err != nil {
return Config{}, err
}
// Unmarshal the YAML data into a Config struct.
var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return Config{}, err
}
return config, nil
}接下来,我们需要创建一个执行单个任务的函数。我们将使用 `os/exec` 模块在 shell 中运行该任务。在 Golang 中,`os/exec` 包提供了一种执行 shell 命令和外部程序的方法。
// executeTask recursively executes the specified task and its dependencies.
// It first ensures that all dependencies are executed before running the current task's command.
func executeTask(taskName string, tasks map[string]Task, executed map[string]bool) error {
// If the task has already been executed, skip it.
if executed[taskName] {
return nil
}
// Get the task details from the tasks map.
task, exists := tasks[taskName]
if !exists {
return fmt.Errorf("task %s not found", taskName)
}
// First, execute all the dependencies of this task.
for _, dep := range task.Dependencies {
// Recursively execute each dependency.
if err := executeTask(dep, tasks, executed); err != nil {
return err
}
}
// Now that dependencies are executed, run the task's command.
fmt.Printf("Running task: %s\n", taskName)
fmt.Printf("Command: %s\n", task.Command)
// Execute the task's command using the shell (sh -c allows for complex shell commands).
cmd := exec.Command("sh", "-c", task.Command)
cmd.Stdout = os.Stdout // Direct standard output to the terminal.
cmd.Stderr = os.Stderr // Direct error output to the terminal.
// Run the command and check for any errors.
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute command %s: %v", task.Command, err)
}
// Mark the task as executed.
executed[taskName] = true
return nil
}现在我们已经有了程序的所有构建块,我们可以在主函数中使用它们来加载配置文件并开始自动化。我们将使用“flag”包来读取命令行标志。
func main() {
// Define command-line flags
configFile := flag.String("f", "Mommy.yaml", "Path to the configuration file") // Path to the config file (defaults to Makefile.yaml)
taskName := flag.String("task", "", "Task to execute") // The task to execute (required flag)
// Parse the flags
flag.Parse()
// Check if the task flag is provided
if *taskName == "" {
fmt.Println("Error: Please specify a task using -task flag.")
os.Exit(1) // Exit if no task is provided
}
// Load the configuration file
config, err := loadConfig(*configFile)
if err != nil {
fmt.Printf("Failed to load config: %v\n", err)
os.Exit(1) // Exit if the configuration file can't be loaded
}
// Map to track which tasks have been executed already (avoiding re-execution).
executed := make(map[string]bool)
// Start executing the specified task (with dependencies)
if err := executeTask(*taskName, config.Tasks, executed); err != nil {
fmt.Printf("Error executing task: %v\n", err)
os.Exit(1) // Exit if task execution fails
}
}让我们测试一下整个过程,创建一个新的“Mommy.yaml”并将 yaml 代码从头粘贴到其中。我们将使用任务运行器为我们的项目创建二进制文件。运行:
go run main.go -task build
如果一切顺利,您将在文件夹的根目录中看到一个新的 `.exe` 文件。太好了,我们现在有一个可以运行的任务运行器。我们可以在系统的环境变量中添加这个 `.exe` 文件的位置,然后从任何地方使用它:
mommy -task build
完整代码
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"gopkg.in/yaml.v3"
)
// Task defines the structure of a task in the configuration file.
// Each task has a description, a command to run, and a list of dependencies
// (other tasks that need to be completed before this task).
type Task struct {
Description string `yaml:"description"` // A brief description of the task.
Command string `yaml:"command"` // The shell command to execute for the task.
Dependencies []string `yaml:"dependencies"` // List of tasks that need to be completed before this task.
}
// Config represents the entire configuration file,
// which contains a map of tasks by name.
type Config struct {
Tasks map[string]Task `yaml:"tasks"` // A map of task names to task details.
}
// loadConfig reads and parses the configuration file (e.g., Makefile.yaml),
// and returns a Config struct containing the tasks and their details.
func loadConfig(filename string) (Config, error) {
// Read the content of the config file.
data, err := os.ReadFile(filename)
if err != nil {
return Config{}, err
}
// Unmarshal the YAML data into a Config struct.
var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return Config{}, err
}
return config, nil
}
// executeTask recursively executes the specified task and its dependencies.
// It first ensures that all dependencies are executed before running the current task's command.
func executeTask(taskName string, tasks map[string]Task, executed map[string]bool) error {
// If the task has already been executed, skip it.
if executed[taskName] {
return nil
}
// Get the task details from the tasks map.
task, exists := tasks[taskName]
if !exists {
return fmt.Errorf("task %s not found", taskName)
}
// First, execute all the dependencies of this task.
for _, dep := range task.Dependencies {
// Recursively execute each dependency.
if err := executeTask(dep, tasks, executed); err != nil {
return err
}
}
// Now that dependencies are executed, run the task's command.
fmt.Printf("Running task: %s\n", taskName)
fmt.Printf("Command: %s\n", task.Command)
// Execute the task's command using the shell (sh -c allows for complex shell commands).
cmd := exec.Command("sh", "-c", task.Command)
cmd.Stdout = os.Stdout // Direct standard output to the terminal.
cmd.Stderr = os.Stderr // Direct error output to the terminal.
// Run the command and check for any errors.
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to execute command %s: %v", task.Command, err)
}
// Mark the task as executed.
executed[taskName] = true
return nil
}
func main() {
// Define command-line flags
configFile := flag.String("f", "Makefile.yaml", "Path to the configuration file") // Path to the config file (defaults to Makefile.yaml)
taskName := flag.String("task", "", "Task to execute") // The task to execute (required flag)
// Parse the flags
flag.Parse()
// Check if the task flag is provided
if *taskName == "" {
fmt.Println("Error: Please specify a task using -task flag.")
os.Exit(1) // Exit if no task is provided
}
// Load the configuration file
config, err := loadConfig(*configFile)
if err != nil {
fmt.Printf("Failed to load config: %v\n", err)
os.Exit(1) // Exit if the configuration file can't be loaded
}
// Map to track which tasks have been executed already (avoiding re-execution).
executed := make(map[string]bool)
// Start executing the specified task (with dependencies)
if err := executeTask(*taskName, config.Tasks, executed); err != nil {
fmt.Printf("Error executing task: %v\n", err)
os.Exit(1) // Exit if task execution fails
}
}